Files
brain-of-reese/.agents/phases/complete/149_sync_progress_box/00_phase.md
T
ducoterra dd524246ec
Build and Push Containers / build-and-push-app (push) Successful in 3m35s
Build and Push Containers / build-and-push-db (push) Successful in 11s
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).
2026-10-01 21:14:48 -04:00

9.2 KiB
Raw Blame History

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 (L810–850: "Syncing… (n/m) · chunk n/m", "Writing KB overview…", "Summarizing folders… (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)