add intelligence features; implement signals and predictions pages in admin panel
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// 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 => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
|
||||
// ── 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", () => {
|
||||
wireOverlays();
|
||||
loadGlobalStats();
|
||||
});
|
||||
Reference in New Issue
Block a user