phase: 149_sync_progress_box
All verification complete. The `ink-soft`/`--surface` pair the box uses is already pinned AA by the existing theme E2E suites (8.2:1 dark, ~6.9:1 light). Everything is green — no fixes were needed.
**Phase 149 final verification report**
- Verified (no re-work needed): `#sync-progress` box wiring (single `setSyncLabel` writer with `running` flag; `is-running` class travels with the spin; aria-label owned by the writer), the panel/icon-only-button CSS, and the 5-test dedicated E2E; all 6 pre-existing sync E2Es' running pins migrated to the box, settled pins untouched.
- Dedicated E2E: `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` → **5 passed in 198.6 s** (isolation, slow-proxy mock; under the 600 s budget).
- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **3463 passed, app/ coverage 99%** (>90% gate).
- Migrated sync E2Es, each in isolation: `test_sync_button` 3 passed, `test_sync_cancel` 3, `test_sync_chunk_progress` 2, `test_sync_label_mobile` 4, `test_sync_summary_visibility` 3, `test_sync_upload_progress` 4 — all green.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- Criteria: in-flight spinner-only button + full untruncated wrapped box (desktop & mobile 390×844, `scrollWidth ≤ clientWidth`) ✓; settle byte-identical ("Synced HH:MM" + `3 added`, box hidden, aria restored) ✓; `#sync-result` sole live region (box `aria-hidden`), mid-run reload re-attaches ✓; WCAG (AA contrast both themes via the pinned ink-soft/surface pair, focus-visible, reduced-motion kill intact at styles.css L2201) ✓; cancel/409/error/upload/status API unchanged (green suites above) ✓.
- Documentation: README.md reviewed in full (sync-states paragraph now matches the shipped UI; env-var table matches `app/config.py` and covers all `.env.example` vars; pages/commands verified) and AGENTS.md reviewed in full — **verified consistent, no changes needed**.
- Notable: no defects found in this or prior work; no live infrastructure touched.
- Next pending phase: none — `todo/` will be empty once the harness moves 149 (only 149 was pending).
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Phase 149 — sync detail lives in a text box under the button; the button is just the spinner
|
||||
|
||||
**Source:** `TODO.md` L6 — "The "Sync Sources" button now has too much detail and you can't see all of it - it gets cut off. I get that this was the solution to show information on mobile, but can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** the Sources page (`frontend/index.html`, the `view-rag` section): `#sync-btn` (L465 — the pill: `.sync-icon` + `.sync-label` with the three PARTS — `.sync-label-main` (the shrinks-to-fit prefix + file/folder, the part CSS ellipsizes), `.sync-label-count` (the pinned "(n/m)"), `.sync-label-chunk` (the phase-129 pinned "· chunk n/m")) + `#sync-cancel-btn` (L477, the phase-134 Cancel control) in the `.page-head-row`; `#sync-result` (L500 — the `role="status"` aria-live announcer: the LIVE untruncated label while a sync runs, the settle counts after); `#sync-error-banner` (L502). The single label writer: `frontend/assets/sources.js` `setSyncLabel(mainText, countText, chunkText)` (~L759 — writes the parts, then the UNTRUNCATED full label rides the button `title` + `#sync-result` — the A4 announcer contract; EVERY label state goes through it: the running builders `fmtSyncLabel`/`fmtSyncPhaseLabel` (L~810–850: "Syncing… <file> (n/m) · chunk n/m", "Writing KB overview…", "Summarizing folders… <folder> (n/m)"), the idle/settle labels, the 202/409 click, the re-attach). The running state: `sources.js` ~L909 adds `.is-spinning` to the icon (the CSS spin at 1s — `styles.css` L2200, with the `prefers-reduced-motion: none` kill at L2202) and ~L928 removes it on settle; the label's ellipsis lives in `styles.css` (L2216–2239 — `.sync-label-main` min-width 0 + ellipsis; the mobile media query at L5516+ makes the button full-width and the label fill the pill). The owner's complaint: the FULL live label (a long `source/relative/path` + counts + chunk) is clipped in the pill even on desktop — the mobile solution (stuffing the parts in the button) is now the permanent surface.
|
||||
**Locked (owner delegation 2026-09-30, chat — "pick the best choices"):** while a sync/upload run is in flight, the button shows ONLY the spinning icon (the label parts hidden — the button IS the loading spinner, exactly as the owner asked; its `aria-label` carries the full live label for AT), and a NEW bordered text box directly under the button row (`#sync-progress`) shows the SAME full untruncated live label — wrapping, mono, breathing room — hidden at idle/settle. `#sync-result` REMAINS the sole aria-live announcer (the box is presentational, `aria-hidden` — one live region, no double announce); the settle counts line still lands on `#sync-result` exactly as today, and the idle "Sync sources" label returns to the button when the run ends. The box appears for BOTH run kinds (sync + the phase-90 upload "Importing…" run — the shared writer covers both) and re-attaches (a page load mid-run reveals it, the never-stale contract).
|
||||
|
||||
## Objective
|
||||
The live sync detail (file, position, chunk, phase labels) is fully visible in a calm text box under the button at every viewport, the button reduces to a clean spinner while running, and the accessibility contract (one live region, the button's accessible name, focus-visible, contrast) is preserved.
|
||||
|
||||
## Dependencies
|
||||
- `148_pdf_pages_in_sources` (todo) — pipeline order only; no shared code (this phase touches the Sources-page sync UI in `index.html`/`sources.js`/`styles.css`; 148 touches the doc viewer + the importer/docs API).
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **The box (task 01):** `#sync-progress` — a `<div class="sync-progress" hidden aria-hidden="true">` with an inner `<span class="sync-progress-text" id="sync-progress-text"></span>`, placed in `index.html` BETWEEN the `.page-head` block and `#sync-result` (directly under the button row — the owner's "under the button"). `sources.js`: element constants beside the existing ones (~L707–714); `setSyncLabel` (the SINGLE writer — the invariant: every label state, running or not, flows through here) writes the full label into the box's text AND reveals the box for the RUNNING label states (the running builders' outputs + the re-attach running entry) and HIDES it for every non-running state (idle "Sync sources", the settle "Synced HH:MM" + counts, the 202/409 clicks, the error state) — the reveal/hide decision lives at the `setSyncLabel` callsites via a small `running` flag parameter (or a dedicated `setSyncProgress(full, running)` called from the same sites — the executor picks the minimal clean shape; the CONTRACT is: the box is visible iff a run is in flight, and its text is exactly the full label `setSyncLabel` computes). The button: `#sync-btn` gains `is-running` class while in flight (added where `.is-spinning` is added, ~L909; removed where it is removed, ~L928 — including the error settle and the 409 path); its `aria-label` is set to the full live label while running (the icon-only button needs an accessible name — the `title` already carries it for hover, the aria-label for AT) and restored to `"Sync sources"` on every settle. The Cancel button is untouched (it already rides the running state).
|
||||
- **The styling (task 02):** `styles.css` — `.sync-progress`: the bordered panel (1px border in the theme border var, the theme surface/background var, the house radius, mono text in `var(--mono)` — the `.sync-label-count`/`.sync-result` mono language: a position reads as data, `overflow-wrap: anywhere` for the long `source/relative/path` (it must WRAP, never clip — the whole point), a comfortable min-height (~2 lines' breathing room, no jump when empty→full because it is hidden when idle), margin below the button row, inside the existing 72rem centered container (the page already rides it — no new width logic); `.sync-btn.is-running .sync-label { display: none; }` (the pill shrinks to an icon-only control — the width adjusts naturally; the `.sync-btn:disabled`/cursor behavior while running is preserved as today); the mobile media query (L5516+): the button stays full-width, the box full-width, the label-hidden rule identical (no mobile special-case — the box REPLACES the old mobile-stuffing reason, which is exactly what the owner asked); `prefers-reduced-motion` untouched (the box is static text; the spinner kill already exists); contrast AA in both themes (the ink vars — the muted `.sync-result` language is ≥4.5:1 already; the box reuses it); focus-visible on the button + Cancel unchanged.
|
||||
- **The E2E (task 03):** the `slow_llm` proxy idiom (the `test_sync_chunk_progress.py` recipe — a module app with `BOR_LLM_BASE_URL` = the `tests/e2e/slow_llm.py` delay proxy in front of the mock, ~1.5s per LLM call, the fixture repo with a few docs so the import phase has many polls of observable running state) — observe the in-flight state deterministically.
|
||||
|
||||
## Tasks
|
||||
1. `01_progress_box_wiring.md` — the box markup + the `setSyncLabel` wiring (reveal/hide contract) + the button's `is-running` + aria-label.
|
||||
2. `02_progress_box_styles.md` — the panel + the icon-only running button + mobile + the AA/motion gates.
|
||||
3. `03_e2e_progress_box.md` — the dedicated E2E: in-flight (spinner-only button + full untruncated box), settled (box hidden, counts line, label restored).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: one dedicated Playwright file, run in isolation, mock-only (slow proxy).
|
||||
- Frontend pins: the pre-existing sync E2Es (label parts, chunk progress, mobile label, cancel, summary visibility, upload progress) pass UNCHANGED in their settled/idle assertions; their RUNNING-state assertions that pinned the button label text are updated to the box (the executor finds them via the `sync-label` selectors and migrates the expectation to `#sync-progress` — the A4 announcer (`#sync-result`) is asserted unchanged).
|
||||
- Coverage: n/a (frontend phase — `app/` coverage stays >90%).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] while a sync (or upload) run is in flight: the button shows only the spinning icon (no visible label text, at any viewport), its `aria-label` carries the full live label, and `#sync-progress` is visible with the EXACT full untruncated label (a long `source/relative/path` wraps — never clipped)
|
||||
- [ ] at settle/idle: the box is hidden, the button shows "Sync sources" (the settle "Synced HH:MM" + counts line on `#sync-result` is byte-identical to pre-phase), and the label parts are restored
|
||||
- [ ] `#sync-result` remains the sole aria-live region (the box is `aria-hidden`); a page load mid-run re-attaches with the box visible (the never-stale contract)
|
||||
- [ ] WCAG: focus-visible on the button + Cancel, contrast ≥4.5:1 both themes, `prefers-reduced-motion` respected (the spinner kill intact)
|
||||
- [ ] `uv run pytest` green (all pre-existing E2Es — running-state label pins migrated to the box, settled-state pins untouched); `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean
|
||||
- [ ] no behavior change in completed phases (the cancel control, the 409 double-trigger, the error banner/modal, the upload run's status, the sync status API)
|
||||
Reference in New Issue
Block a user