/* Brain of Reese — Sources view (the git-sources manager; phase 35 * task 04; archive uploads, phase 49 task 03; phase 76 task 02: shell * view module — formerly the standalone git-sources.html). * * The view module for /git-sources.html: the admin-only manager for * the stored source list (git-sources table, phase 35 tasks 01/02) — * git repo URLs (kind "git") and uploaded archives unpacked under * BOR_UPLOAD_DIR (kind "local", phase 49; the phase-38 * local-directory form is gone — the kind=local API POST is * unchanged, the page just no longer offers it). * This module is the single owner of the view's behaviour: * * Phase 76 (task 02) — shell view module (the "Sources" view of the * ONE-document shell; /git-sources.html now serves the shell, and * assets/router.js lazy-imports THIS module on first show): * * • the top-level boot is now `export async function mount(root)` — * root is the view's
, and every * DOM lookup scopes to root (the view ids stay unique across the * shell — scoped lookups keep the module honest and testable). * The router mounts a view ONCE (mount-once, hide-forever), so * the bindings and the upload-progress state machine survive * every switch: the scan poller is a self-chaining setTimeout * started when an upload begins (never at boot), so progress * continues while the user is on another view, and nothing * refetches on re-show. The single toast node/timer stay module * scope (one per document life — toasts never stack). * • the initSharedHeader() call is DROPPED: in the shell the shared * header boots exactly once, via the chat module (app.js) at shell * boot — the view never re-boots it. The gate keeps fetchIsAdmin() * — the SAME cached /api/whoami promise header.js exports (zero * extra requests). * * • boot (the mount's tail) — fetchIsAdmin() (the cached whoami): * anonymous → the sign-in gate shows and the manager stays hidden * (the exact Sources page gate pattern, and NO /api/git-sources * call is made); admin → gate hidden, #git-sources-content * revealed, loadSources(). * • loadSources() — GET /api/git-sources → the table rows * (#git-sources-tbody), the env-fallback note's visibility * (from_env), and the empty state. Each row leads with its kind * badge (Git/Local — text + color, never color alone) plus the * location in a mono : the git URL, or the full local path * for kind "local" rows (phase 38). Values are ALWAYS rendered * with textContent — never innerHTML (URLs may embed user:pass@ * credentials; phase 32's masking discipline). Non-2xx or a * network failure renders the role="alert" load error with a * retry button — never a stuck page. * • add — #git-source-form submit → POST /api/git-sources {url}. * The §7.4 never-stale lifecycle (wireAddForm): the button * disables + relabels "Adding…" while the request is out, * re-enables on success AND failure. 201 clears the input, * reloads the list, and focuses the new row's Remove button * (a11y); a failure (409 duplicate, 422 validation) shows the * server detail inline under the form (role="alert", 422 * shape-aware like the tuning forms) and keeps the input — the * instruction survives. 409/422 details are fixed generic strings * (credential safety — the URL is never echoed). * • upload — #archive-upload-form submit (phase 49, reworked to the * phase-64 202 contract in task 05 — the phase-49 synchronous * 200 paragraph is superseded): POST * /api/git-sources/upload with a FormData file (NO manual * Content-Type — the browser sets the multipart boundary). The * §7.4 never-stale lifecycle keeps its shape — the button * disables + relabels "Uploading…" while the request is out — * but the transfer is now short: the 202 arrives the moment the * archive is safely on disk (A1). 202 → the page-local * "Successfully uploaded — " toast fires (showUploadToast, * the phase-55 share-toast pattern; A2: safe to navigate away), * the file input clears, and the button hands over to the scan — * the processing state ("Processing…", disabled, title cleared) * plus startUploadPolling(): a 2 s poll of * GET /api/git-sources/upload/status renders the live * "Processing… (n/m)" label (A4 — bare during unpack; the * full path rides the button title) and settles it: success → * the sync-style count line (fmtUploadResult, the role=status * result line) + the "Archive uploaded: …" announce + * loadSources (the new/updated row lands with the Local badge; * a re-upload refreshes the row — no duplicate; NO second toast * — A2); failure → the sanitized server error in the role=alert * banner + loadSources, the file selection KEPT for a one-click * re-upload. 409 (an upload is already in progress) raises NO * error banner — it re-attaches to the in-flight run (processing * state + poll, never stale). Other non-2xx (422 format/name, * 413 cap, 5xx) keep the phase-49 error banner + the kept file * selection. The submit finally restores the button ONLY when no * poll is active (§7.4). Boot re-attach (initUploadStatus, admin * branch): a running scan re-enters the processing state + poll * (a reload mid-scan re-attaches — no second upload), a terminal * run re-renders its result line / error banner. * • remove — a row's Remove button opens the page-local * confirmation modal (#remove-confirm-dialog, a real * role="alertdialog" — the native confirm() retired, phase 69): * it * names the source (#remove-confirm-source, textContent ONLY — * URLs may embed user:pass@ credentials, phase 32) and states * the full-removal policy — the row, the source's indexed * documents (chunks + embeddings), and, for git clones and * uploaded archives, the files on the server's disk, all removed * immediately by the server-side DELETE. Cancel is the safe * default: focus lands on Cancel at open; Escape, the Cancel * button, and the dim backdrop all close as CANCEL (no request — * focus returns to the row's Remove button). "Remove source" runs * the §7.4 in-flight lifecycle IN the modal: both buttons * disable + the confirm relabels "Removing…" while the DELETE is * out — the in-flight window covers the whole server-side * cleanup (DB prune → file removal → best-effort overview * refresh; a slow LLM refresh is expected, not a stuck button — * the same "wait for the terminal state" pattern as the * Sync/Upload processing states). Navigating away mid-removal is * not recommended: the row + index commit first, so the KB stays * consistent; a rare interrupted file step leaves an inert orphan * dir (no row → never imported again). 204 → close (focus * return), loadSources(), then announce — the removal * confirmation is the LAST announcement, so the reload's "N * sources listed." cannot overwrite it (the announcer is the * screen-reader confirmation for the destructive action); * non-2xx → the in-modal role="alert" line (the server detail, * apiDetail) + both * buttons re-enabled + the confirm relabeled "Remove source" (the * dialog stays open — the fix is one retry, not a re-search for * the row); network failure → the fixed "is the app reachable?" * line, same restore. Env-fallback rows (id null — the list * comes from BOR_GIT_SOURCES, not the table) carry no Remove: * nothing is stored to remove — they show a "from .env" tag * instead. * • announce(msg) — #git-sources-announcer (role=status, * aria-live=polite): the screen-reader confirmation for loads, * adds, and removals. * * Scope boundary (phase locked decisions): adding a git repo does * NOT clone — the sync service (server-side) does that. Removing a * source, however, performs the FULL cleanup server-side (phase 69): * the row, the source's indexed documents (chunks + embeddings), and * — for git clones and uploaded archives — the app-managed files on * disk (foreign local directories are never touched), all in one * action; the confirmation modal states exactly that, and the * page's hint box matches. The Sync button still mirrors the * remaining sources (upstream file churn is pruned on that run). * The phase-49 upload is the other in-place exception: it unpacks * and scans the single source in place (the phase-64 background task * — 202 + status endpoint), and its counts render as the result * line. * * The shared header module loads through this script's own relative * import ("./header.js") — a hoisted import evaluated before this body * runs (single-evaluation design: no direct