From cb4cc281c69c28e27310f764eb1ea29be355d185 Mon Sep 17 00:00:00 2001 From: pablorevilla-meshtastic Date: Thu, 8 Jan 2026 17:38:56 -0800 Subject: [PATCH] fix speed of node list rendering --- meshview/templates/nodelist.html | 236 ++++++++++--------------------- 1 file changed, 73 insertions(+), 163 deletions(-) diff --git a/meshview/templates/nodelist.html b/meshview/templates/nodelist.html index 593b0b0..c68c1ba 100644 --- a/meshview/templates/nodelist.html +++ b/meshview/templates/nodelist.html @@ -26,7 +26,6 @@ table { width: max-content; /* table keeps its natural width */ min-width: 100%; /* won't shrink smaller than viewport */ } - th, td { padding: 10px; border: 1px solid #333; @@ -104,21 +103,6 @@ select, .export-btn, .search-box, .clear-btn { font-weight: bold; color: white; } -.node-status { - margin-left: 10px; - padding: 2px 8px; - border-radius: 12px; - border: 1px solid #2a6a8a; - background: #0d2a3a; - color: #9fd4ff; - font-size: 0.9em; - display: inline-block; - opacity: 0; - transition: opacity 0.15s ease-in-out; -} -.node-status.active { - opacity: 1; -} /* Favorite stars */ .favorite-star { @@ -251,7 +235,6 @@ select, .export-btn, .search-box, .clear-btn { Showing 0 nodes - @@ -322,11 +305,6 @@ let allNodes = []; let sortColumn = "short_name"; let sortAsc = true; let showOnlyFavorites = false; -let favoritesSet = new Set(); -let isBusy = false; -let statusHideTimer = null; -let statusShownAt = 0; -const minStatusMs = 300; const headers = document.querySelectorAll("thead th"); const keyMap = [ @@ -342,38 +320,22 @@ function debounce(fn, delay = 250) { }; } -function nextFrame() { - return new Promise(resolve => requestAnimationFrame(() => resolve())); -} - -function loadFavorites() { +function getFavorites() { const favorites = localStorage.getItem('nodelist_favorites'); - if (!favorites) { - favoritesSet = new Set(); - return; - } - - try { - const parsed = JSON.parse(favorites); - favoritesSet = new Set(Array.isArray(parsed) ? parsed : []); - } catch (err) { - console.warn("Failed to parse favorites, resetting.", err); - favoritesSet = new Set(); - } + return favorites ? JSON.parse(favorites) : []; } -function saveFavorites() { - localStorage.setItem('nodelist_favorites', JSON.stringify([...favoritesSet])); +function saveFavorites(favs) { + localStorage.setItem('nodelist_favorites', JSON.stringify(favs)); } function toggleFavorite(nodeId) { - if (favoritesSet.has(nodeId)) { - favoritesSet.delete(nodeId); - } else { - favoritesSet.add(nodeId); - } - saveFavorites(); + let favs = getFavorites(); + const idx = favs.indexOf(nodeId); + if (idx >= 0) favs.splice(idx, 1); + else favs.push(nodeId); + saveFavorites(favs); } function isFavorite(nodeId) { - return favoritesSet.has(nodeId); + return getFavorites().includes(nodeId); } function timeAgoFromMs(msTimestamp) { @@ -395,7 +357,6 @@ function timeAgoFromMs(msTimestamp) { document.addEventListener("DOMContentLoaded", async function() { await loadTranslationsNodelist(); - loadFavorites(); const tbody = document.getElementById("node-table-body"); const mobileList = document.getElementById("mobile-node-list"); @@ -406,7 +367,6 @@ document.addEventListener("DOMContentLoaded", async function() { const firmwareFilter = document.getElementById("firmware-filter"); const searchBox = document.getElementById("search-box"); const countSpan = document.getElementById("node-count"); - const statusSpan = document.getElementById("node-status"); const exportBtn = document.getElementById("export-btn"); const clearBtn = document.getElementById("clear-btn"); const favoritesBtn = document.getElementById("favorites-btn"); @@ -414,8 +374,6 @@ document.addEventListener("DOMContentLoaded", async function() { let lastIsMobile = (window.innerWidth <= 768); try { - setStatus("Loading nodes…"); - await nextFrame(); const res = await fetch("/api/nodes?days_active=3"); if (!res.ok) throw new Error("Failed to fetch nodes"); @@ -446,13 +404,11 @@ document.addEventListener("DOMContentLoaded", async function() { populateFilters(allNodes); applyFilters(); // ensures initial sort + render uses same path updateSortIcons(); - setStatus(""); } catch (err) { tbody.innerHTML = ` ${nodelistTranslations.error_loading_nodes || "Error loading nodes"} `; - setStatus(""); return; } @@ -543,9 +499,7 @@ document.addEventListener("DOMContentLoaded", async function() { applyFilters(); } - async function applyFilters() { - setStatus("Updating…"); - await nextFrame(); + function applyFilters() { const searchTerm = searchBox.value.trim().toLowerCase(); let filtered = allNodes.filter(n => { @@ -565,34 +519,27 @@ document.addEventListener("DOMContentLoaded", async function() { renderTable(filtered); updateSortIcons(); - setStatus(""); } function renderTable(nodes) { + tbody.innerHTML = ""; + mobileList.innerHTML = ""; + + const tableFrag = document.createDocumentFragment(); + const mobileFrag = document.createDocumentFragment(); + const isMobile = window.innerWidth <= 768; - const shouldRenderTable = !isMobile; - - if (shouldRenderTable) { - tbody.innerHTML = ""; - } else { - mobileList.innerHTML = ""; - } - - const tableFrag = shouldRenderTable ? document.createDocumentFragment() : null; - const mobileFrag = shouldRenderTable ? null : document.createDocumentFragment(); if (!nodes.length) { - if (shouldRenderTable) { - tbody.innerHTML = ` - - ${nodelistTranslations.no_nodes_found || "No nodes found"} - - `; - } else { - mobileList.innerHTML = `
+ tbody.innerHTML = ` + ${nodelistTranslations.no_nodes_found || "No nodes found"} -
`; - } + + `; + + mobileList.innerHTML = `
+ ${nodelistTranslations.no_nodes_found || "No nodes found"} +
`; countSpan.textContent = 0; return; @@ -602,56 +549,54 @@ document.addEventListener("DOMContentLoaded", async function() { const fav = isFavorite(node.node_id); const star = fav ? "★" : "☆"; - if (shouldRenderTable) { - // DESKTOP TABLE ROW - const row = document.createElement("tr"); - row.innerHTML = ` - ${node.short_name || "N/A"} - ${node.long_name || "N/A"} - ${node.hw_model || "N/A"} - ${node.firmware || "N/A"} - ${node.role || "N/A"} - ${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"} - ${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"} - ${node.channel || "N/A"} - ${timeAgoFromMs(node.last_seen_ms)} - - - ${star} - - - `; - tableFrag.appendChild(row); - } else { - // MOBILE CARD VIEW - const card = document.createElement("div"); - card.className = "node-card"; - card.innerHTML = ` -
- ${node.short_name || node.long_name || node.node_id} - - ${star} - -
+ // DESKTOP TABLE ROW + const row = document.createElement("tr"); + row.innerHTML = ` + ${node.short_name || "N/A"} + ${node.long_name || "N/A"} + ${node.hw_model || "N/A"} + ${node.firmware || "N/A"} + ${node.role || "N/A"} + ${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"} + ${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"} + ${node.channel || "N/A"} + ${timeAgoFromMs(node.last_seen_ms)} + + + ${star} + + + `; + tableFrag.appendChild(row); -
ID: ${node.node_id}
-
Name: ${node.long_name || "N/A"}
-
HW: ${node.hw_model || "N/A"}
-
Firmware: ${node.firmware || "N/A"}
-
Role: ${node.role || "N/A"}
-
Location: - ${node.last_lat ? (node.last_lat / 1e7).toFixed(5) : "N/A"}, - ${node.last_long ? (node.last_long / 1e7).toFixed(5) : "N/A"} -
-
Channel: ${node.channel || "N/A"}
-
Last Seen: ${timeAgoFromMs(node.last_seen_ms)}
+ // MOBILE CARD VIEW + const card = document.createElement("div"); + card.className = "node-card"; + card.innerHTML = ` +
+ ${node.short_name || node.long_name || node.node_id} + + ${star} + +
- - View Node → - - `; - mobileFrag.appendChild(card); - } +
ID: ${node.node_id}
+
Name: ${node.long_name || "N/A"}
+
HW: ${node.hw_model || "N/A"}
+
Firmware: ${node.firmware || "N/A"}
+
Role: ${node.role || "N/A"}
+
Location: + ${node.last_lat ? (node.last_lat / 1e7).toFixed(5) : "N/A"}, + ${node.last_long ? (node.last_long / 1e7).toFixed(5) : "N/A"} +
+
Channel: ${node.channel || "N/A"}
+
Last Seen: ${timeAgoFromMs(node.last_seen_ms)}
+ + + View Node → + + `; + mobileFrag.appendChild(card); }); // Toggle correct view @@ -659,11 +604,8 @@ document.addEventListener("DOMContentLoaded", async function() { countSpan.textContent = nodes.length; - if (shouldRenderTable) { - tbody.appendChild(tableFrag); - } else { - mobileList.appendChild(mobileFrag); - } + tbody.appendChild(tableFrag); + mobileList.appendChild(mobileFrag); } function clearFilters() { @@ -734,38 +676,6 @@ document.addEventListener("DOMContentLoaded", async function() { keyMap[i] === sortColumn ? (sortAsc ? "▲" : "▼") : ""; }); } - - function setStatus(message) { - if (!statusSpan) return; - if (statusHideTimer) { - clearTimeout(statusHideTimer); - statusHideTimer = null; - } - - if (message) { - statusShownAt = Date.now(); - statusSpan.textContent = message; - statusSpan.classList.add("active"); - isBusy = true; - return; - } - - const elapsed = Date.now() - statusShownAt; - const remaining = Math.max(0, minStatusMs - elapsed); - if (remaining > 0) { - statusHideTimer = setTimeout(() => { - statusHideTimer = null; - statusSpan.textContent = ""; - statusSpan.classList.remove("active"); - isBusy = false; - }, remaining); - return; - } - - statusSpan.textContent = ""; - statusSpan.classList.remove("active"); - isBusy = false; - } }); {% endblock %}