Files
brain-of-reese/frontend/assets/sources.js
T

126 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Brain of Reese — Sources page (knowledge base index view).
*
* Wires the real `GET /api/docs` endpoint (import phase): stat cards +
* full-width document table, or the designed empty state when nothing is
* indexed yet. Cells are built with DOM APIs (textContent) — never
* innerHTML with document-derived data (XSS-safe by construction).
*/
const tbody = document.querySelector("#docs-tbody");
const emptyEl = document.querySelector("#sources-empty");
const tableWrap = document.querySelector(".table-wrap");
const statCards = document.querySelector("#stat-cards");
const gateEl = document.querySelector("#sources-gate");
const statDocs = document.querySelector("#stat-docs");
const statChunks = document.querySelector("#stat-chunks");
const statLast = document.querySelector("#stat-last");
/* Phase 16: whoami BEFORE the docs fetch. Anonymous visitors get the
* sign-in gate (stat cards + table hidden) and NO /api/docs call — the
* catalog is admin-only. The document viewer itself stays public (the
* soft rule), so the gate copy points at what keeps working. */
async function isAdmin() {
try {
const r = await fetch("/api/whoami");
if (r.ok) return (await r.json()).authenticated === true;
} catch { /* API unreachable: anonymous-safe gate */ }
return false;
}
function fmtDate(iso) {
try {
return new Date(iso).toLocaleString();
} catch {
return iso;
}
}
/* Viewer link (phase 10) — same encoded URL the chat chips use; both query
* values are percent-encoded (paths contain slashes, sometimes spaces). */
export function documentUrl(source, path) {
return "/document.html?source=" + encodeURIComponent(source) + "&path=" + encodeURIComponent(path);
}
async function loadDocs() {
let r;
try {
r = await fetch("/api/docs");
} catch {
showEmpty();
return;
}
if (!r.ok) {
showEmpty();
return;
}
const { documents } = await r.json();
if (!documents.length) {
showEmpty();
return;
}
tbody.replaceChildren();
let totalChunks = 0;
let last = "";
for (const d of documents) {
totalChunks += d.chunks;
if (d.indexed_at > last) last = d.indexed_at;
tbody.appendChild(makeRow(d));
}
statDocs.textContent = String(documents.length);
statChunks.textContent = String(totalChunks);
statLast.textContent = last ? fmtDate(last) : "–";
emptyEl.hidden = true;
tableWrap.hidden = false;
}
function makeRow(d) {
const tr = document.createElement("tr");
const sourceTd = document.createElement("td");
sourceTd.textContent = d.source; // document-derived text — never innerHTML
tr.appendChild(sourceTd);
// Path cell: a link to the document viewer (phase 10), full path as the
// accessible/hover name (the column is ellipsized).
const pathTd = document.createElement("td");
pathTd.title = d.path; // full path on hover (column is ellipsized)
const link = document.createElement("a");
link.className = "doc-link";
link.href = documentUrl(d.source, d.path);
link.target = "_blank"; // open the full document in a new tab
link.rel = "noopener";
link.title = d.path; // full path as the link's hover/accessible name
link.textContent = d.path;
pathTd.appendChild(link);
tr.appendChild(pathTd);
for (const value of [d.title, String(d.chunks), fmtDate(d.indexed_at)]) {
const td = document.createElement("td");
td.textContent = value;
tr.appendChild(td);
}
return tr;
}
function showEmpty() {
statDocs.textContent = "0";
statChunks.textContent = "0";
statLast.textContent = "–";
emptyEl.hidden = false;
if (tableWrap) tableWrap.hidden = true;
}
(async () => {
if (!(await isAdmin())) {
// Anonymous: gate in, catalog out, and no /api/docs request at all.
if (statCards) statCards.hidden = true;
if (tableWrap) tableWrap.hidden = true;
if (emptyEl) emptyEl.hidden = true;
if (gateEl) gateEl.hidden = false;
return;
}
if (gateEl) gateEl.hidden = true;
loadDocs();
})();