Files
brain-of-reese/.agents/phases/complete/154_sync_status_freshness/00_phase.md
T
ducoterra c275735db4
Build and Push Containers / build-and-push-app (push) Successful in 2m26s
Build and Push Containers / build-and-push-db (push) Successful in 16s
phase: 154_sync_status_freshness
All 4 task files exist in `complete/`, and only `00_phase.md` remains in `todo/`. This is the final verification pass. I'll read through the task files and check the current state of the code.
2026-10-04 14:24:33 -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)