From 476aa0e066963a0ee88517613a63ba489939f3c0 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Tue, 25 Aug 2026 10:15:42 -0400 Subject: [PATCH] =?UTF-8?q?chore(agent):=20phase=20roadmap=20from=20TODO.m?= =?UTF-8?q?d=20=E2=80=94=203=20phases=20(document=20modal,=20global=20tuni?= =?UTF-8?q?ng,=20git-based=20sources)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/user_stories/document-modal.md | 54 +++++++++++++++++++++++++++ .agent/user_stories/git-sources.md | 40 ++++++++++++++++++++ .agent/user_stories/global-tuning.md | 51 +++++++++++++++++++++++++ TODO.md | 5 ++- 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 .agent/user_stories/document-modal.md create mode 100644 .agent/user_stories/git-sources.md create mode 100644 .agent/user_stories/global-tuning.md diff --git a/.agent/user_stories/document-modal.md b/.agent/user_stories/document-modal.md new file mode 100644 index 0000000..a76a9a0 --- /dev/null +++ b/.agent/user_stories/document-modal.md @@ -0,0 +1,54 @@ +# Story: Open Documents in a Modal (Almost-Fullscreen) + +**Phase:** `26_document_modal_viewer.md` · **E2E:** `tests/e2e/test_document_viewer.py` + +## Narrative + +As **a user**, when I click a document source chip (in the chat) or a +document path link (in the Sources table), I want the document to open +**right where I am** — in an almost-fullscreen modal overlay — instead of +navigating away to a new page/tab. I want to read it, then close the modal +and keep going. + +- **Given** a document cited in a chat answer (or listed in Sources) +- **When** I click its chip / path link +- **Then** the document opens in an almost-fullscreen modal on the same + page, rendered exactly as the `/document.html` viewer renders it + (markdown in a centered column, other formats in a mono `pre`), with a + close button, Escape-to-close, and backdrop-to-close; and the dedicated + `/document.html` page still works for direct links / no-JS fallback. + +## Acceptance criteria +1. Clicking a source chip or a Sources-table path link opens the document + in a modal (`#doc-modal`, `.doc-modal-panel`, ~`96vw × 92vh`) on the + current page — **no new tab, no navigation**. +2. The modal renders the same content as `/document.html`: md/markdown via + the shared escape-first renderer (`.doc-md`), other formats in + `
`, source/format/path/indexed/chunks meta.
+3. The modal closes on the close button, on `Escape`, and on backdrop
+   click; focus moves into the panel on open and returns on close.
+4. The modal honours the dark theme, `prefers-reduced-motion`, no-CDN, and
+   the ≥4.5:1 contrast / focus-visible a11y rules.
+5. The standalone `/document.html` page is unchanged (direct link, back
+   button, not-found state, XSS-safe rendering).
+6. No new packages; vanilla HTML/CSS/JS; the modal reuses the existing
+   stateless `GET /api/documents/content` endpoint (no backend change).
+7. Story E2E green in isolation, no regressions in the document-navigation
+   / header / smoke suites, one `--no-gpg-sign` commit.
+
+## Playwright Mapping Rule
+**Test Scenario → `tests/e2e/test_document_viewer.py`** (mock LLM, seeded
+KB — same harness as the phase-10 suite):
+1. `test_source_chip_opens_modal` — chip → modal opens in-page (no popup),
+   title + `.doc-md` content present, page URL unchanged.
+2. `test_sources_row_opens_modal` — Sources path link → modal, yaml in
+   ``, mono font.
+3. `test_modal_closes_on_button_escape_and_backdrop` — close via button,
+   backdrop, and `Escape`.
+4. `test_modal_focus_and_a11y` — `role="dialog"` + `aria-modal`, focus
+   inside the panel, close button has an `aria-label`.
+5. `test_modal_xss_safe` — hostile md renders escaped, no dialog fires.
+6. `test_standalone_page_still_works` — `/document.html` still renders,
+   not-found, dark theme, no-CDN, a11y frame, ≤736px md column.
+7. `test_modal_theme_and_no_cdn` — dark surface; all assets same-origin /
+   `data:`.
diff --git a/.agent/user_stories/git-sources.md b/.agent/user_stories/git-sources.md
new file mode 100644
index 0000000..cff88cf
--- /dev/null
+++ b/.agent/user_stories/git-sources.md
@@ -0,0 +1,40 @@
+# Story: Git-Based Sources (No Hardcoded Directories)
+
+**Phase:** `28_git_based_sources.md` · **E2E:** none (no UI/API change; unit + integration cover the git flow)
+
+## Narrative
+
+As **the admin (owner)**, I don't want the import sources hard-coded to
+`~/Homelab` and `~/Deployments`. I want to **specify a list of git
+repository URLs** and have `import_docs` **clone or pull** each one into a
+dedicated local location and then **index all the code** in them.
+
+- **Given** a list of git URLs and a destination directory
+- **When** I run `import_docs`
+- **Then** each repo is cloned (first run) or pulled to `ff` (subsequent
+  runs) into the dedicated directory, and the resulting files are indexed
+  exactly like any other A9-format directory.
+
+## Acceptance criteria
+1. `BOR_GIT_SOURCES` (comma-separated git URLs) + `BOR_SOURCES_DIR`
+   (default `~/bor-sources`) settings; `git_source_list` parses the CSV.
+2. `scripts/git_sync.py::clone_or_pull(url, dest)` — shallow clone when the
+   dest has no `.git`, `--ff-only` pull otherwise; raises `GitSyncError`
+   (with stderr) on failure and on missing `git`.
+3. `scripts.import_docs` resolves `BOR_GIT_SOURCES` → cloned/pulled local
+   dirs and passes them to `import_sources`; `--source ` overrides
+   (manual mode wins); a failed sync aborts the run (non-zero exit, no
+   partial import).
+4. The A9 format filter / hidden-dir skip / exclusion list still apply to
+   the cloned content (the importer's walk is unchanged).
+5. `documents.source` = the repo directory name (e.g. `homelab`).
+6. `.env.example` + README document both source modes.
+7. Unit (git_sync dispatch/errors) + integration (import_docs resolution
+   with mocked git + import) green; `app/` coverage >90%; one
+   `--no-gpg-sign` commit.
+
+## Playwright Mapping Rule
+No story E2E suite (no `app/` UI/API change). The git flow is covered by
+unit tests (`tests/unit/test_git_sync.py`) and an integration test
+(`tests/integration/test_import_docs_git.py`) with `clone_or_pull` and
+`import_sources` mocked — no network, no `git`, no real embed endpoint.
diff --git a/.agent/user_stories/global-tuning.md b/.agent/user_stories/global-tuning.md
new file mode 100644
index 0000000..64ab167
--- /dev/null
+++ b/.agent/user_stories/global-tuning.md
@@ -0,0 +1,51 @@
+# Story: Global Tuning Manager (No Chat Required, Editable)
+
+**Phase:** `27_global_tuning.md` · **E2E:** `tests/e2e/test_global_tuning.py`
+
+## Narrative
+
+As **the admin (owner)**, I want to **manage my global tuning notes**
+without having to have a chat conversation first, and I want to be able to
+**edit** a note I've already saved — not just create and delete.
+
+- **Given** I'm the admin and I have no chat open yet
+- **When** I open the "Tuning" page (from the header)
+- **Then** I can add a new note, edit any existing note inline, list all
+  notes, and delete them — and every note is stored in Postgres and read
+  into the system prompt of every future turn.
+
+## Acceptance criteria
+1. New endpoint `PUT /api/steering/{id}` (200 updated note; 404 unknown;
+   422 empty/whitespace/over-2000; 403 anonymous) — the router keeps its
+   existing `require_admin` gate.
+2. New page `/tuning.html` (admin-only nav link): a "Add a global tuning
+   note" form + a list of notes, each with inline **Edit** (textarea +
+   Save/Cancel) and **Delete**.
+3. A note created on `/tuning.html` is persisted and steers future answers
+   (no chat turn needed to create it; observable in a later answer).
+4. Editing a note updates its text in the list and in the ``
+   prompt.
+5. Delete removes the note from the list and the prompt.
+6. The chat-page "Tune" button and header "Tuning" panel (phase 15) are
+   unchanged — create + delete still work there.
+7. Anonymous users get 403 on the data and on `PUT`; the header "Tuning"
+   link is hidden for anonymous.
+8. UI: landmarks, labeled controls, ≥44px targets, `:focus-visible`,
+   contrast ≥4.5:1, live-region announcements, Phase-08 tokens, no-CDN.
+9. Notes render as text (XSS-safe).
+10. Unit + integration green, `app/` coverage >90%, story E2E green in
+    isolation, one `--no-gpg-sign` commit.
+
+## Playwright Mapping Rule
+**Test Scenario → `tests/e2e/test_global_tuning.py`** (mock LLM, seeded
+KB — the mock echoes the first tuning note into its answer when ``
+is present, so prompt injection is observable in the UI):
+1. `test_create_note_without_chat` — `/tuning.html`, add a note, it
+   appears in the list + announcer.
+2. `test_edit_note_inline` — Edit → change text → Save → list updated.
+3. `test_delete_note` — Delete → row removed, empty state shows.
+4. `test_edit_note_steers_answer` — create via `/tuning.html`, then a chat
+   turn shows the note's marker in the answer.
+5. `test_tuning_page_a11y_and_no_cdn` — landmarks, labels, ≥44px,
+   focus-visible, dark theme, all assets local.
+6. `test_anonymous_cannot_manage` — anonymous list empty; `PUT` → 403.
diff --git a/TODO.md b/TODO.md
index 6554401..4c47398 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,3 +1,6 @@
 # TODO
 
-Phased on 2026-08-24 — items now live in `.agent/phases/todo/24_whole_document_context/` (phase 24, owner-confirmed: no context truncation, A7 revised).
+Phased on 2026-08-25 — items now live in `.agent/phases/todo/`:
+- `26_document_modal_viewer` (documents open in an almost-fullscreen modal, TODO L4)
+- `27_global_tuning` (global tuning without a chat + editable notes, TODO L3)
+- `28_git_based_sources` (configurable git repo URLs instead of hardcoded dirs, TODO L5)