chore(agent): phase roadmap from TODO.md, 3 phases (34-36)

This commit is contained in:
2026-08-26 09:41:32 -04:00
parent 8fabb7efda
commit 0a46f07fa8
20 changed files with 799 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# Story: Admin Page to Add / Remove Git Sources
**Phase:** `35_git_sources_admin` · **E2E:** `tests/e2e/test_git_sources_admin.py`
## Narrative
As **the admin (owner)**, today I manage the git repos that feed the
knowledge base by editing `BOR_GIT_SOURCES` in `.env` and restarting. I
want a page only I can access where I can **add and remove git
sources** — no env file, no restart — and have the Sync button
(phase 32) and `import_docs` (phase 28) use that stored list.
- **Given** I am signed in as admin
- **When** I open the Git sources page and add a repo URL (or remove
one)
- **Then** the change is stored in Postgres (`git_sources` table) and
the next sync / import uses the stored list — the
`BOR_GIT_SOURCES` env var remains only a fallback while the table is
empty, and phase 32's fail-loud "no git sources configured" is
preserved when both are empty.
## Acceptance criteria
1. Migration 0006: `git_sources` (id UUID PK, url TEXT UNIQUE NOT
NULL, added_at TIMESTAMPTZ) — reversible, integration-tested
up/down (existing migration-test pattern).
2. Admin-only API (phase 16 pattern — 403 `admin only` anonymous):
`GET /api/git-sources` (DB rows in added order; while the table is
empty, the env list with `from_env: true`), `POST /api/git-sources`
{url} (trimmed, 1–500 chars, `https?://` / `ssh://` / `git@`
shape, 409 on duplicate with a detail that never echoes the URL),
`DELETE /api/git-sources/{id}` (204 / 404).
3. `effective_git_sources()` shared by `app/api/sync.py::_run_sync`
and `scripts/import_docs.py`: DB rows win, env fallback only while
the table is empty, fail-loud unchanged when both are empty; the
sync log line records the list origin (`origin=db|env`).
4. `/git-sources.html` — soft-gated like the Sources page (anonymous:
sign-in gate) with the standard header (phase 34) and an
admin-only "Git sources" nav link on all five pages (phase 29
pattern): sources list (mono URL + added date + per-row Remove),
an add form (labeled input + Add, inline error, never-stale button
per §7.4), an env-fallback note while the table is empty, and a
hint that the KB itself updates via the Sync button.
5. `.env.example` + README: `BOR_GIT_SOURCES` documented as the
empty-table fallback; the admin page is the primary management UI.
6. Unit + integration green, `app/` coverage >90%, story E2E green in
isolation, ruff + pyright clean, one `--no-gpg-sign` commit.
## Playwright Mapping Rule
`tests/e2e/test_git_sources_admin.py` — one story, one file, run in
isolation: anonymous gets the sign-in gate (and 403s on the API); the
admin adds/removes sources on the page (the list updates live, invalid
input gets an inline error) and reaches the page via the "Git
sources" nav link on every page.
+57
View File
@@ -0,0 +1,57 @@
# Story: One Navbar on Every Page
**Phase:** `34_consistent_navbar` · **E2E:** `tests/e2e/test_nav_consistency.py`
## Narrative
As **any user (admin or anonymous)**, I move between the Chat, Sources,
Document viewer, Global Tuning, and Sign in pages. Today the header bar
is only similar between pages: the document viewer replaces it with a
back + title bar (every nav button vanishes), the login page misses the
Tuning link, and the functional controls are page-scoped — the Tuning
steering toggle lives on chat only, the Sync sources button on Sources
only — so buttons pop in and out as I switch tabs. I want ONE identical
bar on every page.
- **Given** any of the app's five pages
- **When** I look at the header
- **Then** I see the same controls in the same order — brand, nav
[Chat, Sources, Tuning], Tuning toggle, Sync sources (admin), New
chat, and exactly one of Sign in / Sign out — with none missing
because of which page I happen to be on.
## Acceptance criteria
1. `frontend/assets/header.js` is the single owner of the functional
header controls: the steering toggle + panel logic (moved from
`app.js`, exported `refreshSteering()`), the Sync button state
machine (moved from `sources.js`, dispatching `bor:sync-status`),
one New chat binding (chat page: `bor:new-chat` event; other pages:
clear + navigate), and the sign-in `?next=` rewrite to the current
page. No control logic remains in the page scripts.
2. All five pages (`index.html`, `sources.html`, `document.html`,
`tuning.html`, `login.html`) ship the identical header markup; the
`#steering-panel` section exists on every page (chat: after
`#kb-banner` as today; other pages: first child of `<main>`).
3. The document viewer keeps its back link + title in a second
titlebar row below the standard bar (phase 13 back-target behavior
unchanged); the pinned 64px / 58px height contract applies to the
standard row on every page.
4. The locked A10 UI revision is preserved: admin-only controls
(Sources / Tuning nav links, Sync button) ship hidden and are
revealed only for a signed-in admin — for the admin every button is
active on every page; anonymous visitors get the reduced bar,
identically on every page (owner confirmation 2026-08-26).
5. The chat page's per-bubble Tune button + inline form (phases 15/27)
and the Sources page's sync result line + error banner (phase 32)
keep working exactly as before.
6. `tests/e2e/test_nav_consistency.py` green in isolation;
`test_header_consistency.py` / `test_shared_header.py` updated to the
new viewer contract; regressions green; `app/` coverage >90%
(frontend-only phase — unchanged); ruff + pyright clean; one
`--no-gpg-sign` commit.
## Playwright Mapping Rule
`tests/e2e/test_nav_consistency.py` — one story, one file, run in
isolation. It asserts, per role (admin, anonymous), that the visible
header control inventory is identical on all five pages, and that the
viewer's back link + titlebar row survive the change.
+45
View File
@@ -0,0 +1,45 @@
# Story: Summary + Original Document Together in the Viewer
**Phase:** `36_summary_in_viewer` · **E2E:** `tests/e2e/test_summary_in_viewer.py`
## Narrative
As **any user**, phase 30 gives every non-markdown document a
lite-model summary (stored on `documents.summary`, indexed as an
`is_summary` chunk, used for retrieval). When I open such a document —
from the Sources table, a chat source chip, or a direct URL — I only
see the raw original. I want to see **the summary and the original
document together**.
- **Given** a document that has a summary (phase 30: non-markdown A9
documents)
- **When** I open it in the document viewer (the modal or the full
page)
- **Then** a labeled Summary panel sits above the original content —
both visible at once. Documents without a summary (markdown docs,
pre-phase-30 rows, fail-soft rows) render exactly as before.
## Acceptance criteria
1. `DocContent` gains `summary: str | None`; `GET
/api/documents/content` returns `documents.summary`. The endpoint
stays public + stateless — the phase 16 soft rule (catalog gated,
viewer public) is unchanged.
2. The shared renderer `renderDocument` (`assets/document.js`)
renders the summary panel for **both** surfaces at once (the
full-page viewer + the chat/sources modal); null/empty summary →
no panel; the summary text is written with `textContent` (XSS
contract unchanged).
3. `.doc-summary` styling matches the dark tech theme (phase 08
palette, contrast ≥4.5:1) and reads as a summary, not as document
content.
4. Integration tests: a summarized non-markdown doc returns its
summary; a markdown doc returns null; anonymous access unchanged.
5. Story E2E green in isolation (the `summary_kb` fixture: the
deterministic digest panel + the full original with its tail
sentinel visible together; the markdown control doc → no panel);
regressions green; `app/` coverage >90%; ruff + pyright clean; one
`--no-gpg-sign` commit.
## Playwright Mapping Rule
`tests/e2e/test_summary_in_viewer.py` — one story, one file, run in
isolation.