diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index 0e127e6..de79361 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -11,7 +11,7 @@ export function registerRenderer(fn) { } export async function loadWithOptions(options = {}) { - const { force = false } = options; + const { force = false, reorder = false } = options; if (state.isLoading) { state.pendingForce = state.pendingForce || force; return; @@ -38,7 +38,7 @@ export async function loadWithOptions(options = {}) { return; } state.lastDigest = digest; - render(data); + render(data, { reorder }); if (!state.thumbnailsRequested) { state.thumbnailsRequested = true; diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index d823983..1582ca2 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -411,7 +411,7 @@ wireModalClose(); const debouncedSearch = debounce(() => { state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }, 200); searchInput?.addEventListener("input", (e) => { @@ -423,7 +423,7 @@ sortSelect?.addEventListener("change", (e) => { state.sort = e.target.value; state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); filterStatusSelect?.addEventListener("change", (e) => { @@ -436,21 +436,21 @@ filterStatusSelect?.addEventListener("change", (e) => { } state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); filterOsSelect?.addEventListener("change", (e) => { state.filterOs = e.target.value; state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); initCountryPicker((code) => { state.filterCountry = code; state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); showOfflineToggle?.addEventListener("change", (e) => { @@ -467,7 +467,7 @@ showOfflineToggle?.addEventListener("change", (e) => { } state.page = 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); function updateBulkToolbar() { @@ -686,14 +686,14 @@ prevBtn?.addEventListener("click", () => { if (state.page > 1) { state.page -= 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); } }); nextBtn?.addEventListener("click", () => { state.page += 1; state.lastDigest = ""; - loadWithOptions({ force: true }); + loadWithOptions({ force: true, reorder: true }); }); window.addEventListener("click", (e) => { diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index a4d9482..fe61b99 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -33,7 +33,8 @@ export function createRenderer({ const TOUCH_MOVE_CANCEL_PX = 10; let renderToken = 0; - function renderMerge(data) { + function renderMerge(data, options = {}) { + const { reorder = false } = options; totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`; const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); pageLabel.textContent = `Page ${data.page} of ${totalPages}`; @@ -47,8 +48,8 @@ export function createRenderer({ typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - const animateLimit = Math.min(items.length, MAX_ANIMATED_CARDS); - const allowAnimation = !prefersReducedMotion && items.length <= 1000; + + const hadCards = grid.querySelectorAll("article[data-id]").length > 0; items.forEach((client) => { seen.add(client.id); @@ -64,11 +65,20 @@ export function createRenderer({ .filter((el) => !seen.has(el.dataset.id)) .forEach((el) => el.remove()); + if (reorder) { + items.forEach((client) => { + const card = grid.querySelector(`article[data-id="${client.id}"]`); + if (card) grid.appendChild(card); + }); + } + if (newClients.length === 0) { - moveOfflineToBottom(); return; } + const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000; + const animateLimit = Math.min(newClients.length, MAX_ANIMATED_CARDS); + let idx = 0; const insertBatch = () => { if (renderId !== renderToken) return; @@ -91,35 +101,11 @@ export function createRenderer({ requestAnimationFrame(insertBatch); return; } - - moveOfflineToBottom(); }; insertBatch(); } - function moveOfflineToBottom() { - const cards = Array.from(grid.querySelectorAll("article[data-id]")); - if (cards.length === 0) return; - - const online = []; - const offline = []; - cards.forEach((card) => { - if (card.dataset.online === "true") { - online.push(card); - return; - } - offline.push(card); - }); - - if (offline.length === 0) return; - - const fragment = document.createDocumentFragment(); - online.forEach((card) => fragment.appendChild(card)); - offline.forEach((card) => fragment.appendChild(card)); - grid.appendChild(fragment); - } - function prevBtnState(currentPage, totalPages) { const prevBtn = document.getElementById("prev"); const nextBtn = document.getElementById("next"); diff --git a/Overlord-Server/public/assets/state.js b/Overlord-Server/public/assets/state.js index 843bcc9..3d32f3e 100644 --- a/Overlord-Server/public/assets/state.js +++ b/Overlord-Server/public/assets/state.js @@ -2,7 +2,7 @@ export const state = { page: 1, pageSize: 12, searchTerm: "", - sort: "last_seen_desc", + sort: "stable", filterStatus: "all", filterOs: "all", filterCountry: "all", diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index 5f921f4..05d4126 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -92,6 +92,7 @@ id="sort" class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-slate-100" > + diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index 28d08f3..ef100bd 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -348,6 +348,8 @@ export function listClients(filters: ListFilters): ListResult { const orderBy = (() => { const bookmark = "bookmarked DESC"; switch (sort) { + case "stable": + return `ORDER BY ${bookmark}, online DESC, id ASC`; case "ping_asc": return `ORDER BY ${bookmark}, ping_ms IS NULL, ping_ms ASC, id ASC`; case "ping_desc":