phase: 128_sync_label_mobile
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`.
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
# Phase 128 — The sync label shows on mobile, count always visible
|
||||
|
||||
**Source:** `TODO.md` L4 — "The `Syncing… name/of/file/here (2/3)` doesn't show up on mobile, it needs to" (the item is truncated in the TODO; ASSUMPTION A1 — read as: the running label must be fully visible on mobile, with the `(n/m)` position never clipped off-screen).
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** `frontend/index.html` L449–451 — the `#sync-btn` / `#sync-label` markup (one span holds the whole label string); `frontend/assets/sources.js` — `fmtSyncLabel(kind, currentFile, done, total)` (~L650) and `fmtSyncPhaseLabel(status)` (~L669) build the running labels as ONE string, the poll loop applies them to `#sync-label` + the button `title` + `#sync-result` (the aria-live announcer — the untruncated text rides both, A4 convention); `frontend/assets/styles.css` — `.sync-label` (~L2067: `inline-block`, `max-width: min(16rem, 40vw)`, ellipsis, `nowrap`) and the mobile media query (~L5323: `.sync-btn { width: 100%; }`, `.sync-label { max-width: none; min-width: 0; }` — with `max-width: none` the inline-block sizes to its FULL content, so the label's tail — where `(n/m)` lives — runs off-screen: the reported bug); `tests/unit/test_frontend_sync_upload.py` — the pinned frontend label tests to update.
|
||||
|
||||
## Objective
|
||||
On a phone, the running sync label is fully visible: the file path ellipsizes to fit, and the `(n/m)` position sits in its own non-shrinking span that is NEVER clipped — in the import phase (`Syncing… <file> (2/3)`) and the summaries phase (`Summarizing folders… <folder> (n/m)`). Desktop renders the same content with its existing visual layout (the cap, the ellipsis, the title/aria-live untruncated text all preserved).
|
||||
|
||||
## Dependencies
|
||||
- `127_sync_incremental_folder_summaries` (todo) — pipeline order only; code dependencies are the completed phase-64/98 label contract.
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **Markup (task 01):** `#sync-label` gains two child spans — `<span class="sync-label-main"></span><span class="sync-label-count" hidden></span>` (static markup in `index.html`). The JS builders no longer return one string: a single `setSyncLabel(mainText, countText)` helper writes `textContent` into the parts (the createElement/textContent house rule — no HTML strings) and toggles the count span's `hidden` (absent count → hidden, empty). The FULL untruncated label (`mainText + (countText ? " " + countText : "")`) is still set on the button `title` and `#sync-result` exactly as today (A4 — screen readers hear the whole thing).
|
||||
- **Label mapping (task 01):** main/count per state — prelude (null phase, no file): main `"Syncing…"`, no count; import: main `"Syncing… <file>"`, count `"(n/m)"` while `total > 0`; overview: main `"Writing KB overview…"`, no count; summaries: main `"Summarizing folders… <folder>"` (no folder part while `current_summary` is null), count `"(n/m)"`; upload: main `"Importing"`, no count (phase-90 contract — the upload status carries no file or counts); idle/settle labels (`"Sync sources"`, `"Synced HH:MM"`): main only. `fmtSyncPhaseLabel`'s decision tree is preserved 1:1 — only the OUTPUT shape changes (string → parts).
|
||||
- **CSS (task 02):** `.sync-label` becomes a flex row (`display: inline-flex; align-items: baseline; gap: 0.35rem; min-width: 0; max-width: min(16rem, 40vw);`); `.sync-label-main { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }`; `.sync-label-count { flex: 0 0 auto; white-space: nowrap; }`. Mobile media query (the existing block at ~L5323): `.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }` — the label fills the full-width pill, the main part ellipsizes against the remaining width, the count span stays visible at the right edge. The `#sync-result` mobile clip (visually-hidden) is untouched.
|
||||
|
||||
## Tasks
|
||||
1. `01_label_parts.md` — markup + `setSyncLabel` part builders in `sources.js` (all label states, A4 untruncated text preserved).
|
||||
2. `02_mobile_css.md` — the flex-part CSS + the mobile media query.
|
||||
3. `03_e2e_mobile_label.md` — dedicated Playwright E2E, run in isolation.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_frontend_sync_upload.py` — the pinned label outputs updated to the part shape (main/count per state, the byte-identical label strings where they must hold: prelude, upload, settle); the `title`/`#sync-result` untruncated text pins stay.
|
||||
- E2E: one dedicated Playwright file, run in isolation, mobile viewport, slow-LLM pattern.
|
||||
- Coverage: **>90%** on new/modified code (frontend: the JS is covered by the pinned unit tests + the E2E; `app/` untouched this phase).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] at a phone viewport (390×844) the running label is fully in the viewport — file path ellipsized, `(n/m)` visible — in BOTH the import and summaries phases
|
||||
- [ ] desktop label visually unchanged (same strings, same cap/ellipsis, same `title`/`#sync-result` untruncated text)
|
||||
- [ ] `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov` green in isolation
|
||||
- [ ] `uv run pytest` (unit + integration) green; no behavior change in completed phases
|
||||
@@ -1,29 +0,0 @@
|
||||
# Task 01 — `#sync-label` becomes parts: the file path ellipsizes, the `(n/m)` count is pinned
|
||||
|
||||
**Phase:** `128_sync_label_mobile` · **Source:** `TODO.md:4` — "The `Syncing… name/of/file/here (2/3)` doesn't show up on mobile, it needs to" (A1: fully visible on mobile, count never clipped)
|
||||
|
||||
## Objective
|
||||
The label's string is split into a shrinks-to-fit main part (prefix + file/folder path) and a pinned count part (`(n/m)`), so CSS can ellipsize the path while the position always stays on-screen. Every existing label state keeps its exact rendered text.
|
||||
|
||||
## Work
|
||||
1. `frontend/index.html` (L449–451) — inside `#sync-label`, replace the bare text with the two part spans (the initial text lives in the main span so the pre-JS idle label renders):
|
||||
```html
|
||||
<span class="sync-label" id="sync-label"><span class="sync-label-main">Sync sources</span><span class="sync-label-count" hidden></span></span>
|
||||
```
|
||||
2. `frontend/assets/sources.js` — one new helper where `fmtSyncLabel`/`fmtSyncPhaseLabel` live (~L650): `setSyncLabel(mainText, countText = "")` — sets `syncLabelMain.textContent = mainText`; if `countText`: `syncLabelCount.textContent = countText`, `syncLabelCount.hidden = false`; else `syncLabelCount.textContent = ""`, `hidden = true`; then sets the UNTRUNCATED full label — `mainText + (countText ? " " + countText : "")` — on `syncBtn.title` and `syncResult.textContent` (the A4 convention: title + aria-live always carry the whole thing). Get the part elements once at `mount` scope (the `querySelector` house pattern; null-guard like `syncLabel`).
|
||||
- `fmtSyncLabel(kind, currentFile, done, total)` → return the PARTS: `main = currentFile ? `${prefix} ${currentFile}` : prefix` (prefix `"Syncing…"` / `"Importing"` as today), `count = total > 0 ? `(${done}/${total})` : ""`; call `setSyncLabel(main, count)`. The byte-identical full strings hold: `"Syncing…"` (no file), `"Syncing… <file>"` (no total), `"Syncing… <file> (n/m)"`, `"Importing"`.
|
||||
- `fmtSyncPhaseLabel(status)` → same decision tree (overview / summaries / fall-through), emitting parts: overview → `("Writing KB overview…", "")`; summaries → `(`Summarizing folders…${folder}`, `(${status.summaries_done}/${status.summaries_total})`)` with the folder part omitted while `current_summary` is null (as today) — the count is ALWAYS emitted in this phase (the hook always carries a total); fall-through → the `fmtSyncLabel` parts. The full-string shape is byte-identical to the pre-task strings.
|
||||
- Every OTHER call site that writes `#sync-label` (idle `"Sync sources"`, settle `"Synced HH:MM"`, the 202/409 click fallback, the re-attach `initSyncButton` path) goes through `setSyncLabel(main)` — count hidden.
|
||||
- The upload job keeps the bare `"Importing"` (no file, no counts — phase-90 contract).
|
||||
3. `tests/unit/test_frontend_sync_upload.py` — update the pinned label tests to the part shape: pin the main/count pair per state (prelude, import with/without total, overview, summaries with/without folder, upload, idle, settle) AND the `title` / `#sync-result` untruncated concatenation (the A4 pins).
|
||||
|
||||
ASSUMPTION (A1 restated): the truncated TODO item is satisfied by "label fully visible on mobile + count never clipped" — no wrapping, no progress bar.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: the updated pins above green; no HTML strings anywhere in the label path (textContent only).
|
||||
- Coverage: **>90%** on this task's new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] every running/idle/settle label renders the SAME full string as pre-task on desktop (the part concatenation is byte-identical)
|
||||
- [ ] the count span is `hidden` whenever there is no `(n/m)` to show
|
||||
- [ ] `uv run pytest tests/unit -q` green; no behavior change in completed work
|
||||
@@ -1,46 +0,0 @@
|
||||
# Task 02 — CSS: the label parts flex — path ellipsizes, count pinned, mobile fully visible
|
||||
|
||||
**Phase:** `128_sync_label_mobile` · **Source:** `TODO.md:4` — "The `Syncing… name/of/file/here (2/3)` doesn't show up on mobile, it needs to" (A1: fully visible on mobile, count never clipped)
|
||||
|
||||
## Objective
|
||||
The label's two parts are laid out so the path can ellipsize against whatever width is available — and on a phone, where the pill is full-width, the path ellipsizes and the `(n/m)` count sits pinned at the right edge, always on-screen.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/styles.css` — replace the `.sync-label` rule (~L2067, the phase-64 comment block above it stays, updated to name the parts):
|
||||
```css
|
||||
.sync-label {
|
||||
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;
|
||||
}
|
||||
.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) */
|
||||
}
|
||||
```
|
||||
(The old `display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;` moves to `.sync-label-main` — `vertical-align` is gone: the flex row aligns it.)
|
||||
2. `frontend/assets/styles.css` — the mobile media query block (~L5323, the "full-width LABELED pills" comment): update the two sync rules:
|
||||
```css
|
||||
.sync-btn { width: 100%; }
|
||||
.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }
|
||||
```
|
||||
— the label now fills the full-width pill; `.sync-label-main` (min-width: 0 inside a flex row that can shrink) ellipsizes against the width left by the pinned count; the count stays visible at the right edge. Do NOT touch the `#sync-result` visually-hidden clip, the `#sync-btn` aria-label, or the other mobile rules in that block.
|
||||
3. Audit: grep for every `.sync-label` reference in `styles.css` — the only rules are the base + this mobile block (the phase-64/98 history is in comments). Confirm no other stylesheet (theme override, page-scoped) styles the label; the built-in theme must stay byte-identical in the no-row case (B4 — no `ui_settings` dependency here, this is static CSS).
|
||||
|
||||
## Testing & Quality
|
||||
- The visual gate is the E2E (task 03) — desktop vs mobile bounding-box + text assertions; no new Python surface.
|
||||
- Coverage: **>90%** on new/modified code (CSS has no direct coverage — the E2E + unit pins carry it).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] desktop: long paths still ellipsize inside the `min(16rem, 40vw)` cap, count visible after the ellipsis
|
||||
- [ ] mobile (390 px): label fills the pill, path ellipsized, `(n/m)` on-screen at the right edge (the E2E asserts bounding boxes)
|
||||
- [ ] `uv run pytest` green; no behavior change in completed phases
|
||||
@@ -1,24 +0,0 @@
|
||||
# Task 03 — E2E: the running sync label is fully visible with its count on mobile (and unchanged on desktop)
|
||||
|
||||
**Phase:** `128_sync_label_mobile` · **Source:** `TODO.md:4` — "The `Syncing… name/of/file/here (2/3)` doesn't show up on mobile, it needs to" (A1: fully visible on mobile, count never clipped)
|
||||
|
||||
## Objective
|
||||
A dedicated, mock-only Playwright suite (A16 — one per phase, run in isolation) proving the label's geometry: at a phone viewport the rendered `#sync-label` fits the viewport with the `(n/m)` count span visible in BOTH long spans (import + summaries); at a desktop viewport the label keeps its pre-phase strings and cap.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_sync_label_mobile.py` (new) — follow `tests/e2e/test_sync_summary_visibility.py`'s house pattern (module app fixtures over the shared E2E Postgres, host temp-dir `kind=local` source, autouse truncate cleanup, mock-only). Use the SLOW app (the `tests/e2e/slow_llm.py` proxy, `SLOW_DELAY_S = 1.5` as there) so each phase span outlives the UI's 2 s poll — the rendered-label assertions are deterministic, not races.
|
||||
KB fixture — one local source `synclabel/` with TWO files in `alpha/` and TWO in `bravo/` (the summaries phase then has 3 candidate folders: root + 2 — a non-trivial `(n/m)`).
|
||||
2. Test → observable mapping (Playwright Mapping Rule):
|
||||
- `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; `.sync-label-count` is visible (`boundingBox` non-null, not clipped: its right edge ≤ the button's right edge) with text matching `\(\d+/\d+\)` once a file is in flight; `.sync-label-main` carries `Syncing… <file>` (the file part may be visually ellipsized — assert the button `title` carries the UNTRUNCATED label, A4).
|
||||
- `test_mobile_label_shows_summaries_count` — same mobile viewport; through to the summaries phase: `.sync-label-main` matches `Summarizing folders…` (folder part optional), `.sync-label-count` matches `\(\d+/3\)` and stays visible; settle: the button leaves the running label (`Synced HH:MM` or the phase-32 terminal shape), the count span hidden/empty.
|
||||
- `test_desktop_label_unchanged` — viewport 1280×800; same sync: during import the FULL rendered label text (main + count concatenation) matches `Syncing… <full source/relative/path> (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); `title` carries the same untruncated string.
|
||||
3. Header docstring: the run-in-isolation command (`uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`), the mock-only + slow-proxy rationale (re-sized from the phase-98 suite's comment), the fixture layout, the test→observable table.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: this file IS the task's suite — run in isolation, green.
|
||||
- Coverage: the phase's >90% bar is carried by task 01's updated unit pins (frontend JS); this task adds the in-isolation Playwright gate.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov` green in isolation (DB up: `podman compose up -d db`)
|
||||
- [ ] the mobile tests fail against the pre-phase markup/CSS (the count ran off-screen) — i.e. the suite actually pins the fix
|
||||
- [ ] no behavior change in completed phases (label strings, phase machine, aria-live)
|
||||
Reference in New Issue
Block a user