/* 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 upload 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, unpack-only in phase 90 — * 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 * ("Upload") disables + relabels "Uploading…" while the request * is out — but the transfer is 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 * background run — the processing state (bare "Processing…", * disabled, title cleared) plus startUploadPolling(): a 2 s poll * of GET /api/git-sources/upload/status that renders the bare * "Processing…" label for the WHOLE run (phase 90, A2 — the run * is unpack + register only: no file, no "(n/m)" counts, no * title) and settles it: success → the ready-for-sync line * ("Uploaded — press Sync sources to import it.", * fmtUploadResult off the status's {"message": "uploaded"} * detail — the role=status result line) + the "Archive uploaded * — press Sync sources to import it." 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 run re-enters the processing state + poll * (a reload mid-run re-attaches — no second upload), a terminal * run re-renders its result line / error banner (the safe name * was page-local — lastUploadName is null after a reload — so the * re-rendered line is the nameless "Uploaded — press Sync sources * to import it."). * • 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. * * Phase 77 (task 02) — the re-show refresh: the shell router * dispatches `bor:view-refresh` on the view's section when the user * RE-SHOWS an already-mounted view (a switch back onto it, a re-click * of the Sources nav link, or back/forward) — the first show (mount) * and boot never (the mount's own load is the first fetch). This * module listens on root and re-runs `loadSources()`, whose re-call * resets all three list states: the populated render (renderSources * replaces the tbody + re-syncs the empty state) and the load error * (hideLoadError() runs on the success path, so an error followed by * a successful refresh clears it). The listener is armed only in the * ADMIN branch, after the whoami gate passes: anonymous shows the * gate and never fetches /api/git-sources (the Sources-page gate * pattern). * * Phase 89 (task 05) — the per-source ignore-paths editor: every * STORED row (s.id truthy — env-fallback rows carry NO button, A3: * nothing is stored to edit) gets an "Ignore paths" button in the * actions cell (left of Remove) that opens the page-local alertdialog * (#ignore-editor-dialog — the EXACT #remove-confirm-dialog pattern, * phase 69): #ignore-editor-source names the source (textContent ONLY * — the same `value` expression makeRow uses — URLs may embed * user:pass@ credentials, phase 32), the mono textarea prefills the * row's stored list (one path per line — the phase-89 A1 prefix rule, * stated in plain words in the helper copy), and focus lands on * Cancel (the safe default); Escape / Cancel / the dim backdrop close * as CANCEL (no request — focus returns to the row's trigger button). * "Save" runs the §7.4 in-flight lifecycle: both buttons disable + * the save relabels "Saving…" while the PATCH * /api/git-sources/{id} is out (blank lines in the box are separators, * not entries — split + trim + drop empty client-side; the server * still rejects empty entries defensively, A4). 200 → close (focus * return), loadSources (the row's "N ignored" count tag lands — the * A5 replace semantics round-trip through GET), announce — the update * confirmation is the LAST announcement (the reload's "N sources * listed." must not overwrite it); non-2xx → the in-dialog * role="alert" line (the server detail, apiDetail 422-shape-aware) + * the buttons restored, the dialog STAYS open and the textarea * content is KEPT (the instruction survives — tuning-form * convention); network failure → the fixed reachable? line, same * restore. Rows whose list is non-empty show the "N ignored" count * tag next to the location (text + a distinct background — * never color alone, WCAG 1.4.1). * * 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 registers the source in place (the phase-64 background task — * 202 + status endpoint) and STOPS THERE — no model check, no * import, no overview refresh (phase 90, A1): the scan is the RAG * page's "Sync sources" button's job (it imports the uploaded * kind=local row with prune + the row's ignore list), and the result * line points at that button. * * 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