Files
brain-of-reese/.agents/phases/todo/154_sync_status_freshness/00_phase.md
T
ducoterra 966c4b0540
Build and Push Containers / build-and-push-app (push) Successful in 2m45s
Build and Push Containers / build-and-push-db (push) Successful in 19s
phase: 153_document_viewer_context
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`.
2026-10-04 09:23:54 -04:00

7.4 KiB
Raw Blame History

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 theHH: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)