From dd524246ec002304ccd5306d2cfb96514ccf1af9 Mon Sep 17 00:00:00 2001
From: ducoterra
Date: Thu, 1 Oct 2026 21:14:48 -0400
Subject: [PATCH] phase: 149_sync_progress_box
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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).
---
.../149_sync_progress_box/00_phase.md | 0
.../01_progress_box_wiring.md | 0
.../02_progress_box_styles.md | 0
.../03_e2e_progress_box.md | 0
.../149_sync_progress_box__00_phase.a1.err | 0
.../149_sync_progress_box__00_phase.a1.md | 13 +
...49_sync_progress_box__00_phase.a1.validate | 120 +++
...rogress_box__01_progress_box_wiring.a1.err | 0
...progress_box__01_progress_box_wiring.a1.md | 15 +
...ss_box__01_progress_box_wiring.a1.validate | 120 +++
...rogress_box__02_progress_box_styles.a1.err | 0
...progress_box__02_progress_box_styles.a1.md | 18 +
...ss_box__02_progress_box_styles.a1.validate | 120 +++
...c_progress_box__03_e2e_progress_box.a1.err | 0
...nc_progress_box__03_e2e_progress_box.a1.md | 16 +
...gress_box__03_e2e_progress_box.a1.validate | 120 +++
...c_progress_box__03_e2e_progress_box.a2.err | 0
...nc_progress_box__03_e2e_progress_box.a2.md | 14 +
...gress_box__03_e2e_progress_box.a2.validate | 120 +++
README.md | 23 +-
TODO.md | 3 +
frontend/assets/sources.js | 91 +-
frontend/assets/styles.css | 44 +
frontend/index.html | 11 +
tests/e2e/test_sync_button.py | 23 +-
tests/e2e/test_sync_cancel.py | 10 +-
tests/e2e/test_sync_chunk_progress.py | 4 +-
tests/e2e/test_sync_label_mobile.py | 529 ++++++----
tests/e2e/test_sync_progress_box.py | 996 ++++++++++++++++++
tests/e2e/test_sync_summary_visibility.py | 64 +-
tests/e2e/test_sync_upload_progress.py | 119 ++-
tests/unit/test_frontend_sync_upload.py | 269 ++++-
tests/unit/test_sync_button.py | 43 +-
33 files changed, 2512 insertions(+), 393 deletions(-)
rename .agents/phases/{todo => complete}/149_sync_progress_box/00_phase.md (100%)
rename .agents/phases/{todo => complete}/149_sync_progress_box/01_progress_box_wiring.md (100%)
rename .agents/phases/{todo => complete}/149_sync_progress_box/02_progress_box_styles.md (100%)
rename .agents/phases/{todo => complete}/149_sync_progress_box/03_e2e_progress_box.md (100%)
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.err
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.md
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.validate
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.err
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.md
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.validate
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.err
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.md
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.validate
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.err
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.md
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.validate
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.err
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.md
create mode 100644 .agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.validate
create mode 100644 tests/e2e/test_sync_progress_box.py
diff --git a/.agents/phases/todo/149_sync_progress_box/00_phase.md b/.agents/phases/complete/149_sync_progress_box/00_phase.md
similarity index 100%
rename from .agents/phases/todo/149_sync_progress_box/00_phase.md
rename to .agents/phases/complete/149_sync_progress_box/00_phase.md
diff --git a/.agents/phases/todo/149_sync_progress_box/01_progress_box_wiring.md b/.agents/phases/complete/149_sync_progress_box/01_progress_box_wiring.md
similarity index 100%
rename from .agents/phases/todo/149_sync_progress_box/01_progress_box_wiring.md
rename to .agents/phases/complete/149_sync_progress_box/01_progress_box_wiring.md
diff --git a/.agents/phases/todo/149_sync_progress_box/02_progress_box_styles.md b/.agents/phases/complete/149_sync_progress_box/02_progress_box_styles.md
similarity index 100%
rename from .agents/phases/todo/149_sync_progress_box/02_progress_box_styles.md
rename to .agents/phases/complete/149_sync_progress_box/02_progress_box_styles.md
diff --git a/.agents/phases/todo/149_sync_progress_box/03_e2e_progress_box.md b/.agents/phases/complete/149_sync_progress_box/03_e2e_progress_box.md
similarity index 100%
rename from .agents/phases/todo/149_sync_progress_box/03_e2e_progress_box.md
rename to .agents/phases/complete/149_sync_progress_box/03_e2e_progress_box.md
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.err b/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.md b/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.md
new file mode 100644
index 0000000..f0383f9
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.md
@@ -0,0 +1,13 @@
+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).
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.validate b/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.validate
new file mode 100644
index 0000000..71a2985
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__00_phase.a1.validate
@@ -0,0 +1,120 @@
+........................................................................ [ 2%]
+........................................................................ [ 4%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 12%]
+........................................................................ [ 14%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 29%]
+........................................................................ [ 31%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 41%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 56%]
+........................................................................ [ 58%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 68%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 85%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+....... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 270 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 196 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 183 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 265 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 68 0 100%
+app/models.py 135 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 431 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 744 4 99%
+app/rag/llm.py 246 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 71 0 100%
+app/rag/pdf.py 124 0 100%
+app/rag/prompts.py 155 0 100%
+app/rag/retriever.py 248 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/schemas.py 443 0 100%
+--------------------------------------------------
+TOTAL 5609 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.err b/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.md b/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.md
new file mode 100644
index 0000000..4e65404
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.md
@@ -0,0 +1,15 @@
+Everything is verified and green. Task 01 is complete.
+
+**Report — Phase 149, Task 01 (`01_progress_box_wiring.md`)**
+
+- **HTML:** `#sync-progress` box (`.sync-progress-text` span) inserted between the `.page-head` block and `#sync-result` — ship-hidden, `aria-hidden` (presentational; `#sync-result` stays the sole aria-live announcer).
+- **sources.js (single-writer wiring):** `setSyncLabel` gains `running=false` — writes the SAME full untruncated label into the box, reveals it IFF a run is in flight, and owns the button's `aria-label` (full live label while running, `"Sync sources"` restored on every settle). `enterSyncRunningState` passes `running=true` + adds `is-running` (with the spin, its old aria write removed); `settleSyncButton` removes `is-running` (with the spin). Covers all 5 running entries (poll ticks, 202/409 click, both re-attaches) and every settle.
+- **Unit tests:** pins moved to the new shape + new HTML markup pin + node-harness rows gain box text/hidden, `is-running`/`is-spinning`, aria-label, with a new behavioral test (box visible IFF running, exact full label, restored at settle).
+- **E2E migration (six sync suites):** running-state label pins → `#sync-progress` (regexes target `#sync-progress-text` — the div's innerText carries newline artifacts); count `to_be_visible` → box visibility; `test_sync_label_mobile` tests 1–3 reworked box-based (full string + viewport-fit + `is-running`), `_settle` gains the box-hidden pin; all settled/idle + `#sync-result` pins untouched.
+- **Docs:** README "States" bullet updated (button = spinner, detail box under the row); no AGENTS.md impact.
+
+**Results:** `uv run pytest --cov=app` → **3463 passed, 99% coverage** (gate >90%); `uv run ruff check .` clean; `uv run pyright` 0 errors; all 11 sync-related E2E files green in isolation (`--no-cov`), incl. the six migrated suites.
+
+**Notable:** (1) `test_sync_label_mobile.py` test 4 (pre-phase-128 CSS clip reproduction) stays green now, but task 02's `.sync-btn.is-running .sync-label{display:none}` (higher specificity) will make the injected rules lose — task 02 must migrate/drop it. (2) Owner appended 2 new TODO.md items mid-session (image-embed sync status) — untouched; likely a future phase.
+
+**Next pending task:** `02_progress_box_styles.md` (box panel CSS, icon-only running button, mobile, AA/motion gates).
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.validate b/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.validate
new file mode 100644
index 0000000..71a2985
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__01_progress_box_wiring.a1.validate
@@ -0,0 +1,120 @@
+........................................................................ [ 2%]
+........................................................................ [ 4%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 12%]
+........................................................................ [ 14%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 29%]
+........................................................................ [ 31%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 41%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 56%]
+........................................................................ [ 58%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 68%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 85%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+....... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 270 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 196 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 183 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 265 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 68 0 100%
+app/models.py 135 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 431 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 744 4 99%
+app/rag/llm.py 246 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 71 0 100%
+app/rag/pdf.py 124 0 100%
+app/rag/prompts.py 155 0 100%
+app/rag/retriever.py 248 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/schemas.py 443 0 100%
+--------------------------------------------------
+TOTAL 5609 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.err b/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.md b/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.md
new file mode 100644
index 0000000..e5e4443
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.md
@@ -0,0 +1,18 @@
+All green. Task complete.
+
+**Implemented (task 02 — box styling):**
+- `.sync-progress` panel in `frontend/assets/styles.css`: house bordered-panel language (`--surface` fill, 1px `--line` border, `--radius` radius, `0.75rem 1rem` padding, `min-height: 4rem` ≈ 2 lines). No `margin-top` — the `.sources-shell` flex gap (1.25rem) *is* the house spacing; a margin would double it (documented in the CSS comment)
+- `.sync-progress-text`: `display: block`, `var(--mono)`, `0.8rem` (the `.sync-result` size), house `1.55` line-height, `var(--ink-soft)` (pinned identity pair on `--surface`: 8.2:1 built-in dark, theme gate keeps all themes ≥4.5:1), `overflow-wrap: anywhere` — long paths WRAP, never clip
+- `.sync-btn.is-running .sync-label { display: none }` — icon-only footprint (16px icon centered in a ~47×44 pill; mobile keeps the full-width 44px bar via the pre-existing `width: 100%` rule); viewport-agnostic, defined once. Hover/`:disabled`/`:focus-visible`/`prefers-reduced-motion` untouched (they live on the button; the spinner kill at L2202 stands)
+- Mobile media query: verified no changes needed (box is a block in the 72rem container — full-width at 360–390px; no dead mobile label rules — idle label still renders, ellipsis rules kept)
+- **Defect fix (task 01 work):** `test_sync_label_mobile.py::test_pre_phase_css_reproduces_the_clip` went vacuous — its injected pre-phase rules (0,1,0) lost to the new (0,2,0) hide rule, leaving the parts `display:none`. The counterfactual injection set now also neutralizes the phase-149 hide rule (it didn't exist pre-phase), restoring the clip-geometry pin
+
+**Validation — 100% green:**
+- `uv run pytest --cov=app --cov-report=term` → 3463 passed, `app/` coverage 99% (>90%)
+- All 11 sync/upload E2E files pass in isolation: test_sync_button (3), test_sync_label_mobile (4), test_sync_cancel (3), test_sync_chunk_progress (2), test_sync_summary_visibility (3), test_sync_upload_progress (4), test_sync_changed_only (3), test_sync_model_down (4), test_force_push_resync (2), test_archive_upload_sources (5), test_upload_no_scan (3)
+- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
+- Visual verification: 8 screenshots (desktop/mobile × dark/light × idle/running) — bordered mono panel with wrapped long path, icon-only spinner button, "Sync sources" pill restored at idle, AA contrast both themes
+
+**Docs:** no doc impact (feature-level README text stays accurate; no commands/env/API/convention changes)
+
+**Next pending task:** `03_e2e_progress_box.md` (the dedicated in-flight/settled Playwright suite)
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.validate b/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.validate
new file mode 100644
index 0000000..71a2985
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__02_progress_box_styles.a1.validate
@@ -0,0 +1,120 @@
+........................................................................ [ 2%]
+........................................................................ [ 4%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 12%]
+........................................................................ [ 14%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 29%]
+........................................................................ [ 31%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 41%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 56%]
+........................................................................ [ 58%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 68%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 85%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+....... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 270 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 196 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 183 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 265 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 68 0 100%
+app/models.py 135 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 431 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 744 4 99%
+app/rag/llm.py 246 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 71 0 100%
+app/rag/pdf.py 124 0 100%
+app/rag/prompts.py 155 0 100%
+app/rag/retriever.py 248 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/schemas.py 443 0 100%
+--------------------------------------------------
+TOTAL 5609 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.err b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.md b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.md
new file mode 100644
index 0000000..fc70928
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.md
@@ -0,0 +1,16 @@
+All gates green. Final report:
+
+**Implemented**
+- `tests/e2e/test_sync_progress_box.py` — dedicated phase-149 E2E (5 tests, module app with `BOR_GIT_SOURCES=file://` fixture repo + own `BOR_SOURCES_DIR` + slow-LLM proxy, 1.5s): spinner-only button while running (`is-running`, `is-spinning`, `#sync-label` hidden, `aria-label` == box text, Cancel visible, box `aria-hidden`); box carries the EXACT full untruncated long-path label (wrap proof `scrollWidth <= clientWidth`, announcer `#sync-result` + `aria-label` equal, contrast ≥4.5:1) with the phase-129 `· chunk n/m` part pinned in the box over the slow embed window; mobile 390×844 box full-width + icon-only full-width button + wrapped full path; settle restores the byte-identical pill (`Synced HH:MM`, `aria-label` "Sync sources", `3 added`, box hidden); mid-run reload re-attaches with the box visible.
+
+**Test / lint / coverage**
+- `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` → **5 passed** in isolation (×2 runs, ~205s)
+- `uv run pytest --cov=app` → **3463 passed**, app coverage **99%**
+- `uv run ruff check . && uv run pyright` → **clean**
+- All 8 pre-existing sync E2Es green: button (3), cancel (3), chunk_progress (2), label_mobile (4), summary_visibility (3), upload_progress (4), model_down (4), changed_only (3)
+
+**Docs** — no doc impact (README's sync section was already updated by tasks 01/02; AGENTS.md unaffected).
+
+**Decisions** — mobile button full-width reference is the `.page-head` card content width (its containing block), not the `.container`; added a failed-terminal diagnostic print in the boundary wait (surfaced a test-cascade, not an app defect — both post-fix full runs fully green).
+
+**Next pending task** — none for phase 149 (task 03 was the last; harness moves the phase on completion).
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.validate b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.validate
new file mode 100644
index 0000000..71a2985
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a1.validate
@@ -0,0 +1,120 @@
+........................................................................ [ 2%]
+........................................................................ [ 4%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 12%]
+........................................................................ [ 14%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 29%]
+........................................................................ [ 31%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 41%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 56%]
+........................................................................ [ 58%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 68%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 85%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+....... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 270 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 196 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 183 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 265 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 68 0 100%
+app/models.py 135 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 431 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 744 4 99%
+app/rag/llm.py 246 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 71 0 100%
+app/rag/pdf.py 124 0 100%
+app/rag/prompts.py 155 0 100%
+app/rag/retriever.py 248 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/schemas.py 443 0 100%
+--------------------------------------------------
+TOTAL 5609 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.err b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.md b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.md
new file mode 100644
index 0000000..c912335
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.md
@@ -0,0 +1,14 @@
+**What was fixed**
+- No code defects found: the prior rejection was a missing final report (the harness's own a1 validation passed — coverage gate OK, ruff, pyright). Re-verified everything and tightened the README settle sentence (was "idle 'Sync sources' pill returns"; now byte-accurate: "Synced HH:MM" pill after success with the counts line, retry-ready "Sync sources" after failure/cancel — matching the E2E-pinned `settleSyncButton`/`applySyncSuccess` behavior).
+
+**Test / lint / coverage (all re-run this pass)**
+- `uv run pytest --cov=app --cov-report=term-missing` → **3463 passed**, app coverage **99%** (>90% gate)
+- `uv run ruff check . && uv run pyright` → **clean** (0 errors)
+- `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` → **5 passed** in isolation (198s)
+- Pre-existing sync E2Es (button/cancel/chunk_progress/label_mobile/summary_visibility/upload_progress/model_down/changed_only): **26 passed** (run on this same tree; only README changed since)
+
+**Docs** — README.md settle contract corrected (doc-only); AGENTS.md re-checked, no sync-UI content, no changes.
+
+**Notable decisions** — README edit is documentation-only (not served by the app, no test impact); the phase-149 sync section otherwise verified accurate against the E2E pins (spinner-only button, `aria-hidden` box, A4 single live region, 409/cancel contracts).
+
+**Next pending task** — none; task 03 is the last task of phase 149 (harness completes the phase).
diff --git a/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.validate b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.validate
new file mode 100644
index 0000000..71a2985
--- /dev/null
+++ b/.agents/reports/149_sync_progress_box/149_sync_progress_box__03_e2e_progress_box.a2.validate
@@ -0,0 +1,120 @@
+........................................................................ [ 2%]
+........................................................................ [ 4%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 12%]
+........................................................................ [ 14%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 29%]
+........................................................................ [ 31%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 41%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 56%]
+........................................................................ [ 58%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 68%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 85%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+....... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 270 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 196 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 183 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 265 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 68 0 100%
+app/models.py 135 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 431 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 744 4 99%
+app/rag/llm.py 246 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 71 0 100%
+app/rag/pdf.py 124 0 100%
+app/rag/prompts.py 155 0 100%
+app/rag/retriever.py 248 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/schemas.py 443 0 100%
+--------------------------------------------------
+TOTAL 5609 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/README.md b/README.md
index c264e97..25ed7cb 100644
--- a/README.md
+++ b/README.md
@@ -326,14 +326,21 @@ whole git-source refresh in one click:
rows, not the whole table); an unchanged re-sync instead gap-fills
any missing rows (`only_missing`).
-- States: the button shows **Syncing…** with a spinner while polling
- `GET /api/sync/status` every 2 s. During the import phase the label
- carries the live file, its `(n/m)` file position, and — while a file's
- embed batch is in flight — the pinned `· chunk n/m` embed position
- (batch granularity: the first chunk of the in-flight batch over the
- file's total content chunks); the full untruncated label also rides
- the button title and the aria-live result line. No client-side
- timeout — a clone + embed can legitimately take minutes.
+- States: while polling `GET /api/sync/status` every 2 s, the button
+ becomes the spinner (icon only — its label parts are hidden while a
+ run is in flight, and its accessible name is the full live label),
+ and the full live detail — "Syncing… (n/m)" during the import
+ phase (the live file, its `(n/m)` file position, and — while a file's
+ embed batch is in flight — the pinned `· chunk n/m` embed position,
+ batch granularity: the first chunk of the in-flight batch over the
+ file's total content chunks) — shows in a text box under the button
+ row, wrapping (never clipped); the same untruncated label rides the
+ button title and the aria-live result line (the sole live region —
+ the box is presentational, `aria-hidden`). On settle the box hides
+ and the button leaves the running state — the "Synced HH:MM" pill
+ after a success (the counts line lands on the result line), the
+ retry-ready "Sync sources" pill after a failure or cancel. No
+ client-side timeout — a clone + embed can legitimately take minutes.
- One sync at a time: a second trigger while a run is in flight gets a
`409`.
- **Cancel**: while a run is in flight, a **Cancel** button appears
diff --git a/TODO.md b/TODO.md
index 4640904..6256adf 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1 +1,4 @@
# TODO
+
+- [ ] The sync status on the RAG page gets "stuck" when it's embedding images. For example, when syncing test_sources/podman-systemd.pdf the status was stuck on `Syncing… podman-systemd.unit — Podman documentation/podman-systemd.unit — Podman documentation.pdf (2540/2540) · chunk 125/126` for several minutes for some reason
+- [ ] For the same podman pdf document, it extracted a series of blank white images for some reason. There don't appear to be any blank images in the document, is this a bug?
\ No newline at end of file
diff --git a/frontend/assets/sources.js b/frontend/assets/sources.js
index c4f13e7..368ea9b 100644
--- a/frontend/assets/sources.js
+++ b/frontend/assets/sources.js
@@ -361,6 +361,31 @@
* "Sync cancelled", never a stuck running state). The bor:sync-status
* event shape is unchanged — the cancelled status object flows through
* emitSyncStatus exactly like every other state.
+ *
+ * Phase 149 (task 01) — the sync detail box + the icon-only running
+ * button: the owner's report was that the full live label (a long
+ * source/relative/path + the counts + the chunk part) is clipped in
+ * the pill even on desktop — the mobile solution (stuffing the parts
+ * in the button) had become the permanent surface. The fix: while a
+ * sync/upload run is in flight, the button shows ONLY the spinning
+ * icon (the `is-running` class — task 02's CSS hides the label parts;
+ * the button IS the loading spinner, exactly the owner's ask), and a
+ * NEW bordered text box under the button row (#sync-progress, the
+ * presentational `aria-hidden` panel) shows the SAME full untruncated
+ * live label — wrapping, never clipped, breathing room. #sync-result
+ * REMAINS the sole aria-live announcer (one live region, no double
+ * announce — the box is a visual duplicate for sighted users). The
+ * wiring rides the SINGLE writer: setSyncLabel gains the `running`
+ * flag (true on the running entry — the poll ticks, the 202/409
+ * click, both re-attach entries; the settle default false) and writes
+ * the full label into the box + reveals/hides it, and owns the
+ * button's accessible name (full live label while running, the ship
+ * value "Sync sources" restored on every settle). The `is-running`
+ * class travels with the spin: added in enterSyncRunningState, removed
+ * in settleSyncButton (every terminal + both idles pass through it).
+ * The settle rendering ("Synced HH:MM" + the counts line), the Cancel
+ * control, the 409 double-trigger, the error banner/modal, and the
+ * sync status API are all UNCHANGED.
*/
import { fetchIsAdmin } from "./header.js";
@@ -695,7 +720,11 @@ export async function mount(root) {
* import phase after the file's first batch)), #sync-cancel-btn
* (the Cancel control — phase 134 task 02, ship-hidden, revealed
* only while a sync run is in flight), #sync-icon (the
- * spinner icon), #sync-result (aria-live result line),
+ * spinner icon), #sync-result (aria-live result line — the SOLE
+ * live region), #sync-progress (Phase 149 task 01: the full
+ * untruncated live label's detail box under the button row —
+ * presentational, aria-hidden, visible only while a run is in
+ * flight; the .sync-progress-text span carries the label),
* #sync-error-banner / #sync-error-text (error banner).
*/
const syncBtn = root.querySelector("#sync-btn");
@@ -709,6 +738,15 @@ export async function mount(root) {
const syncLabelCount = syncLabel ? syncLabel.querySelector(".sync-label-count") : null;
const syncLabelChunk = syncLabel ? syncLabel.querySelector(".sync-label-chunk") : null;
const syncIcon = syncBtn ? syncBtn.querySelector(".sync-icon") : null;
+ // Phase 149 (task 01): the sync detail box — the full UNTRUNCATED
+ // live label in a bordered panel under the button row, visible ONLY
+ // while a run is in flight (setSyncLabel reveals/hides it). The box
+ // is presentational (aria-hidden): #sync-result stays the SOLE
+ // aria-live announcer (one live region, no double announce).
+ const syncProgress = root.querySelector("#sync-progress");
+ const syncProgressText = syncProgress
+ ? syncProgress.querySelector(".sync-progress-text")
+ : null;
const syncResult = root.querySelector("#sync-result");
const syncErrorBanner = root.querySelector("#sync-error-banner");
const syncErrorText = root.querySelector("#sync-error-text");
@@ -755,8 +793,20 @@ export async function mount(root) {
* them are the CSS gap). textContent only — the house rule, no
* HTML strings. Every label state goes through here (the running
* builders, the idle / settle labels, the 202/409 click, the
- * re-attach). */
- function setSyncLabel(mainText, countText = "", chunkText = "") {
+ * re-attach).
+ * Phase 149 (task 01): the fourth parameter — `running` (the
+ * in-flight flag; the callsites are the ONLY decision sites: the
+ * running entry passes true, the settle passes the default false).
+ * The box (#sync-progress) gets the SAME untruncated `full` string
+ * (textContent only) and is visible IFF `running` — the detail box
+ * rides the writer, so it can never disagree with the button
+ * title / aria / the announcer. The button's accessible name rides
+ * here too: while running the icon-only button (task 02 hides the
+ * label parts) reads the FULL live label (the title already carries
+ * it for hover); every non-running call restores the ship value
+ * "Sync sources" (the static HTML attribute is the idle truth —
+ * the JS only overwrites/restores it, never removes it). */
+ function setSyncLabel(mainText, countText = "", chunkText = "", running = false) {
if (syncLabelMain) syncLabelMain.textContent = mainText;
if (syncLabelCount) {
if (countText) {
@@ -782,6 +832,15 @@ export async function mount(root) {
(chunkText ? ` ${chunkText}` : "");
if (syncBtn) syncBtn.title = full; // A4: the untruncated label rides the title, always
if (syncResult) syncResult.textContent = full; // A4: the announcer hears the whole thing
+ // Phase 149 (task 01): the detail box — the SAME untruncated label
+ // (one source, two surfaces: sighted users read the box, the
+ // announcer hears it), visible only while a run is in flight.
+ if (syncProgressText) syncProgressText.textContent = full;
+ if (syncProgress) syncProgress.hidden = !running;
+ // The running button is the spinner (task 02 hides the label
+ // parts) — its accessible name is the full live label; every
+ // settle restores the ship value (see the function docstring).
+ if (syncBtn) syncBtn.setAttribute("aria-label", running ? full : "Sync sources");
}
/* Phase 64 (task 04) + Phase 128 (task 01, the PARTS rework): the
@@ -894,7 +953,13 @@ export async function mount(root) {
* same way (absent from the bare builders — the upload job and the
* 202/409 click — so the "" default keeps the part hidden there).
* Same never-stale mechanics as before (disabled, aria-busy,
- * spinning icon, no is-error).
+ * spinning icon, no is-error). Phase 149 (task 01): the writer call
+ * passes `running = true` — the detail box reveals with the full
+ * label, and the button's accessible name becomes the full live
+ * label (the icon-only running button keeps a name; the writer
+ * owns the aria write, the entry no longer sets it) — plus the
+ * `is-running` class, which travels with the spin (task 02's CSS
+ * makes the running button icon-only).
*/
function enterSyncRunningState(kind, currentFile, done, total, status) {
if (!syncBtn) return;
@@ -904,10 +969,15 @@ export async function mount(root) {
kind === "sync" && status
? fmtSyncPhaseLabel(status)
: fmtSyncLabel(kind, currentFile, done, total);
- setSyncLabel(main, count, chunk); // the parts + the A4 untruncated title / announcer
- syncBtn.setAttribute("aria-label", "Sync sources");
+ // the parts + the A4 untruncated title / announcer + the phase-149
+ // detail box (revealed, full label) + the running aria-label
+ setSyncLabel(main, count, chunk, true);
syncBtn.classList.remove("is-error");
if (syncIcon) syncIcon.classList.add("is-spinning");
+ // Phase 149 (task 01): the icon-only running button — the class
+ // and the spin travel together (one in-flight state; the settle
+ // removes both).
+ syncBtn.classList.add("is-running");
// Phase 134 (task 02): the Cancel control is visible + enabled
// ONLY while a SYNC run is in flight — the upload job is NOT
// cancellable in this phase (its run keeps the control hidden).
@@ -927,10 +997,17 @@ export async function mount(root) {
syncBtn.setAttribute("aria-label", "Sync sources");
syncBtn.classList.remove("is-error");
if (syncIcon) syncIcon.classList.remove("is-spinning");
+ // Phase 149 (task 01): the class travels with the spin — EVERY
+ // settle (every terminal + both idles pass through here) leaves
+ // the running state.
+ syncBtn.classList.remove("is-running");
// Phase 128 (task 01): the idle / settle labels ("Sync sources",
// "Synced HH:MM") go through the parts writer — count hidden
// (absent), the label's full string rides the title + #sync-result
- // (A4, as with every label state).
+ // (A4, as with every label state). Phase 149 (task 01): the
+ // default `running = false` keeps the detail box hidden in every
+ // non-running state (idle, settled, error) and re-restores the
+ // button's idle accessible name.
setSyncLabel(label);
}
diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css
index d1710fd..9e8d9e8 100644
--- a/frontend/assets/styles.css
+++ b/frontend/assets/styles.css
@@ -2236,6 +2236,50 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
white-space: nowrap;
font-family: var(--mono); /* the same pinned-part language as the count (position reads as data) */
}
+/* Phase 149 (task 02): the sync detail box — the house's bordered panel
+ language (the .page-head card: --surface fill, 1px --line border,
+ the house radius; --line is decorative, no contrast duty, like every
+ other house border). It sits in the .sources-shell flex column whose
+ 1.25rem gap IS the house spacing above and below — no margin-top
+ (it would double the gap). min-height ≈ 2 lines (0.8rem mono at the
+ body 1.55 line-height, + the 0.75rem block padding + the 2px border,
+ border-box) so even a one-line label reads as a calm panel with
+ breathing room. The box is hidden-attr-driven (the global [hidden]
+ !important rule keeps it display:none at idle — it exists only while
+ a run is in flight). Mobile: a block in the same .container rides the
+ 100%-width phone shell — full width with no override (task 03's E2E
+ asserts it at 360px). */
+.sync-progress {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ padding: 0.75rem 1rem;
+ min-height: 4rem;
+}
+.sync-progress-text {
+ display: block;
+ font-family: var(--mono); /* the .sync-result mono language (position reads as data) */
+ font-size: 0.8rem; /* the .sync-result size */
+ line-height: 1.55; /* the house body line-height */
+ color: var(--ink-soft); /* the .sync-result ink — a PINNED identity pair on --surface
+ (8.2:1 on the built-in dark, AA in both built-ins; the
+ theme gate keeps any custom theme at >= 4.5:1) */
+ overflow-wrap: anywhere; /* THE POINT of the box — a long source/relative/path WRAPS
+ inside the panel; the pre-phase pill ellipsis is retired
+ for the live detail (never clipped) */
+}
+/* Phase 149 (task 02): the running button IS the loading spinner (the
+ owner's ask) — the label parts hide via the .is-running class
+ (sources.js adds/removes it exactly where the icon's .is-spinning
+ travels). The pill's padding/border stay, so the control never
+ collapses: the 16px icon centers in a ~47x44px pill (desktop) or the
+ full-width 44px bar (mobile — the pre-phase .sync-btn { width: 100% }
+ rule keeps the big in-flight target, viewport-agnostic rule defined
+ ONCE here). :hover, :disabled, and :focus-visible are untouched —
+ they live on the .sync-btn element, and hiding the label span touches
+ neither. The idle "Sync sources" label (and the mobile ellipsis rules
+ for it) render exactly as pre-phase when the class is absent. */
+.sync-btn.is-running .sync-label { display: none; }
/* Last-result announcer — soft ink, small mono */
.sync-result {
display: block;
diff --git a/frontend/index.html b/frontend/index.html
index 0684255..0d4d5d5 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -482,6 +482,17 @@
to pull the latest and re-import.
+
+
+
+