chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks
Converts the 9 TODO items into an executable phase roadmap (Protocol B, appended after phase 39): - 40 tuning toggle anonymous flash (TODO L3) - 41 sync fail-fast + modal when a model is down (TODO L4) - 42 no reply autoscroll (TODO L5) - 43 thinking scroll back — user scroll + gated autoscroll (TODO L7) - 44 markdown tables (TODO L6) - 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8) - 46 mobile hamburger nav (TODO L9) - 47 quadlet + jinja import formats, A9 revision (TODO L10–L11) Each phase carries a user story, a dedicated Playwright E2E suite plan, and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget revision, A1–A5 scope decisions) confirmed 2026-08-27. Also records the completed phases 30–39 todo/ -> complete/ moves that were pending in the working tree. TODO.md is cleared (items now live in .agent/phases/todo/).
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Story: Sync fails fast + modal when a model is down
|
||||
|
||||
**Phase:** `41_sync_fail_fast_models` · **Source:** `TODO.md` L4 ·
|
||||
**E2E:** `tests/e2e/test_sync_model_down.py`
|
||||
|
||||
## Bug report (verbatim, `TODO.md` L4)
|
||||
|
||||
> "If the embedding or lite model is not accessible the sync button
|
||||
> should fail fast and there should be a modal error popup explaining
|
||||
> that the model isn't available."
|
||||
|
||||
## Narrative
|
||||
|
||||
As **the admin**, when I press "Sync sources" with the aipi models
|
||||
(`embed` or `lite`) unreachable, I don't want to wait through git clones
|
||||
and a partial import to discover the KB can't be updated — and a tooltip
|
||||
on a button is not a readable error. The sync should **fail fast**
|
||||
(before any expensive work) with a clear "the model isn't available"
|
||||
message, shown in a **modal dialog** I can read and dismiss.
|
||||
|
||||
- **Given** the `embed` or `lite` model endpoint is unreachable
|
||||
- **When** I press "Sync sources"
|
||||
- **Then** the run fails within a couple of seconds (before any git
|
||||
clone), the button settles retry-ready, and a modal dialog explains
|
||||
which model isn't available.
|
||||
|
||||
## Acceptance criteria
|
||||
1. **Server fail-fast:** `POST /api/sync` with a dead LLM endpoint
|
||||
reaches `state: "failed"` with a message naming the unavailable model
|
||||
(embedding first, then summary/lite) **without** cloning any source —
|
||||
the probe (one small embedding + one tiny completion against
|
||||
`BOR_LLM_SUMMARY_MODEL`) runs before source resolution and before any
|
||||
`clone_or_pull`.
|
||||
2. **Modal:** on a failed sync the page shows a modal error dialog
|
||||
(`role="alertdialog"`, `aria-modal="true"`) with a title, the
|
||||
sanitized error text (rendered via `textContent` — XSS-safe), and a
|
||||
close control; it closes on the close button, `Esc`, or backdrop
|
||||
click; focus moves into the dialog on open and returns to `#sync-btn`
|
||||
on close.
|
||||
3. **Every page:** the modal is built by the shared header module
|
||||
(`frontend/assets/header.js`), which owns the sync state machine — so
|
||||
it appears wherever `#sync-btn` exists (all six pages from phase 34).
|
||||
4. **Existing surfaces kept:** the button's failed-state `title` /
|
||||
`aria-label` / `.is-error` affordance and the Sources page's
|
||||
`#sync-error-banner` (via `bor:sync-status`) are unchanged — the modal
|
||||
is the primary, readable surface.
|
||||
5. **Success path unchanged:** a healthy model still runs clone → import
|
||||
→ overview exactly as phase 32/35/38 define it (regression).
|
||||
|
||||
## Owner-confirmed (2026-08-27, roadmap A4)
|
||||
1. **The probe runs before git clones** — the fastest possible failure;
|
||||
it costs one small embedding request and one ~1-token completion.
|
||||
2. **The modal is the primary failure surface on every page;** the
|
||||
button-title affordance and the Sources banner stay as secondary
|
||||
surfaces.
|
||||
|
||||
## UI Visualization & Structure
|
||||
- **Server** (`app/rag/llm.py`, `app/api/sync.py`): a new
|
||||
`ModelUnavailableError` (subclass of `LLMError`) + an `async
|
||||
check_models(llm)` probe: `embed_one("sync model check")` then a tiny
|
||||
`chat([...])` against the summary model; each failure mode maps to a
|
||||
message naming the model and that it isn't available (the sync
|
||||
sanitizer's credential masking still applies downstream). `_run_sync`
|
||||
calls it first, after `LLMClient()` construction — before
|
||||
`effective_sources`, before any clone.
|
||||
- **UI** (`frontend/assets/header.js`, `frontend/assets/styles.css`):
|
||||
`applySyncFailure(status)` additionally opens `showSyncModal(status)`:
|
||||
a lazily-created backdrop + `role="alertdialog"` panel appended to
|
||||
`<body>` (so no page markup changes), error text via `textContent`,
|
||||
close button + `Esc` + backdrop-click dismissal, focus management as
|
||||
in AC 2. Styled with the existing dark-theme error palette
|
||||
(PLAN §7.2: `#fca5a5` on `#2d1318` class, error border), `:focus-visible`
|
||||
per the global rule, no motion under `prefers-reduced-motion`.
|
||||
- **Non-goals:** no new endpoint, no retry-from-modal button (the button
|
||||
itself is retry-ready), no change to the 2 s poll lifecycle.
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_sync_model_down.py`** (mock LLM; DB
|
||||
up). The suite boots its **own module-scoped app** on a distinct port
|
||||
(conftest pattern used by `test_sync_button.py`) with
|
||||
`BOR_LLM_BASE_URL=http://127.0.0.1:9/v1` (dead port — connection
|
||||
refused) and a local `file://` fixture repo as the configured source, so
|
||||
a (regressed, non-fail-fast) run would spend time cloning before failing:
|
||||
1. `test_model_down_fails_fast_with_modal` — admin login, click
|
||||
`#sync-btn`; within a short wall-clock budget (≤ ~10 s, vs the 60 s
|
||||
generous budget of the healthy-run suite) the button settles
|
||||
retry-ready **and** the modal is visible with an error naming the
|
||||
model; assert the dialog role/aria contract.
|
||||
2. `test_modal_dismissal` — close via button, `Esc`, and backdrop click
|
||||
(one fresh failure per path); focus returns to `#sync-btn` each time.
|
||||
3. `test_sync_error_surfaces_unaffected` (phase 32 regression) — after
|
||||
the failure the button keeps its `title` / `.is-error` affordance; on
|
||||
`/sources.html` the `#sync-error-banner` still renders off
|
||||
`bor:sync-status`.
|
||||
4. `test_healthy_sync_still_succeeds` (phase 32/35 regression) — the
|
||||
session mock-backed app (or a second healthy module app) still runs
|
||||
the full clone → import → overview pipeline to "Synced HH:MM".
|
||||
Reference in New Issue
Block a user