// shared helpers + global chrome (stats bar) used by every admin page. // each page-specific script depends on this loading first. const PAGE = 50; const api = (path, opts) => fetch(path, opts).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); }); function toast(msg, err) { const el = document.getElementById("toast"); if (!el) return; document.getElementById("toast-msg").textContent = msg; el.className = "show" + (err ? " error" : ""); clearTimeout(toast._t); toast._t = setTimeout(() => el.className = "", 2500); } function badgeHtml(status) { const s = status || "null"; const cls = s === "ok" ? "ok" : s === "error" ? "err" : s === "pending" ? "pending" : "null"; return `${s}`; } function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function formatNumber(value, compact = false) { const number = Number(value || 0); return new Intl.NumberFormat("en-GB", compact ? { notation: "compact", maximumFractionDigits: 1 } : {}).format(number); } function formatPercent(value, digits = 1) { if (value == null || !Number.isFinite(Number(value))) return "—"; return `${(Number(value) * 100).toFixed(digits)}%`; } function formatRelative(value) { if (!value) return "—"; const timestamp = new Date(value.endsWith && value.endsWith("Z") ? value : `${value}Z`).getTime(); if (!Number.isFinite(timestamp)) return value; const seconds = Math.round((timestamp - Date.now()) / 1000); const units = [[86400, "day"], [3600, "hour"], [60, "minute"]]; const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); for (const [size, name] of units) { if (Math.abs(seconds) >= size) return formatter.format(Math.round(seconds / size), name); } return "just now"; } function installChrome() { const header = document.querySelector("header.app-header"); if (!header) return; const title = header.querySelector("h1"); if (title) title.innerHTML = `DDuriinAutonomous intelligence`; const nav = header.querySelector(".tabs"); if (nav && !nav.querySelector('[href="/admin/autonomy"]')) { nav.insertAdjacentHTML("afterbegin", 'Autonomy'); } if (nav) { const path = location.pathname; nav.querySelectorAll("a").forEach(link => { const href = link.getAttribute("href"); const active = href === "/admin/autonomy" ? path === href : href === "/admin/ingest" ? path.startsWith("/admin/ingest") : href === "/admin/intelligence" ? path.startsWith("/admin/intelligence") : path === href; link.classList.toggle("active", active); }); } const status = document.createElement("div"); status.className = "rail-status"; status.innerHTML = `
Connecting
Runtime status
`; header.appendChild(status); api("/admin/api/autonomy/overview").then(data => { const mode = String(data.mode || "offline").toUpperCase(); document.getElementById("rail-mode").textContent = data.enabled ? `${mode} runtime` : "Runtime unavailable"; document.getElementById("rail-meta").textContent = data.broker?.configured ? `${data.broker.name} · connected` : "Broker not configured"; document.querySelector(".rail-status-dot")?.classList.toggle("live", data.enabled); }).catch(() => { document.getElementById("rail-mode").textContent = "Runtime unavailable"; }); } // ── url query-param helpers ──────────────────────────────────────────────── // // filters and sort state live in the url so reloads and shared links keep // their shape. queryGet reads a single param, queryAll returns them all, // queryWrite replaces the query string with the cleaned-up params (empty // values removed). we use replaceState so each filter change doesnt spam // history. function queryGet(key, fallback = "") { const v = new URLSearchParams(location.search).get(key); return v == null ? fallback : v; } function queryAll() { return Object.fromEntries(new URLSearchParams(location.search)); } function queryWrite(params) { const clean = {}; for (const [k, v] of Object.entries(params)) { if (v === "" || v == null) continue; clean[k] = v; } const qs = new URLSearchParams(clean).toString(); const next = location.pathname + (qs ? "?" + qs : ""); history.replaceState(null, "", next); } // apply current query params onto form inputs — call on page init function queryApplyToInputs(bindings) { for (const [id, key] of Object.entries(bindings)) { const el = document.getElementById(id); if (!el) continue; const v = queryGet(key, ""); if (v !== "") el.value = v; } } // global stats bar — small counters shown on articles/events/stats pages async function loadGlobalStats() { const bar = document.getElementById("statsBar"); if (!bar) return; try { const data = await api("/admin/api/stats"); const t = document.getElementById("s-total"); if (t) t.textContent = data.total.toLocaleString(); const c = document.getElementById("s-content"); if (c) c.textContent = data.withContent.toLocaleString(); const em = document.getElementById("s-embed"); if (em) em.textContent = data.withEmbedding.toLocaleString(); const ev = document.getElementById("s-events"); if (ev) ev.textContent = data.eventCount.toLocaleString(); } catch (_) { /* ignore — stats bar is best-effort */ } } // common overlay close-on-backdrop wiring function wireOverlays() { document.querySelectorAll(".overlay").forEach(ov => { ov.addEventListener("click", e => { if (e.target === ov) ov.classList.remove("open"); }); }); } document.addEventListener("DOMContentLoaded", () => { installChrome(); wireOverlays(); loadGlobalStats(); });