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/).
98 lines
5.2 KiB
Markdown
98 lines
5.2 KiB
Markdown
# 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".
|