phase: 153_document_viewer_context
Build and Push Containers / build-and-push-app (push) Successful in 2m45s
Build and Push Containers / build-and-push-db (push) Successful in 19s

That last `--co` (collect-only) run was my probe mistake — it shows 34% because no tests execute under collect-only. The valid coverage run was the real gate command executed earlier: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (exit 0, all 3544 tests executed). All gates are verified.

## Phase 153 — final verification pass report

**Verified (all tasks 01–04 complete in `complete/`):**
- Modal `fullPageUrl(source, path, back = null)` + `openDocumentModal(…, back = null)` with module-level `currentBack`; 5 chat call sites pass `"/"`, sources.js call site unchanged (byte-identical Sources path); `documentUrl` comment now true
- Copy-path control (shared `renderDocument` core, icon+text, text = accessible name) on both surfaces; per-page clipboard helpers with the fallback-field + status-line contract; `#doc-copy-announcer` live region; styles + 44px touch floor
- Unit pins: `&back=%2F` / no-`back` byte-identity, `currentBack` single-writer guard, copy strings byte-pinned, phase-13 regression pins untouched

**Test / lint / coverage results:**
- `uv run pytest` → **3544 passed**, 0 failed (full unit + integration)
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% gate)
- `uv run pytest tests/e2e/test_document_viewer_context.py -v --no-cov` → **8 passed** in 47.33s, in isolation (DB up, mock LLM)
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)

**Completion criteria:** all met — (1) chat→modal→Full page: `href="/"` + "Chat"; Sources→modal→Full page: `href="/sources.html"` + "Sources" with `back=` asserted absent; (2) no-`back` URLs byte-identical to pre-phase; (3) Copy path: "Path copied." on success (clipboard readback), prefilled select-on-focus fallback field + "Path ready — copy it from the field." on rejected clipboard, both surfaces parameterized; (4) copied string exactly `docs/homelab/kubernetes.md`; (5) full suite green, E2E isolated green, lint clean, coverage 99%; (6) commit by harness.

**Documentation:** README.md reviewed in full — phase-153 feature already named in the Chat section; routes, env reference, workflows, deployment all consistent — no changes needed. AGENTS.md reviewed in full — rules, gates, and quick-reference commands verified against the repo (incl. `test_smoke.py`, `.env.example`, gitignore entries) — consistent, no changes needed.

**Notable:** pytest 9.1.1 quirk — a doubled `-q` (pyproject `addopts` + explicit `-q`) swallows the final "N passed" summary line while still exiting 0; the plain `uv run pytest` gate command prints it. No defects found in prior work.

**Next pending phase:** `154_sync_status_freshness`.
This commit is contained in:
2026-10-04 09:23:54 -04:00
parent 12fa69b7ea
commit 966c4b0540
59 changed files with 4304 additions and 34 deletions
@@ -0,0 +1,44 @@
# Phase 154 — sync + upload stamps say WHEN (date-qualified freshness)
**Source:** owner UX audit 2026-10-03 (live interactive-browser pass): "RAG 'Synced 16:06' badge is time-only — ambiguous the next day" + "Sources 'Uploaded — press Sync sources to import it.' has no timestamp — it's server-persisted and re-rendered on every mount; after that archive is imported by a sync, the line can sit there indefinitely and mislead."
**Story:** n/a (audit-derived phase)
**Context:** `sources.js::fmtSyncTime(iso)` (L814–819) — `HH:MM` only; its consumer `applySyncSuccess` (L1195–1196) settles the RAG sync button as `Synced ${time}` (or bare "Synced" when the ISO is unparseable — the app-restart case: the sync status is in-memory, A12). The label parts writer (L821 onward) carries the same text in the button `title` + the running `aria-label` — the qualified stamp rides all three (the phase-128 parts-writer idiom: write once, three surfaces). `git-sources.js::fmtUploadResult(detail, name)` (L1128–1133) — the no-count contract (phase 90): `Uploaded <name> — press Sync sources to import it.` (fresh 202 settle, the page-local `lastUploadName`) or the nameless `Uploaded — press Sync sources to import it.` (the boot re-render, `initUploadStatus` L1279–1304, where the name is unknown after a reload). `GET /api/git-sources/upload/status` (git_sources.py L704–729) ALREADY exposes `started_at` / `finished_at` (ISO) + `detail` — the server change is ZERO; this phase is frontend-only. The per-page duplication idiom is established (history.js L301's copyText, sources.js vs app.js) — the date-qualifier helper is duplicated into the two modules, not shared.
## Objective
Both freshness stamps tell the owner *when*: a same-day stamp stays `HH:MM` (byte-identical to today's behavior); a non-same-day stamp carries the date (`M/D, h:MM AM/PM` — the table-date family); the Sources upload line carries its completion stamp on every render path (fresh settle AND boot re-render), so a stale "Uploaded" line is self-dating.
## Dependencies
- `150_drop_sync_result` (complete) — the settled sync button is the sole "Synced" surface (the phase-149 box is the running label; the button settles it).
- `64_sync_upload_progress` (complete) — the upload 202/poll/re-attach state machine this phase's line change rides on.
- `90_upload_no_scan` (complete) — the no-count result-line contract (phase 90, A2) this phase EXTENDS (a stamp is added; no counts return).
## Design (shared by all tasks — the executor reads this, not the chat)
- **The date-qualifier rule (ASSUMPTION A1, owner-locked):** the stamp is computed from the ISO's LOCAL date (the page's clock — the table dates already use local time). Same local year as now AND same calendar day → `HH:MM` (byte-identical to the current `fmtSyncTime` output — a same-day badge is unchanged). Otherwise, same year → `M/D, h:MM AM/PM` (e.g. `10/2, 3:14 PM`); different year → `M/D/YYYY, h:MM AM/PM` (e.g. `1/3/2025, 9:12 PM`). An unparseable/missing ISO → the existing fallback (sync: bare "Synced"; upload: no stamp — the pre-phase strings). `h:MM` uses the locale 12-hour form WITHOUT leading zero on the hour (matching the table dates' `5:34:06 PM` family — drop the seconds), month/day without leading zero (the tables' `M/D/YYYY` shape).
- **RAG sync badge (task 01):** `fmtSyncTime` becomes the date-qualifier (keep the name — its contract widens; or add `fmtStamp` and delegate — the executor's choice, the pins fix the output). `applySyncSuccess` is unchanged in shape (`settleSyncButton(time ? \`Synced ${time}\` : "Synced")` — the qualifier lives inside the formatter). The parts writer's comment block (L821 onward) names the qualified stamp in the label parts where the `HH:MM` contract is cited (cite, don't restate).
- **Sources upload line (task 02):** `fmtUploadResult(detail, name, when)` — `when` is the precomputed stamp string (the caller computes it from `status.finished_at`, falling back to `status.started_at`; the success state always has a `finished_at` on the server, the fallback is the defensive guard for a legacy/partial payload). Shapes (the phase-90 contract extended, name present/absent): `Uploaded <name> (<stamp>) — press Sync sources to import it.` / `Uploaded <stamp> — press Sync sources to import it.`; `when` empty (running state, legacy payload) → BYTE-IDENTICAL to the current strings (the no-stamp fallback). Call sites: the poll settle (L1239–1244, the page-local `lastUploadName`) and `initUploadStatus`'s success branch (L1279–1304, nameless) — both read `status.finished_at` from the same `/upload/status` payload. The L78–96 and L1125–1133 comment blocks name the stamp in the contract (cite, don't restate).
- **A11y:** the stamps are text in existing `role="status"` / button-label surfaces — no new nodes, no color-alone state (B5).
## Tasks
1. `01_rag_sync_stamp.md` — the date-qualifier in `sources.js` (`fmtSyncTime`'s contract widens; same-day byte-identical).
2. `02_upload_line_stamp.md` — the `when` param on `fmtUploadResult` + both call sites reading `finished_at`/`started_at`.
3. `03_unit_pins.md` — the qualifier's boundary pins + both stamp shapes (with/without name, with/without stamp).
4. `04_e2e_status_freshness.md` — the dedicated Playwright suite.
## Testing & Quality
- Unit: source-level pins — the qualifier (same-day `HH:MM` byte-identical; yesterday `M/D, h:MM AM/PM`; last-year `M/D/YYYY, h:MM AM/PM`; garbage/missing ISO → the pre-phase fallbacks — a fixed clock in the test, not wall-clock math on `new Date()`); `fmtUploadResult`'s four shapes (name×stamp, name×no-stamp byte-identical, no-name×stamp, no-name×no-stamp byte-identical); the `applySyncSuccess` settle string for a non-same-day `finished_at`.
- Integration: ZERO server change — the `/upload/status` payload is untouched (regression guard: the full-dict pin, if one exists, stays byte-identical).
- Coverage: **>90%** on the modified JS (source-level pins + E2E).
- The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the project, not just this phase's).
- E2E: `uv run pytest tests/e2e/test_sync_status_freshness.py -v --no-cov` in isolation (DB up: `podman compose up -d db`).
## Documentation & Consistency
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands after this phase — fixing every inconsistency found, **including pre-existing ones not caused by the phase**.
- Tasks update `README.md`/`AGENTS.md` immediately when their own changes are user-visible (the stamp format is a visible UI change — name it where the sync/upload surfaces are described).
## Completion Criteria
- [ ] a same-day sync settles as `Synced HH:MM` — byte-identical to pre-phase (the pin + the E2E)
- [ ] a non-same-day `finished_at` (the unit pins; the E2E asserts the same-day shape on a real mock sync — the date branch is unit-pinned with a fixed clock, the E2E never depends on the wall clock)
- [ ] the upload line carries the stamp on BOTH render paths (the fresh 202 settle with the name; the boot re-render without it) — the E2E seeds a prior upload via the API and reloads the page
- [ ] a running/legacy upload payload renders the pre-phase strings byte-identically (no partial-state regression)
- [ ] `uv run pytest` green; the dedicated E2E green in isolation; `uv run ruff check . && uv run pyright` clean; `app/` coverage >90%
- [ ] one atomic commit per AGENTS.md rule 8 (`--no-gpg-sign`)