Files
Duriin-API/public/admin/assets/js/app.js
T

179 lines
6.1 KiB
JavaScript

// 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 `<span class="badge ${cls}">${s}</span>`;
}
function escapeHtml(s) {
return String(s ?? "").replace(/[&<>"']/g, c => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
}[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 = `<span class="brand-mark">D</span><span class="brand-copy">Duriin<span>Autonomous intelligence</span></span>`;
const nav = header.querySelector(".tabs");
if (nav && !nav.querySelector('[href="/admin/autonomy"]')) {
nav.insertAdjacentHTML("afterbegin", '<a href="/admin/autonomy">Autonomy</a>');
}
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 = `<div class="rail-status-label"><span class="rail-status-dot"></span><span id="rail-mode">Connecting</span></div><div class="rail-status-meta" id="rail-meta">Runtime status</div>`;
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();
});