perf: make admin navigation responsive

This commit is contained in:
ImBenji
2026-08-04 21:36:39 +01:00
parent 5680ee8a44
commit bd162606b6
15 changed files with 204 additions and 80 deletions
+34
View File
@@ -68,6 +68,40 @@ tbody tr:hover { background: rgba(255,255,255,.022); }
.empty-state { padding: 42px 24px; color: var(--muted-dark); text-align: center; }
.skeleton-row:hover { background: transparent; }
.skeleton-row td { height: 205px; padding: 22px 18px; vertical-align: top; }
.skeleton-row td span {
width: min(680px, 82%);
height: 11px;
margin-bottom: 20px;
display: block;
border-radius: 2px;
background: linear-gradient(90deg, #171b17 20%, #252b24 42%, #171b17 64%);
background-size: 300% 100%;
animation: skeleton-wave 1.6s ease infinite;
}
.skeleton-row td span:nth-child(2) { width: 58%; }
.skeleton-row td span:nth-child(3) { width: 72%; }
.skeleton-row td span:nth-child(4) { width: 45%; }
.content-loading { min-height: 160px; position: relative; }
.content-loading::after { content: "Loading"; position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted-dark); font-family: var(--mono); font-size: 9px; letter-spacing: .1em; text-transform: uppercase; }
@keyframes skeleton-wave { from { background-position: 100% 0; } to { background-position: 0 0; } }
html::after {
content: "";
position: fixed;
z-index: 500;
top: 0;
left: var(--rail);
width: 0;
height: 2px;
opacity: 0;
background: var(--accent);
box-shadow: 0 0 12px rgba(223,255,79,.45);
transition: width 220ms ease, opacity 120ms;
}
html.navigating::after { width: calc(100% - var(--rail)); opacity: 1; }
@media (max-width: 640px) {
.table-wrap { border-radius: 0; margin-inline: -16px; border-left: 0; border-right: 0; }
th, td { padding-inline: 11px; }
+49 -2
View File
@@ -148,7 +148,7 @@ async function loadGlobalStats() {
if (!bar) return;
try {
const data = await api("/admin/api/stats");
const data = await api("/admin/api/stats/summary");
const t = document.getElementById("s-total");
if (t) t.textContent = data.total.toLocaleString();
const c = document.getElementById("s-content");
@@ -161,6 +161,49 @@ async function loadGlobalStats() {
}
function installLoadingStates() {
document.querySelectorAll("tbody:empty").forEach(tbody => {
const columns = Math.max(1, tbody.closest("table")?.querySelectorAll("thead th").length || 5);
tbody.setAttribute("aria-busy", "true");
tbody.innerHTML = `<tr class="skeleton-row"><td colspan="${columns}"><span></span><span></span><span></span><span></span></td></tr>`;
const observer = new MutationObserver(() => {
if (!tbody.querySelector(".skeleton-row")) {
tbody.removeAttribute("aria-busy");
observer.disconnect();
}
});
observer.observe(tbody, { childList: true });
});
["intel-signals-grid", "sourceTable", "statusTable"].forEach(id => {
const node = document.getElementById(id);
if (node && !node.children.length) node.classList.add("content-loading");
});
}
function installNavigationPrefetch() {
const seen = new Set();
const prefetch = link => {
const href = link?.href;
if (!href || seen.has(href) || link.origin !== location.origin) return;
seen.add(href);
const hint = document.createElement("link");
hint.rel = "prefetch";
hint.href = href;
hint.as = "document";
document.head.appendChild(hint);
};
document.querySelectorAll(".tabs a, .subnav a").forEach(link => {
link.addEventListener("pointerenter", () => prefetch(link), { once: true });
link.addEventListener("focus", () => prefetch(link), { once: true });
link.addEventListener("click", () => document.documentElement.classList.add("navigating"));
});
const warm = () => document.querySelectorAll(".tabs a, .subnav a").forEach(prefetch);
if ("requestIdleCallback" in window) requestIdleCallback(warm, { timeout: 2500 });
else setTimeout(warm, 1200);
}
// common overlay close-on-backdrop wiring
function wireOverlays() {
document.querySelectorAll(".overlay").forEach(ov => {
@@ -173,6 +216,10 @@ function wireOverlays() {
document.addEventListener("DOMContentLoaded", () => {
installChrome();
installLoadingStates();
installNavigationPrefetch();
wireOverlays();
loadGlobalStats();
const loadStats = () => loadGlobalStats();
if ("requestIdleCallback" in window) requestIdleCallback(loadStats, { timeout: 1800 });
else setTimeout(loadStats, 500);
});
+1
View File
@@ -13,6 +13,7 @@ const signalsById = new Map();
function renderSignals(data) {
const grid = document.getElementById("intel-signals-grid");
const empty = document.getElementById("intel-signals-empty");
grid.classList.remove("content-loading");
if (!data || data.length === 0) {
grid.innerHTML = "";
+3
View File
@@ -4,6 +4,9 @@
async function loadStatsPage() {
const data = await api("/admin/api/stats");
document.getElementById("sourceTable").classList.remove("content-loading");
document.getElementById("statusTable").classList.remove("content-loading");
document.getElementById("sourceTable").innerHTML = data.bySource
.map(r => `<tr><td>${escapeHtml(r.source)}</td><td style="text-align:right; padding-left:24px">${r.n.toLocaleString()}</td></tr>`).join("");