diff --git a/public/admin/assets/css/components.css b/public/admin/assets/css/components.css
index 214e404..f8487c0 100644
--- a/public/admin/assets/css/components.css
+++ b/public/admin/assets/css/components.css
@@ -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; }
diff --git a/public/admin/assets/js/app.js b/public/admin/assets/js/app.js
index cc61c01..98b3505 100644
--- a/public/admin/assets/js/app.js
+++ b/public/admin/assets/js/app.js
@@ -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 = `
|
`;
+ 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);
});
diff --git a/public/admin/assets/js/intel-signals.js b/public/admin/assets/js/intel-signals.js
index ffc7a08..e31f363 100644
--- a/public/admin/assets/js/intel-signals.js
+++ b/public/admin/assets/js/intel-signals.js
@@ -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 = "";
diff --git a/public/admin/assets/js/stats.js b/public/admin/assets/js/stats.js
index 64c3640..e9ad3f6 100644
--- a/public/admin/assets/js/stats.js
+++ b/public/admin/assets/js/stats.js
@@ -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 => `| ${escapeHtml(r.source)} | ${r.n.toLocaleString()} |
`).join("");
diff --git a/public/admin/pages/autonomy.html b/public/admin/pages/autonomy.html
index b7dd3dd..0b4b6cd 100644
--- a/public/admin/pages/autonomy.html
+++ b/public/admin/pages/autonomy.html
@@ -5,10 +5,10 @@
Duriin — Autonomy
-
-
-
-
+
+
+
+
@@ -131,7 +131,7 @@
-
-
+
+