/* Brain of Reese — Git sources admin page (phase 35, task 04; * archive uploads, phase 49 task 03). * * The page 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 page's behaviour: * * • boot — initSharedHeader() (one cached whoami, shared with the * header toggling): 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) → POST * /api/git-sources/upload with a FormData file (NO manual * Content-Type — the browser sets the multipart boundary). The * SAME §7.4 never-stale lifecycle: the button disables + * relabels "Uploading…" while the request is out and is restored * on success AND failure. 200 clears the file input, shows the * sync-style count line ("2 added · 1 pruned" — fmtUploadResult, * sources.js's fmtSyncResult convention) in the role=status * result line, announces "Archive uploaded: …" and reloads the * list (the new/updated row lands with the Local badge; a * re-upload simply refreshes the row — no duplicate). Non-2xx * inlines the server detail (422 format/name/traversal, 413 * size, 409 busy — the messages are already user-safe) and KEEPS * the file selection — the fix is one re-pick, not a re-type. * • remove — a row's Remove button asks window.confirm first * (removal prunes the documents only on the NEXT sync — the * confirm says so). Cancel → nothing; ok → the row button * disables, DELETE /api/git-sources/{id}, loadSources(). A * failure shows a per-row role="alert" error and re-enables the * button. 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 or * removing a source does NOT clone, import, or prune — the sync * service (server-side) performs that; the page's hint box says so. * The phase-49 upload is the exception: it unpacks and scans the * single source in place, and its response 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