From 1139f007cf9d472a956f8a93d79fd04e2e9d9eb3 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Sun, 27 Sep 2026 23:58:57 -0400 Subject: [PATCH] phase: 128_sync_label_mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything verified and 100% green. The earlier rejection was a report-format issue, not a test failure — all work is in the working tree. Final report: **Phase 128 final pass — verified (all 3 tasks complete; no code defects found)** - Verified: `#sync-label` parts markup, `setSyncLabel` writer, part builders (A4 untruncated title/aria-live), flex-row CSS + mobile media query — all match design 1:1. - `uv run pytest --cov=app --cov-report=term`: 3024 passed; TOTAL coverage **99%** (>90% gate). - `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`: 4 passed (119s, isolated). - `uv run ruff check .`: clean; `uv run pyright`: 0 errors. - Criteria: (1) mobile 390×844 label in-viewport, count visible in import+summaries — ✅ E2E; (2) desktop strings/cap/title unchanged — ✅ E2E+unit; (3) E2E isolated green — ✅; (4) full suite green — ✅. - Docs: README.md fixed 5 pre-existing gaps (PDF phase-126 section + `pdf` token, hidden-folders toggle ph.105, document dates ph.106, private git-source masked tokens ph.121, doc-draft Discard ph.115); AGENTS.md verified consistent — no changes. - Notable: double-`-q` (addopts `-q` + CLI `-q`) suppresses pytest's summary line — standard `-qq` behavior, not a bug; exit codes confirm green. - Next pending phase: `129_sync_chunk_progress`. --- .../128_sync_label_mobile/00_phase.md | 0 .../128_sync_label_mobile/01_label_parts.md | 0 .../128_sync_label_mobile/02_mobile_css.md | 0 .../03_e2e_mobile_label.md | 0 .../128_sync_label_mobile__00_phase.a1.err | 0 .../128_sync_label_mobile__00_phase.a1.md | 14 + ...28_sync_label_mobile__00_phase.a1.validate | 113 +++ .../128_sync_label_mobile__00_phase.a2.err | 0 .../128_sync_label_mobile__00_phase.a2.md | 11 + ...28_sync_label_mobile__00_phase.a2.validate | 113 +++ ...8_sync_label_mobile__01_label_parts.a1.err | 0 ...28_sync_label_mobile__01_label_parts.a1.md | 17 + ...c_label_mobile__01_label_parts.a1.validate | 113 +++ ...8_sync_label_mobile__01_label_parts.a2.err | 0 ...28_sync_label_mobile__01_label_parts.a2.md | 15 + ...c_label_mobile__01_label_parts.a2.validate | 113 +++ ...28_sync_label_mobile__02_mobile_css.a1.err | 0 ...128_sync_label_mobile__02_mobile_css.a1.md | 18 + ...nc_label_mobile__02_mobile_css.a1.validate | 113 +++ ...c_label_mobile__03_e2e_mobile_label.a1.err | 0 ...nc_label_mobile__03_e2e_mobile_label.a1.md | 16 + ...el_mobile__03_e2e_mobile_label.a1.validate | 113 +++ ...c_label_mobile__03_e2e_mobile_label.a2.err | 0 ...nc_label_mobile__03_e2e_mobile_label.a2.md | 14 + ...el_mobile__03_e2e_mobile_label.a2.validate | 113 +++ README.md | 61 +- frontend/assets/sources.js | 164 +++- frontend/assets/styles.css | 33 +- frontend/index.html | 2 +- tests/e2e/test_sync_label_mobile.py | 816 ++++++++++++++++++ tests/e2e/test_sync_summary_visibility.py | 59 +- tests/e2e/test_sync_upload_progress.py | 36 +- tests/unit/test_frontend_sync_upload.py | 236 +++-- tests/unit/test_sync_button.py | 73 +- 34 files changed, 2203 insertions(+), 173 deletions(-) rename .agents/phases/{todo => complete}/128_sync_label_mobile/00_phase.md (100%) rename .agents/phases/{todo => complete}/128_sync_label_mobile/01_label_parts.md (100%) rename .agents/phases/{todo => complete}/128_sync_label_mobile/02_mobile_css.md (100%) rename .agents/phases/{todo => complete}/128_sync_label_mobile/03_e2e_mobile_label.md (100%) create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.validate create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.err create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.md create mode 100644 .agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.validate create mode 100644 tests/e2e/test_sync_label_mobile.py diff --git a/.agents/phases/todo/128_sync_label_mobile/00_phase.md b/.agents/phases/complete/128_sync_label_mobile/00_phase.md similarity index 100% rename from .agents/phases/todo/128_sync_label_mobile/00_phase.md rename to .agents/phases/complete/128_sync_label_mobile/00_phase.md diff --git a/.agents/phases/todo/128_sync_label_mobile/01_label_parts.md b/.agents/phases/complete/128_sync_label_mobile/01_label_parts.md similarity index 100% rename from .agents/phases/todo/128_sync_label_mobile/01_label_parts.md rename to .agents/phases/complete/128_sync_label_mobile/01_label_parts.md diff --git a/.agents/phases/todo/128_sync_label_mobile/02_mobile_css.md b/.agents/phases/complete/128_sync_label_mobile/02_mobile_css.md similarity index 100% rename from .agents/phases/todo/128_sync_label_mobile/02_mobile_css.md rename to .agents/phases/complete/128_sync_label_mobile/02_mobile_css.md diff --git a/.agents/phases/todo/128_sync_label_mobile/03_e2e_mobile_label.md b/.agents/phases/complete/128_sync_label_mobile/03_e2e_mobile_label.md similarity index 100% rename from .agents/phases/todo/128_sync_label_mobile/03_e2e_mobile_label.md rename to .agents/phases/complete/128_sync_label_mobile/03_e2e_mobile_label.md diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.md new file mode 100644 index 0000000..a55df90 --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.md @@ -0,0 +1,14 @@ +All green. Final report: + +**Phase 128 — final verification pass (all 3 tasks already complete)** +- **Verified implementation**: `#sync-label` split into `.sync-label-main` (ellipsizable) + `.sync-label-count` (pinned, `hidden` when absent); single `setSyncLabel(main, count)` writer; builders return parts; A4 untruncated label still rides button `title` + `#sync-result`; CSS flex-row + mobile `flex: 1 1 auto` media query all match the design 1:1. +- **Completion criteria**: + - Mobile 390×844 label fully in viewport, count visible in import AND summaries — ✅ (E2E bounding-box assertions) + - Desktop byte-identical strings/cap/title — ✅ (E2E + unit pins) + - `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov` in isolation — ✅ 4 passed (119s) + - `uv run pytest` (unit+integration) — ✅ 3024 passed +- **Coverage**: `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% gate). +- **Lint/types**: `uv run ruff check .` clean; `uv run pyright` → 0 errors. +- **Docs**: README.md — fixed 5 pre-existing gaps: added "PDF documents (phase 126)" section + `pdf` token in indexed-formats/config reference; documented hidden-folders toggle (ph.105), document dates (ph.106), private git-source masked tokens (ph.121, old "no credentials stored" claim corrected), doc-draft Discard (ph.115). AGENTS.md — verified consistent, no changes needed. +- **Notable**: no code defects found; pytest "missing summary line" under double-`-q` is standard `-qq` behavior, not a bug. +- **Next pending phase**: `129_sync_chunk_progress`. diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a1.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.md new file mode 100644 index 0000000..dfc7caa --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.md @@ -0,0 +1,11 @@ +Everything verified and 100% green. The earlier rejection was a report-format issue, not a test failure — all work is in the working tree. Final report: + +**Phase 128 final pass — verified (all 3 tasks complete; no code defects found)** +- Verified: `#sync-label` parts markup, `setSyncLabel` writer, part builders (A4 untruncated title/aria-live), flex-row CSS + mobile media query — all match design 1:1. +- `uv run pytest --cov=app --cov-report=term`: 3024 passed; TOTAL coverage **99%** (>90% gate). +- `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`: 4 passed (119s, isolated). +- `uv run ruff check .`: clean; `uv run pyright`: 0 errors. +- Criteria: (1) mobile 390×844 label in-viewport, count visible in import+summaries — ✅ E2E; (2) desktop strings/cap/title unchanged — ✅ E2E+unit; (3) E2E isolated green — ✅; (4) full suite green — ✅. +- Docs: README.md fixed 5 pre-existing gaps (PDF phase-126 section + `pdf` token, hidden-folders toggle ph.105, document dates ph.106, private git-source masked tokens ph.121, doc-draft Discard ph.115); AGENTS.md verified consistent — no changes. +- Notable: double-`-q` (addopts `-q` + CLI `-q`) suppresses pytest's summary line — standard `-qq` behavior, not a bug; exit codes confirm green. +- Next pending phase: `129_sync_chunk_progress`. diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__00_phase.a2.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.md new file mode 100644 index 0000000..a10c7b7 --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.md @@ -0,0 +1,17 @@ +All green and consistent. Final report: + +**Implemented (task 01 — `#sync-label` parts)** +- `frontend/index.html`: `#sync-label` now ships two child spans — `.sync-label-main` (carries the idle "Sync sources" pre-JS) + `.sync-label-count` (hidden, empty). +- `frontend/assets/sources.js`: new `setSyncLabel(mainText, countText = "")` is the SINGLE label writer (main part textContent; count part textContent + `hidden` toggle — hidden/emptied when absent; the untruncated full label `main + " " + count` on `syncBtn.title` + `#sync-result`, A4). `fmtSyncLabel`/`fmtSyncPhaseLabel` return `{main, count}` (decision trees + byte-identical copy preserved, incl. always-emitted summaries count and bare upload "Importing"); `enterSyncRunningState` and `settleSyncButton` (idle/settle) go through it. textContent only — no HTML strings. +- `tests/unit/test_frontend_sync_upload.py`: label pins re-pointed to the part shape (main/count per state: prelude, import ±total, overview, summaries ±folder, upload, idle/settle; A4 full-string join; hidden toggle; single-writer + no-direct-write pins). `tests/unit/test_sync_button.py`: markup + running/settle pins updated. +- Fixed two existing E2E suites broken by the DOM split (step 8): `test_sync_upload_progress.py` (count-label regexes → new visible concatenation + pinned visible count span + full-space title) and `test_sync_summary_visibility.py` (atomic JS check → main/count part regexes + `title === main + " " + count`). + +**Verification** +- `uv run pytest --cov=app --cov-report=term-missing` → **3024 passed, 0 failed**, `app/` coverage **99%** (>90% gate) +- `uv run ruff check .` → all passed; `uv run pyright` → 0 errors, 0 warnings +- E2E in isolation: `test_sync_button.py` 3 ✓, `test_sync_summary_visibility.py` 3 ✓, `test_sync_upload_progress.py` 4 ✓, `test_sync_model_down.py` 4 ✓, `test_upload_no_scan.py` 3 ✓ +- Node harness: every running/idle/settle full string byte-identical to pre-task; count span hidden whenever absent. + +**Docs:** no doc impact (README's "Syncing…" line stays accurate; AGENTS.md unchanged). +**Decisions:** settled/idle labels now also ride the title + `#sync-result` (the task's unconditional `setSyncLabel` spec — "title + aria-live always carry the whole thing"); visible file↔count space is now the CSS gap (task 02). No live-infra changes. +**Next pending task:** `.agents/phases/todo/128_sync_label_mobile/02_mobile_css.md` diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a1.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.md new file mode 100644 index 0000000..353dcd8 --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.md @@ -0,0 +1,15 @@ +All green. Everything verified — final report: + +**Implemented (task 01 — `#sync-label` parts; changes intact from prior attempt, fully re-verified)** +- `frontend/index.html`: `#sync-label` ships `.sync-label-main` (idle "Sync sources" pre-JS) + `.sync-label-count` (hidden, empty). +- `frontend/assets/sources.js`: `setSyncLabel(mainText, countText="")` is the single label writer — parts via textContent (count `hidden` whenever absent), untruncated full label `main + " " + count` on `syncBtn.title` + `#sync-result` (A4); `fmtSyncLabel`/`fmtSyncPhaseLabel` return `{main, count}` (decision trees + byte-identical copy); `enterSyncRunningState` + `settleSyncButton` route through it. No HTML strings. +- Unit pins re-pointed to the part shape (`test_frontend_sync_upload.py`, `test_sync_button.py`); fixed the two E2E suites broken by the DOM split (`test_sync_upload_progress.py`, `test_sync_summary_visibility.py`). + +**Results (exact commands)** +- `uv run pytest --cov=app --cov-report=term-missing` → **3024 passed, 0 failed**; `app/` coverage **99%** (>90% gate) +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings +- E2E in isolation: `test_sync_button.py` 3 ✓, `test_sync_summary_visibility.py` 3 ✓, `test_sync_upload_progress.py` 4 ✓ (all `--no-cov`) + +**Docs:** no doc impact (README's "Syncing…" line stays accurate; AGENTS.md unchanged). +**Decisions:** settle/idle labels also ride title + `#sync-result` per the task's unconditional `setSyncLabel` spec; the visible file↔count space becomes the CSS gap (task 02's job). No live-infra changes. +**Next pending task:** `.agents/phases/todo/128_sync_label_mobile/02_mobile_css.md` diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__01_label_parts.a2.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.md new file mode 100644 index 0000000..bc3d78b --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.md @@ -0,0 +1,18 @@ +All green. Task 02 is complete. + +**Implemented** +- `frontend/assets/styles.css` — `.sync-label` is now a flex row (`inline-flex; align-items: baseline; gap: 0.35rem; min-width: 0; max-width: min(16rem, 40vw)`); new `.sync-label-main` (the shrinkable ellipsizing part) and `.sync-label-count` (`flex: 0 0 auto`, never clipped, `var(--mono)`); phase-64 comment kept + updated to name the parts +- Mobile 640px block — `.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }` (label fills the full-width pill; count pinned at the right edge); comment updated; `#sync-result` clip / aria-labels / other rules untouched +- Audit: only base + mobile rules exist; sole stylesheet is `styles.css`; theming is variable-only — B4 byte-identity holds (static CSS, no `ui_settings` dependency) +- Updated the existing CSS pin in `tests/unit/test_frontend_sync_upload.py` to the part shape (container props, main/count blocks, mobile fill rule) +- Verified layout in Chromium at 390×844 and 1440×900: long path ellipsizes, `(n/m)` fully on-screen at the pill's right edge on mobile; desktop label caps at exactly 16rem; short/idle labels unclipped, hidden count takes no space + +**Tests / lint** +- `uv run pytest --cov=app --cov-report=term` → 3024 passed, 0 failed; `app/` TOTAL 99% (>90% gate) +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings + +**Docs:** no doc impact (pure CSS layout fix to an existing feature; README/AGENTS.md don't reference the label CSS) + +**Deviations:** none — CSS applied verbatim per task; only the stale mobile-block comment sentence was updated alongside the rule. + +**Next pending task:** `.agents/phases/todo/128_sync_label_mobile/03_e2e_mobile_label.md` (the Playwright bounding-box E2E, run in isolation) diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__02_mobile_css.a1.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.md new file mode 100644 index 0000000..2daa83d --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.md @@ -0,0 +1,16 @@ +All green. Final report: + +**Implemented (task 03 — `tests/e2e/test_sync_label_mobile.py`, new)** +- Dedicated mock-only Playwright suite (house pattern from `test_sync_summary_visibility.py`): module SLOW app (port 8164) behind the `slow_llm` proxy (8905, `SLOW_DELAY_S=1.5`), `synclabel/` local source (2 files × `alpha/`+`bravo/` → 3 summary candidates), autouse truncate cleanup +- 4 tests: mobile 390×844 import (label in viewport, `(n/4)` count visible & never clipped, `title` untruncated A4) · mobile summaries (`Summarizing folders…` + `(n/3)` visible, settle hides count) · desktop 1280×800 (pre-phase byte-identical string, 256px `min(16rem,40vw)` cap, count pinned) · pre-phase-CSS bug reproduction (permanent pin check) + +**Test / lint / coverage** +- `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov` → 4 passed (in isolation) +- Combined with `test_sync_button` + `test_sync_summary_visibility` + `test_sync_upload_progress` → 14 passed +- `uv run pytest --cov=app` → 3024 passed, app coverage 99% (>90%) · `uv run ruff check .` clean · `uv run pyright` 0 errors + +**Docs:** no doc impact (no setup/command/API changes; README's sync-button "States" bullet remains accurate) + +**Notable decisions:** added the 4th reproduction test to encode the "mobile tests fail pre-phase" criterion permanently (probe-measured: pre-phase count box 352.9→386.2 fully past the 339.2 clip edge — tests 1–2's pins fail exactly there). Pre-phase CSS injected via CSSOM `insertRule` into the origin stylesheet (A20 CSP blocks `add_style_tag`). Two Playwright quirks handled: object `wait_for_function` results need `.json_value()`; this Chromium build's index-less `insertRule` prepends, so an explicit end index is passed. `_settle` polls the status endpoint for the terminal before the UI settle (informative failures). + +**Next pending task:** phase 128 is complete (01/02/03 done); next is `129_sync_chunk_progress` (00_phase onward, per `todo/` order). diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a1.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.err b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.md b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.md new file mode 100644 index 0000000..7f8f6c1 --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.md @@ -0,0 +1,14 @@ +Everything is green — the previous rejection was a reporting hiccup, not a test failure (the suite was already in place and passing). Full re-verification this run: + +**Implemented** — `tests/e2e/test_sync_label_mobile.py` (task 03): mock-only Playwright suite on a SLOW module app (slow-proxy `SLOW_DELAY_S=1.5`, own port) with the `synclabel/` local source (4 files, 3 summary candidates): mobile 390×844 import (label in viewport, `(n/4)` count visible/never clipped, A4 `title` untruncated) · mobile summaries (`Summarizing folders…` + `(n/3)` visible, settle hides count) · desktop 1280×800 (pre-phase byte-identical string, 256 px `min(16rem,40vw)` cap) · pre-phase-CSS reproduction test (permanent pin check — its "count not clipped" pins are exactly what tests 1–2 fail against). + +**Results (this run)** +- `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov` → **4 passed** in isolation +- `uv run pytest --cov=app --cov-report=term` → **3024 passed**, app coverage **99%** (>90%) +- `uv run ruff check .` → All checks passed · `uv run pyright` → **0 errors, 0 warnings** + +**Docs:** consistency check run (phase final pass) — no impact; README/AGENTS.md remain accurate. + +**Notable decisions:** pre-phase CSS injected via CSSOM `insertRule` with explicit end index into the origin stylesheet (A20 CSP blocks `add_style_tag`; this Chromium build's index-less `insertRule` prepends). `_settle` polls the status endpoint for the terminal first for informative failures. + +**Next pending task:** phase 128 complete → next is `129_sync_chunk_progress` (per `todo/` order). diff --git a/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.validate b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.validate new file mode 100644 index 0000000..cdd7d1e --- /dev/null +++ b/.agents/reports/128_sync_label_mobile/128_sync_label_mobile__03_e2e_mobile_label.a2.validate @@ -0,0 +1,113 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 14%] +........................................................................ [ 16%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 23%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 40%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 47%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 64%] +........................................................................ [ 66%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 80%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [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 262 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 179 1 99% +app/api/git_sources.py 241 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 139 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 374 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 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 244 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 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 49 0 100% +app/schemas.py 400 0 100% +-------------------------------------------------- +TOTAL 5133 22 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 3517ae2..5781b8b 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,13 @@ fresh deployment they seed from `BOR_SUGGESTIONS`. ### RAG / Knowledge base (`/sources.html`) The indexed document list. The *Path* column opens each document in the same -**almost-fullscreen modal**. Admin-only — anonymous visitors see a sign-in -gate instead. +**almost-fullscreen modal**. Document rows carry a *Created* date and folder +rows an *Updated* date (phase 106) — sourced at sync time (git commit dates +for git sources, restored metadata for archive uploads, file mtime otherwise; +unresolvable or future dates fall back to today) — and the admin can correct a +document's date from the document viewer. The date also feeds the small +recency boost in retrieval ranking (`BOR_RECENCY_BOOST`). Admin-only — +anonymous visitors see a sign-in gate instead. ### Git sources (`/git-sources.html`) @@ -118,7 +123,8 @@ everyone — no login or token required. ### Edit doc (`/doc-edit.html`) Review and adjust an AI-generated answer before committing it to the docs -repository. Admin-only flow page. +repository. A draft that should never be pushed can be **Discard**ed from +the same page (permanent; admin-only). Admin-only flow page. --- @@ -200,9 +206,15 @@ reflect the new files. so it is safe to run from cron or after every commit. - Indexed formats: **md, markdown, txt, yaml, yml, json, py**, the Podman quadlet family (**container, network, volume, image, pod, kube, swap, - os, endpoint**), and **j2** Jinja templates (case-insensitive). + os, endpoint**), **j2** Jinja templates — and, **opt-in**, **pdf** + (add the `pdf` token to `BOR_IMPORT_EXTENSIONS`; see + [PDF documents](#pdf-documents-phase-126)). - Hidden files and common cache dirs (`.venv`, `node_modules`, `.git`, - `__pycache__`, etc.) are skipped. + `__pycache__`, etc.) are skipped — unless the per-source **Index hidden + folders** checkbox on the Git sources page is on for that source + (phase 105): then dot-prefixed folders/files with an importable + extension are indexed like visible ones, and switching it off prunes + them on the next sync. - Non-markdown files get format-aware chunking (YAML keys, JSON keys, Python classes/defs). - With `BOR_IMAGES=1`, standalone images (`png, jpg, jpeg, webp, gif, @@ -227,8 +239,14 @@ BOR_GIT_SOURCES=https://git.reeseapps.com/reese/homelab.git,git@github.com:reese BOR_SOURCES_DIR=~/bor-sources # default; each repo lands in // ``` -- Auth is whatever the machine supplies — HTTPS via the OS credential - helper, or SSH via your key; no credentials are stored in the app. +- Public repos need no setup: auth is whatever the machine supplies — + HTTPS via the OS credential helper, or SSH via your key. +- Private repos: the add/edit form on the Git sources page takes an + optional **masked token** field (phase 121). The token is stored in a + dedicated DB column and injected into the clone URL only at + sync/clone time — it is never returned by any API response or shown + in the UI (legacy rows that already embed `user:token@` in the stored + URL keep working — the token is sanitized on output). - Every run **clones** (first time, shallow `--depth 1`) or **pulls** (`git pull --ff-only`) each repo, then indexes the checkout. - A failed sync aborts the run — no partial junk. @@ -494,6 +512,33 @@ of its answer: --- +## PDF documents (phase 126) + +PDFs are **opt-in**: add the `pdf` token to `BOR_IMPORT_EXTENSIONS` +(a stock `.env` never indexes them). Once admitted, a PDF is a +first-class document through every entry point (git/local walks and +uploaded archives): + +- **Text, page-aware.** Text is extracted page by page (`pymupdf`); one + document per PDF, its text-bearing pages joined with `---PAGE-N---` + separators. Chunking is page-aware — every chunk keeps a `Page N:` + anchor, so a retrieval hit stays readable in context. +- **Summarized + dated like any document.** The `lite` model summarizes + it, sync sources its date (git commit / file metadata), and it appears + in the catalog, the KB tree, and the agent's `ls` / `read` / `grep` + tools. +- **Embedded images join the image pipeline.** With `BOR_IMAGES=1`, + images embedded in the PDF are extracted and run through the same + image-document flow as standalone images (chat-model description, + persistent copy under `BOR_IMAGE_DIR`, retrievable + displayable); + with the toggle off the pass is skipped and the PDF text indexes + normally. Embedded images in a format outside `BOR_IMAGE_EXTENSIONS` + are skipped with a warning. +- **Served as text.** The document viewer renders the extracted text — + never the binary PDF. + +--- + ## Caching & deploys A deploy is a commit — and the browser must see it **without a hard refresh**. @@ -611,7 +656,7 @@ to `.env` and adjust. | Env | Default | Meaning | |-----|---------|---------| -| `BOR_IMPORT_EXTENSIONS` | A9 family | Importable text formats — the A9 default is `md,markdown,txt,yaml,yml,json,py,container,network,volume,image,pod,kube,swap,os,endpoint,j2`; any well-formed extension is allowed. Images are **not** added here — see `BOR_IMAGES` | +| `BOR_IMPORT_EXTENSIONS` | A9 family | Importable text formats — the A9 default is `md,markdown,txt,yaml,yml,json,py,container,network,volume,image,pod,kube,swap,os,endpoint,j2`; any well-formed extension is allowed. Add the `pdf` token to make PDFs first-class, retrievable documents (phase 126, opt-in). Images are **not** added here — see `BOR_IMAGES` | | `BOR_GIT_SOURCES` | — (empty) | CSV of git repo URLs — **fallback while the admin Git sources page's list is empty** | | `BOR_SOURCES_DIR` | `~/bor-sources` | Where git repos are cloned/pulled | | `BOR_UPLOAD_DIR` | `~/bor-sources/uploads` | Where uploaded source archives are unpacked | diff --git a/frontend/assets/sources.js b/frontend/assets/sources.js index b5cc3b3..d4e04b9 100644 --- a/frontend/assets/sources.js +++ b/frontend/assets/sources.js @@ -300,6 +300,34 @@ * The glyph is static SVG (aria-hidden — the alt text is the * accessible content); the box + img are properties only, never * innerHTML with document-derived data (the house rule). + * + * Phase 128 (task 01) — the sync label becomes PARTS: the running + * label's string is split into a shrinks-to-fit MAIN part (prefix + + * the file/folder path — the part CSS may ellipsize) and a pinned + * COUNT part ("(n/m)" — the position, NEVER clipped), so on a phone + * the file path ellipsizes to fit while the count stays on-screen + * (the phase-64 bug: with max-width lifted, the inline-block sized to + * its full content and the count ran off-screen). #sync-label now + * ships TWO child spans — .sync-label-main (the initial idle text + * lives there so the pre-JS label renders) + .sync-label-count + * (hidden + empty by default) — and the label builders no longer + * return one string: fmtSyncLabel / fmtSyncPhaseLabel return the + * PARTS ({ main, count } — the same decision tree, the same byte- + * identical copy), and ONE writer — setSyncLabel(mainText, + * countText) — lands them: mainText → the main part's textContent, + * countText → the count part's textContent with the hidden toggle + * (absent count → hidden + empty), then the UNTRUNCATED full label + * — mainText + (countText ? " " + countText : "") — onto the button + * title + #sync-result (A4 — the screen reader hears the whole + * thing; the visible span carries main + count, the space between + * them is the CSS gap). Every other #sync-label writer — the idle / + * settle labels ("Sync sources", "Synced HH:MM"), the 202/409 click, + * the initSyncButton re-attach — goes through setSyncLabel(main) + * (count hidden). The full-string shapes are byte-identical to the + * pre-task labels ("Syncing…", "Syncing… ", + * "Syncing… (n/m)", "Importing", "Writing KB overview…", + * "Summarizing folders… (n/m)"); textContent only — no HTML + * strings (the house rule). */ import { fetchIsAdmin } from "./header.js"; @@ -598,12 +626,17 @@ export async function mount(root) { * upload the same way; a terminal upload is a no-op there (the * boot-time loadTree() already shows the current catalog). * - * Elements: #sync-btn (the button), #sync-label (the text), - * #sync-icon (the spinner icon), #sync-result (aria-live result - * line), #sync-error-banner / #sync-error-text (error banner). + * Elements: #sync-btn (the button), #sync-label (the text — + * Phase 128 (task 01): the PARTS — .sync-label-main, the + * shrinks-to-fit prefix + file/folder, + .sync-label-count, the + * pinned "(n/m)" position (hidden when absent)), #sync-icon (the + * spinner icon), #sync-result (aria-live result line), + * #sync-error-banner / #sync-error-text (error banner). */ const syncBtn = root.querySelector("#sync-btn"); const syncLabel = syncBtn ? syncBtn.querySelector(".sync-label") : null; + const syncLabelMain = syncLabel ? syncLabel.querySelector(".sync-label-main") : null; + const syncLabelCount = syncLabel ? syncLabel.querySelector(".sync-label-count") : null; const syncIcon = syncBtn ? syncBtn.querySelector(".sync-icon") : null; const syncResult = root.querySelector("#sync-result"); const syncErrorBanner = root.querySelector("#sync-error-banner"); @@ -632,45 +665,86 @@ export async function mount(root) { return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } - /* Phase 64 (task 04): the live-file label. `kind` picks the prefix — - * "sync" → "Syncing…", "upload" → "Importing" (the background run's - * word, A3). The current file — the status endpoint's full - * source/relative/path (A4) — is appended while one is being processed; - * the BARE prefix shows during the clone/pull (sync), before any file - * is indexed. Phase 90: the upload run is unpack + register only (no - * scan), so its status never carries a file or counts — the - * "Importing" label is always the bare one. The counts appear only - * once the import has started (total > 0). CSS ellipsizes the button - * label; the same untruncated text goes to the button title + - * #sync-result (the aria-live announcer). Phase 98 (task 02): the - * SYNC job's post-import phases (overview / summaries) take their - * own label via fmtSyncPhaseLabel (below) — this builder keeps that + /* Phase 128 (task 01): the label PARTS writer — the SINGLE point + * that writes #sync-label. mainText lands in the MAIN part + * (.sync-label-main — the shrinks-to-fit prefix + file/folder, the + * part CSS ellipsizes); countText lands in the COUNT part + * (.sync-label-count — the pinned "(n/m)" position, NEVER clipped): + * present → its textContent + hidden=false, absent → emptied + + * hidden=true (the count span is hidden whenever there is no + * "(n/m)" to show). Then the UNTRUNCATED full label — + * mainText + (countText ? " " + countText : "") — rides the button + * title + #sync-result (the aria-live announcer) exactly as before + * (A4 — the screen reader hears the whole thing; the visible span + * carries main + count, the space between them is 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 = "") { + if (syncLabelMain) syncLabelMain.textContent = mainText; + if (syncLabelCount) { + if (countText) { + syncLabelCount.textContent = countText; + syncLabelCount.hidden = false; + } else { + syncLabelCount.textContent = ""; + syncLabelCount.hidden = true; + } + } + const full = countText ? `${mainText} ${countText}` : mainText; + 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 64 (task 04) + Phase 128 (task 01, the PARTS rework): the + * live-file label. `kind` picks the prefix — "sync" → "Syncing…", + * "upload" → "Importing" (the background run's word, A3). The + * current file — the status endpoint's full source/relative/path + * (A4) — lands in the MAIN part while one is being processed; the + * BARE prefix shows during the clone/pull (sync), before any file + * is indexed. Phase 90: the upload run is unpack + register only + * (no scan), so its status never carries a file or counts — the + * "Importing" label is always the bare one. The counts — the PINNED + * count part, never clipped (Phase 128) — appear only once the + * import has started (total > 0). The builders return the PARTS + * ({ main, count }); setSyncLabel writes them (the untruncated full + * string — main + " " + count — rides the button title + + * #sync-result, A4). Phase 98 (task 02): the SYNC job's + * post-import phases (overview / summaries) take their own label + * via fmtSyncPhaseLabel (below) — this builder keeps that * prelude/import fall-through byte-identical and serves the UPLOAD * job unchanged (its status has no phase — D2). */ function fmtSyncLabel(kind, currentFile, done, total) { const prefix = kind === "upload" ? "Importing" : "Syncing…"; - let label = currentFile ? `${prefix} ${currentFile}` : prefix; - if (total > 0) label += ` (${done}/${total})`; - return label; + const main = currentFile ? `${prefix} ${currentFile}` : prefix; + const count = total > 0 ? `(${done}/${total})` : ""; + return { main, count }; } - /* Phase 98 (task 02, D2): the SYNC job's phase-aware running label - * — the post-import span where the file count sits still (the - * owner's "the number pauses for a really long time"): "overview" - * → "Writing KB overview…"; "summaries" → "Summarizing folders… - * (n/m)" — the folder part is omitted while - * current_summary is null (the phase's first poll); anything else - * (the null prelude, "import") → the phase-64 fmtSyncLabel - * fall-through, byte-identical (same builder, same fields, same - * order). The UPLOAD job never passes a status (its endpoint has no - * phase — D2) and keeps the bare fmtSyncLabel label. The untruncated - * result rides the button title + #sync-result like every running - * label (A4). */ + /* Phase 98 (task 02, D2) + Phase 128 (task 01, the PARTS rework): + * the SYNC job's phase-aware running label — the post-import span + * where the file count sits still (the owner's "the number pauses + * for a really long time"): "overview" → "Writing KB overview…" + * (no count part); "summaries" → main "Summarizing folders… + * " + count "(n/m)" — the folder part is omitted + * while current_summary is null (the phase's first poll) and the + * COUNT IS ALWAYS EMITTED in this phase (the hook always carries a + * total); anything else (the null prelude, "import") → the + * phase-64 fmtSyncLabel parts, byte-identical (same builder, same + * fields, same order). The UPLOAD job never passes a status (its + * endpoint has no phase — D2) and keeps the bare fmtSyncLabel + * parts. The untruncated full label rides the button title + + * #sync-result like every running label (A4 — setSyncLabel). */ function fmtSyncPhaseLabel(status) { - if (status.phase === "overview") return "Writing KB overview…"; + if (status.phase === "overview") { + return { main: "Writing KB overview…", count: "" }; + } if (status.phase === "summaries") { const folder = status.current_summary ? ` ${status.current_summary}` : ""; - return `Summarizing folders…${folder} (${status.summaries_done}/${status.summaries_total})`; + return { + main: `Summarizing folders…${folder}`, + count: `(${status.summaries_done}/${status.summaries_total})`, + }; } // null phase (the prelude) or "import" — today's byte-identical // sync label (the phase-64 contract, unchanged). @@ -702,26 +776,26 @@ export async function mount(root) { * is phase-aware (fmtSyncPhaseLabel: "Writing KB overview…" / * "Summarizing folders… (n/m)"); the UPLOAD job and the * 202/409 click (no status yet) keep the bare fmtSyncLabel label. - * Same mechanics as before (disabled, aria-busy, spinning icon, no - * is-error) plus: the FULL untruncated label on the button title - * (set for EVERY running label — the phase-64 file-only rule is - * adjusted for the phase labels) and in #sync-result (the aria-live - * announcer reads it; CSS ellipsizes the button's label span only). + * Phase 128 (task 01): the builders return the PARTS and the single + * writer (setSyncLabel) lands them — the main part (CSS-ellipsized), + * the pinned count part (hidden when absent), and the FULL + * untruncated label on the button title + #sync-result (A4 — set + * for EVERY running label; CSS ellipsizes the main part only). Same + * never-stale mechanics as before (disabled, aria-busy, spinning + * icon, no is-error). */ function enterSyncRunningState(kind, currentFile, done, total, status) { if (!syncBtn) return; syncBtn.disabled = true; syncBtn.setAttribute("aria-busy", "true"); - const label = + const { main, count } = kind === "sync" && status ? fmtSyncPhaseLabel(status) : fmtSyncLabel(kind, currentFile, done, total); - syncBtn.title = label; // A4: the untruncated label rides the title, always + setSyncLabel(main, count); // the parts + the A4 untruncated title / announcer syncBtn.setAttribute("aria-label", "Sync sources"); syncBtn.classList.remove("is-error"); if (syncIcon) syncIcon.classList.add("is-spinning"); - if (syncLabel) syncLabel.textContent = label; - if (syncResult) syncResult.textContent = label; } function settleSyncButton(label) { @@ -732,7 +806,11 @@ export async function mount(root) { syncBtn.setAttribute("aria-label", "Sync sources"); syncBtn.classList.remove("is-error"); if (syncIcon) syncIcon.classList.remove("is-spinning"); - if (syncLabel) syncLabel.textContent = label; + // 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). + setSyncLabel(label); } function showSyncError(detail) { diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 9a2fa6a..16cf747 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -2063,14 +2063,28 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und /* Phase 64: the live-file label ("Syncing… (n/m)" / "Importing (n/m)") — a long source/relative/path ellipsizes inside the pill; the full path lives in the button title + #sync-result (the - aria-live announcer). */ + aria-live announcer). Phase 128 (task 02): the label is a FLEX ROW + of two parts — .sync-label-main (the prefix + file/folder: min-width + 0, the shrinkable item whose ellipsis engages) + .sync-label-count + (the pinned "(n/m)": flex 0 0 auto, NEVER clipped — the + .sync-result mono language, a position reads as data). */ .sync-label { - display: inline-block; + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + min-width: 0; max-width: min(16rem, 40vw); +} +.sync-label-main { + min-width: 0; /* the flex item that shrinks — what engages the ellipsis */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - vertical-align: bottom; +} +.sync-label-count { + flex: 0 0 auto; /* NEVER clipped — the (n/m) position is the point */ + white-space: nowrap; + font-family: var(--mono); /* the .sync-result mono language (position reads as data) */ } /* Last-result announcer — soft ink, small mono */ .sync-result { @@ -5313,14 +5327,15 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und target to discover on a phone). The RAG page-head row wraps so the Sync pill drops below the "Knowledge base" title at full width; the History page-head already wraps the pill below its - title block. The Sync label's min(16rem, 40vw) cap lifts so the - live-file text ellipsizes against the FULL width (min-width: 0 - lets the nowrap flex item shrink, which is what engages the - ellipsis); the refresh glyph joins its visible label. The - aria-labels keep the accessible names. */ + title block. The Sync label's min(16rem, 40vw) cap lifts and the + label FILLS the pill (phase 128: flex 1 1 auto) — .sync-label-main + ellipsizes against the width left by the pinned .sync-label-count, + so the (n/m) stays on-screen at the right edge, never clipped; + the refresh glyph joins its visible label. The aria-labels keep + the accessible names. */ .page-head-row { flex-wrap: wrap; } .sync-btn { width: 100%; } - .sync-label { max-width: none; min-width: 0; } + .sync-label { max-width: none; flex: 1 1 auto; min-width: 0; } .history-refresh { width: 100%; } .history-refresh svg { display: block; } .sync-result { diff --git a/frontend/index.html b/frontend/index.html index f67261b..8486772 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -448,7 +448,7 @@

Knowledge base

diff --git a/tests/e2e/test_sync_label_mobile.py b/tests/e2e/test_sync_label_mobile.py new file mode 100644 index 0000000..03f6fb1 --- /dev/null +++ b/tests/e2e/test_sync_label_mobile.py @@ -0,0 +1,816 @@ +"""Phase 128 task 03 E2E (Playwright, mock-only): the RUNNING sync label +is fully visible at a phone viewport — the file path ellipsizes to fit +and the ``(n/m)`` count span sits in its own non-shrinking part that is +NEVER clipped — in BOTH long spans (import + summaries), while desktop +keeps the pre-phase strings and the ``min(16rem, 40vw)`` cap. + +The dedicated story suite for ``128_sync_label_mobile`` (A16 — one +Playwright file per phase, run in isolation): the owner's report was +that ``Syncing… name/of/file/here (2/3)`` "doesn't show up on mobile" — +pre-phase, ``#sync-label`` was ONE inline-block span (whole label +string, one ellipsis) whose mobile media query LIFTED the +``min(16rem, 40vw)`` cap (``max-width: none``), so the ellipsized tail +— where ``(n/m)`` lives — was clipped away on a phone. Phase 128 splits +the label into two parts (``.sync-label-main`, the shrinkable +prefix + file/folder, + ``.sync-label-count`, the pinned position) so +the count is never the ellipsis's sacrifice. + +Run in isolation (DB must be up: ``podman compose up -d db``): + + uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov + +MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the gate is the +deterministic mock plus the phase-64 ``tests/e2e/slow_llm.py`` delay +proxy that stretches the run past the UI's 2 s status poll, so the +rendered-label assertions are deterministic, not races. + +Timing fixture (the phase-98 sizing rationale, re-sized for this +suite): ``SLOW_DELAY_S = 1.5`` — the import span (4 file embeds +≈ 6 s) and the summaries span (3 folder summaries ≈ 4.5 s) each +outlive the UI's 2 s poll by ≥ 2×, so the page's OWN 2 s poll renders +each running label and holds it in the DOM for at least one full poll +period (any window ≥ the period catches it). The whole changed sync is +≈ 10 LLM calls (2 model probes + 4 file embeds + 1 overview + +3 folder summaries) ≈ 15–18 s — well inside the generous settle +budgets. + +The suite runs ONE module app (the phase-79 full-inventory pattern — +it binds its own port) over the shared E2E Postgres: the SLOW app +(``BOR_LLM_BASE_URL`` = the slow proxy in front of the mock). + +KB fixture — a host temp dir (``tmp_path_factory``; the app runs on +the same host) registered as a ``kind=local`` source (the +``test_local_directory_sources.py`` / phase-94 API-registration +pattern; no git anywhere): ``synclabel/`` with TWO files in ``alpha/`` +and TWO in ``bravo/`` — the summaries phase then has 3 candidate +folders (the source root + the two folders — the recursive-subtree +≥ 1 rule), a non-trivial ``(n/m)``. + +Autouse cleanup (the phase-96 pattern): before each test wait for no +running sync on the app, then truncate the shared registry + KB +tables (documents/chunks/query_log/steering_notes/kb_overview/ +git_sources/folder_summaries) — each test seeds its own source and +runs its own sync; after each test the same truncate. + +Test → observable mapping (Playwright Mapping Rule): +1. ``test_mobile_label_fits_viewport_with_count_in_import`` — admin + page on the sources view at viewport **390×844** (mobile); click + **Sync sources**; while the import phase is running (the page's + own 2 s poll): the rendered ``#sync-label``'s bounding box is + INSIDE the viewport (left ≥ 0, right ≤ 390 — the bug's direct + negation), and the ``.sync-label-count`` part is VISIBLE (non-null + box) and NOT clipped — its right edge sits at or inside the label's + right edge and the button's right edge, with text matching + ``\\(\\d+/\\d+\\)`` once a file is in flight; ``.sync-label-main`` + carries ``Syncing… `` — the FULL source/relative/path + (visually ellipsized, but the DOM text and the button ``title`` + are untruncated, A4). +2. ``test_mobile_label_shows_summaries_count`` — same mobile + viewport; through to the summaries phase: ``.sync-label-main`` + matches ``Summarizing folders…`` (the folder part optional — the + phase's first poll may precede the first hook), the + ``.sync-label-count`` part matches ``\\(\\d+/3\\)`` and stays + visible (same clip pins as test 1); on settle the button leaves + the running label (the phase-32 terminal ``Synced HH:MM``) and the + count span is hidden + empty. +3. ``test_desktop_label_unchanged`` — viewport 1280×800; the same + sync: during import the full rendered label text (main + count + concatenation) matches ``Syncing… + (n/m)`` — the pre-phase byte-identical string — and the label's + width respects the ``min(16rem, 40vw)`` cap (bounding box width + ≤ 256 px + slack); the ``title`` carries the same untruncated + string (A4). +4. ``test_pre_phase_css_reproduces_the_clip`` — the pin check: the + SAME mobile run with the pre-phase-128 ``.sync-label`` rules + re-injected (CSSOM ``insertRule`` into the origin's own linked + stylesheet — the A20 CSP ``default-src 'self'`` blocks inline + ``