phase: 154_sync_status_freshness
Build and Push Containers / build-and-push-app (push) Successful in 2m26s
Build and Push Containers / build-and-push-db (push) Successful in 16s

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.
This commit is contained in:
2026-10-04 14:24:33 -04:00
parent 966c4b0540
commit c275735db4
36 changed files with 2526 additions and 99 deletions
+49 -7
View File
@@ -321,7 +321,9 @@
* title + #sync-progress (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,
* settle labels ("Sync sources", "Synced <stamp>" — the date-
* qualified stamp from fmtSyncTime, phase 154: `HH:MM` on the same
* local day, the dated form otherwise), 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… <file>",
@@ -385,7 +387,8 @@
* every settle). The `is-running`
* class travels with the spin: added in enterSyncRunningState, removed
* in settleSyncButton (every terminal + both idles pass through it).
* The settle rendering ("Synced HH:MM" + the counts line), the Cancel
* The settle rendering ("Synced <stamp>" — the date-qualified stamp,
* phase 154 — + the counts line), the Cancel
* control, the 409 double-trigger, the error banner/modal, and the
* sync status API are all UNCHANGED.
*
@@ -404,7 +407,8 @@
* writes
* the SAME untruncated full label through setSyncProgress, ONLY
* while running (the idle / settle button labels — "Sync sources",
* "Synced HH:MM" — must not fill the panel; they stay on the
* "Synced <stamp>" (the date-qualified stamp, phase 154) — must
* not fill the panel; they stay on the
* button, A2);
* • result mode — the settle paths write the result line through
* setSyncProgress: the counts line (fmtSyncResult — "N added ·
@@ -811,11 +815,40 @@ export async function mount(root) {
}
}
/* Phase 154 (task 01): the contract widens to the DATE-QUALIFIER
* (the phase design's A1 rule — owner-locked; the stamp is
* computed from the ISO's LOCAL calendar date, the page's clock —
* the table `Updated` columns already render local dates):
* • same local calendar day as now → `HH:MM` (byte-identical to
* the pre-phase output — a same-day badge is unchanged);
* • same local year, different day → `M/D, h:MM AM/PM`
* (e.g. `10/2, 3:14 PM`);
* • different local year → `M/D/YYYY, h:MM AM/PM`
* (e.g. `1/3/2025, 9:12 PM`).
* `h:MM` is the 12-hour form WITHOUT a leading zero on the hour
* (the `12:05 AM` edge), month/day without leading zero (the table
* dates' `M/D/YYYY` / `5:34:06 PM` family, the seconds dropped).
* The parse guard is UNCHANGED — an unparseable/missing ISO still
* yields "" (the bare "Synced" settle — e.g. the app-restart case,
* A12). */
function fmtSyncTime(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
if (
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate()
)
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const h12 = d.getHours() % 12 || 12;
const ampm = d.getHours() < 12 ? "AM" : "PM";
const date =
d.getFullYear() === now.getFullYear()
? `${d.getMonth() + 1}/${d.getDate()}`
: `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
return `${date}, ${h12}:${pad(d.getMinutes())} ${ampm}`;
}
/* Phase 128 (task 01) + Phase 129 (task 04) + Phase 152 (task 03):
@@ -861,7 +894,14 @@ export async function mount(root) {
* live label (the title already carries it for hover); every
* non-running call restores the ship value "Sync sources" (the
* static HTML attribute is the idle truth — the JS only
* overwrites/restores it, never removes it). */
* overwrites/restores it, never removes it).
* Phase 154 (task 01): the settle badge among these label parts is
* date-qualified — `Synced <stamp>`, where `<stamp>` is produced
* by fmtSyncTime (its contract widened to the phase-154 A1 rule —
* same-day stays `HH:MM`, non-same-day carries the date; the rule
* is cited there, not restated). The parts themselves are
* UNCHANGED — the longer stamp is just more text in the same part
* (the label part ellipsizes as before). */
function setSyncLabel(mainText, countText = "", chunkText = "", running = false, imageText = "") {
if (syncLabelMain) syncLabelMain.textContent = mainText;
if (syncLabelCount) {
@@ -1106,7 +1146,8 @@ export async function mount(root) {
// the running state.
syncBtn.classList.remove("is-running");
// Phase 128 (task 01): the idle / settle labels ("Sync sources",
// "Synced HH:MM") go through the parts writer — count hidden
// "Synced <stamp>" — the date-qualified stamp, phase 154) go
// through the parts writer — count hidden
// (absent), the label's full string rides the title (A4, as with
// every label state). Phase 150 (task 02): the default
// `running = false` leaves the detail box alone in every
@@ -1196,7 +1237,8 @@ export async function mount(root) {
settleSyncButton(time ? `Synced ${time}` : "Synced");
// Phase 150 (task 02): the settle's counts line IS the box's
// result line (the sole live region) — the string builder is
// unchanged (A2: the "Synced HH:MM" label stays on the button).
// unchanged (A2: the "Synced <stamp>" label stays on the button;
// the stamp is date-qualified, phase 154).
setSyncProgress(fmtSyncResult(status.detail));
hideSyncError();
emitSyncStatus(status);