Skip to content

Commit 9912f6b

Browse files
testing commit message functionality
1 parent cb4cc28 commit 9912f6b

File tree

1 file changed

+166
-74
lines changed

1 file changed

+166
-74
lines changed

meshview/templates/nodelist.html

Lines changed: 166 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@
103103
font-weight: bold;
104104
color: white;
105105
}
106+
.node-status {
107+
margin-left: 10px;
108+
padding: 2px 8px;
109+
border-radius: 12px;
110+
border: 1px solid #2a6a8a;
111+
background: #0d2a3a;
112+
color: #9fd4ff;
113+
font-size: 0.9em;
114+
display: inline-block;
115+
opacity: 0;
116+
transition: opacity 0.15s ease-in-out;
117+
}
118+
.node-status.active {
119+
opacity: 1;
120+
}
106121

107122
/* Favorite stars */
108123
.favorite-star {
@@ -235,6 +250,7 @@
235250
<span data-translate-lang="showing_nodes">Showing</span>
236251
<span id="node-count">0</span>
237252
<span data-translate-lang="nodes_suffix">nodes</span>
253+
<span id="node-status" class="node-status" aria-live="polite"></span>
238254
</div>
239255

240256
<!-- Desktop table -->
@@ -305,6 +321,11 @@
305321
let sortColumn = "short_name";
306322
let sortAsc = true;
307323
let showOnlyFavorites = false;
324+
let favoritesSet = new Set();
325+
let isBusy = false;
326+
let statusHideTimer = null;
327+
let statusShownAt = 0;
328+
const minStatusMs = 300;
308329

309330
const headers = document.querySelectorAll("thead th");
310331
const keyMap = [
@@ -320,22 +341,38 @@
320341
};
321342
}
322343

323-
function getFavorites() {
344+
function nextFrame() {
345+
return new Promise(resolve => requestAnimationFrame(() => resolve()));
346+
}
347+
348+
function loadFavorites() {
324349
const favorites = localStorage.getItem('nodelist_favorites');
325-
return favorites ? JSON.parse(favorites) : [];
350+
if (!favorites) {
351+
favoritesSet = new Set();
352+
return;
353+
}
354+
355+
try {
356+
const parsed = JSON.parse(favorites);
357+
favoritesSet = new Set(Array.isArray(parsed) ? parsed : []);
358+
} catch (err) {
359+
console.warn("Failed to parse favorites, resetting.", err);
360+
favoritesSet = new Set();
361+
}
326362
}
327-
function saveFavorites(favs) {
328-
localStorage.setItem('nodelist_favorites', JSON.stringify(favs));
363+
function saveFavorites() {
364+
localStorage.setItem('nodelist_favorites', JSON.stringify([...favoritesSet]));
329365
}
330366
function toggleFavorite(nodeId) {
331-
let favs = getFavorites();
332-
const idx = favs.indexOf(nodeId);
333-
if (idx >= 0) favs.splice(idx, 1);
334-
else favs.push(nodeId);
335-
saveFavorites(favs);
367+
if (favoritesSet.has(nodeId)) {
368+
favoritesSet.delete(nodeId);
369+
} else {
370+
favoritesSet.add(nodeId);
371+
}
372+
saveFavorites();
336373
}
337374
function isFavorite(nodeId) {
338-
return getFavorites().includes(nodeId);
375+
return favoritesSet.has(nodeId);
339376
}
340377

341378
function timeAgoFromMs(msTimestamp) {
@@ -357,6 +394,7 @@
357394
document.addEventListener("DOMContentLoaded", async function() {
358395

359396
await loadTranslationsNodelist();
397+
loadFavorites();
360398

361399
const tbody = document.getElementById("node-table-body");
362400
const mobileList = document.getElementById("mobile-node-list");
@@ -367,13 +405,16 @@
367405
const firmwareFilter = document.getElementById("firmware-filter");
368406
const searchBox = document.getElementById("search-box");
369407
const countSpan = document.getElementById("node-count");
408+
const statusSpan = document.getElementById("node-status");
370409
const exportBtn = document.getElementById("export-btn");
371410
const clearBtn = document.getElementById("clear-btn");
372411
const favoritesBtn = document.getElementById("favorites-btn");
373412

374413
let lastIsMobile = (window.innerWidth <= 768);
375414

376415
try {
416+
setStatus("Loading nodes…");
417+
await nextFrame();
377418
const res = await fetch("/api/nodes?days_active=3");
378419
if (!res.ok) throw new Error("Failed to fetch nodes");
379420

@@ -404,11 +445,13 @@
404445
populateFilters(allNodes);
405446
applyFilters(); // ensures initial sort + render uses same path
406447
updateSortIcons();
448+
setStatus("");
407449
} catch (err) {
408450
tbody.innerHTML = `<tr>
409451
<td colspan="10" style="text-align:center; color:red;">
410452
${nodelistTranslations.error_loading_nodes || "Error loading nodes"}
411453
</td></tr>`;
454+
setStatus("");
412455
return;
413456
}
414457

@@ -499,7 +542,9 @@
499542
applyFilters();
500543
}
501544

502-
function applyFilters() {
545+
async function applyFilters() {
546+
setStatus("Updating…");
547+
await nextFrame();
503548
const searchTerm = searchBox.value.trim().toLowerCase();
504549

505550
let filtered = allNodes.filter(n => {
@@ -519,27 +564,34 @@
519564

520565
renderTable(filtered);
521566
updateSortIcons();
567+
setStatus("");
522568
}
523569

524570
function renderTable(nodes) {
525-
tbody.innerHTML = "";
526-
mobileList.innerHTML = "";
571+
const isMobile = window.innerWidth <= 768;
572+
const shouldRenderTable = !isMobile;
527573

528-
const tableFrag = document.createDocumentFragment();
529-
const mobileFrag = document.createDocumentFragment();
574+
if (shouldRenderTable) {
575+
tbody.innerHTML = "";
576+
} else {
577+
mobileList.innerHTML = "";
578+
}
530579

531-
const isMobile = window.innerWidth <= 768;
580+
const tableFrag = shouldRenderTable ? document.createDocumentFragment() : null;
581+
const mobileFrag = shouldRenderTable ? null : document.createDocumentFragment();
532582

533583
if (!nodes.length) {
534-
tbody.innerHTML = `<tr>
535-
<td colspan="10" style="text-align:center; color:white;">
584+
if (shouldRenderTable) {
585+
tbody.innerHTML = `<tr>
586+
<td colspan="10" style="text-align:center; color:white;">
587+
${nodelistTranslations.no_nodes_found || "No nodes found"}
588+
</td>
589+
</tr>`;
590+
} else {
591+
mobileList.innerHTML = `<div style="text-align:center; color:white;">
536592
${nodelistTranslations.no_nodes_found || "No nodes found"}
537-
</td>
538-
</tr>`;
539-
540-
mobileList.innerHTML = `<div style="text-align:center; color:white;">
541-
${nodelistTranslations.no_nodes_found || "No nodes found"}
542-
</div>`;
593+
</div>`;
594+
}
543595

544596
countSpan.textContent = 0;
545597
return;
@@ -549,63 +601,68 @@
549601
const fav = isFavorite(node.node_id);
550602
const star = fav ? "★" : "☆";
551603

552-
// DESKTOP TABLE ROW
553-
const row = document.createElement("tr");
554-
row.innerHTML = `
555-
<td>${node.short_name || "N/A"}</td>
556-
<td><a href="/node/${node.node_id}">${node.long_name || "N/A"}</a></td>
557-
<td>${node.hw_model || "N/A"}</td>
558-
<td>${node.firmware || "N/A"}</td>
559-
<td>${node.role || "N/A"}</td>
560-
<td>${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"}</td>
561-
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
562-
<td>${node.channel || "N/A"}</td>
563-
<td>${timeAgoFromMs(node.last_seen_ms)}</td>
564-
<td style="text-align:center;">
565-
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
566-
${star}
567-
</span>
568-
</td>
569-
`;
570-
tableFrag.appendChild(row);
571-
572-
// MOBILE CARD VIEW
573-
const card = document.createElement("div");
574-
card.className = "node-card";
575-
card.innerHTML = `
576-
<div class="node-card-header">
577-
<span>${node.short_name || node.long_name || node.node_id}</span>
578-
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
579-
${star}
580-
</span>
581-
</div>
582-
583-
<div class="node-card-field"><b>ID:</b> ${node.node_id}</div>
584-
<div class="node-card-field"><b>Name:</b> ${node.long_name || "N/A"}</div>
585-
<div class="node-card-field"><b>HW:</b> ${node.hw_model || "N/A"}</div>
586-
<div class="node-card-field"><b>Firmware:</b> ${node.firmware || "N/A"}</div>
587-
<div class="node-card-field"><b>Role:</b> ${node.role || "N/A"}</div>
588-
<div class="node-card-field"><b>Location:</b>
589-
${node.last_lat ? (node.last_lat / 1e7).toFixed(5) : "N/A"},
590-
${node.last_long ? (node.last_long / 1e7).toFixed(5) : "N/A"}
591-
</div>
592-
<div class="node-card-field"><b>Channel:</b> ${node.channel || "N/A"}</div>
593-
<div class="node-card-field"><b>Last Seen:</b> ${timeAgoFromMs(node.last_seen_ms)}</div>
594-
595-
<a href="/node/${node.node_id}" style="color:#9fd4ff; text-decoration:underline; margin-top:5px; display:block;">
596-
View Node →
597-
</a>
598-
`;
599-
mobileFrag.appendChild(card);
604+
if (shouldRenderTable) {
605+
// DESKTOP TABLE ROW
606+
const row = document.createElement("tr");
607+
row.innerHTML = `
608+
<td>${node.short_name || "N/A"}</td>
609+
<td><a href="/node/${node.node_id}">${node.long_name || "N/A"}</a></td>
610+
<td>${node.hw_model || "N/A"}</td>
611+
<td>${node.firmware || "N/A"}</td>
612+
<td>${node.role || "N/A"}</td>
613+
<td>${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"}</td>
614+
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
615+
<td>${node.channel || "N/A"}</td>
616+
<td>${timeAgoFromMs(node.last_seen_ms)}</td>
617+
<td style="text-align:center;">
618+
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
619+
${star}
620+
</span>
621+
</td>
622+
`;
623+
tableFrag.appendChild(row);
624+
} else {
625+
// MOBILE CARD VIEW
626+
const card = document.createElement("div");
627+
card.className = "node-card";
628+
card.innerHTML = `
629+
<div class="node-card-header">
630+
<span>${node.short_name || node.long_name || node.node_id}</span>
631+
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">
632+
${star}
633+
</span>
634+
</div>
635+
636+
<div class="node-card-field"><b>ID:</b> ${node.node_id}</div>
637+
<div class="node-card-field"><b>Name:</b> ${node.long_name || "N/A"}</div>
638+
<div class="node-card-field"><b>HW:</b> ${node.hw_model || "N/A"}</div>
639+
<div class="node-card-field"><b>Firmware:</b> ${node.firmware || "N/A"}</div>
640+
<div class="node-card-field"><b>Role:</b> ${node.role || "N/A"}</div>
641+
<div class="node-card-field"><b>Location:</b>
642+
${node.last_lat ? (node.last_lat / 1e7).toFixed(5) : "N/A"},
643+
${node.last_long ? (node.last_long / 1e7).toFixed(5) : "N/A"}
644+
</div>
645+
<div class="node-card-field"><b>Channel:</b> ${node.channel || "N/A"}</div>
646+
<div class="node-card-field"><b>Last Seen:</b> ${timeAgoFromMs(node.last_seen_ms)}</div>
647+
648+
<a href="/node/${node.node_id}" style="color:#9fd4ff; text-decoration:underline; margin-top:5px; display:block;">
649+
View Node →
650+
</a>
651+
`;
652+
mobileFrag.appendChild(card);
653+
}
600654
});
601655

602656
// Toggle correct view
603657
mobileList.style.display = isMobile ? "block" : "none";
604658

605659
countSpan.textContent = nodes.length;
606660

607-
tbody.appendChild(tableFrag);
608-
mobileList.appendChild(mobileFrag);
661+
if (shouldRenderTable) {
662+
tbody.appendChild(tableFrag);
663+
} else {
664+
mobileList.appendChild(mobileFrag);
665+
}
609666
}
610667

611668
function clearFilters() {
@@ -676,6 +733,41 @@
676733
keyMap[i] === sortColumn ? (sortAsc ? "▲" : "▼") : "";
677734
});
678735
}
736+
737+
function setStatus(message) {
738+
if (!statusSpan) return;
739+
if (statusHideTimer) {
740+
clearTimeout(statusHideTimer);
741+
statusHideTimer = null;
742+
}
743+
744+
if (message) {
745+
statusShownAt = Date.now();
746+
console.log("[nodelist] status:", message);
747+
statusSpan.textContent = message;
748+
statusSpan.classList.add("active");
749+
isBusy = true;
750+
return;
751+
}
752+
753+
const elapsed = Date.now() - statusShownAt;
754+
const remaining = Math.max(0, minStatusMs - elapsed);
755+
if (remaining > 0) {
756+
statusHideTimer = setTimeout(() => {
757+
statusHideTimer = null;
758+
console.log("[nodelist] status: cleared");
759+
statusSpan.textContent = "";
760+
statusSpan.classList.remove("active");
761+
isBusy = false;
762+
}, remaining);
763+
return;
764+
}
765+
766+
console.log("[nodelist] status: cleared");
767+
statusSpan.textContent = "";
768+
statusSpan.classList.remove("active");
769+
isBusy = false;
770+
}
679771
});
680772
</script>
681773
{% endblock %}

0 commit comments

Comments
 (0)