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.
This commit is contained in:
@@ -75,8 +75,12 @@
|
||||
* "Processing…" label for the WHOLE run (phase 90, A2 — the run
|
||||
* is unpack + register only: no file, no "(n/m)" counts, no
|
||||
* title) and settles it: success → the ready-for-sync line
|
||||
* ("Uploaded <name> — press Sync sources to import it.",
|
||||
* fmtUploadResult off the status's {"message": "uploaded"}
|
||||
* ("Uploaded <name> (<stamp>) — press Sync sources to import
|
||||
* it." — the phase-154 completion stamp, fmtUploadStamp off the
|
||||
* status's finished_at, the A1 date-qualified form cited in
|
||||
* sources.js's fmtSyncTime, not restated; a running / partial
|
||||
* payload without timestamps renders the byte-identical pre-phase
|
||||
* line — fmtUploadResult off the status's {"message": "uploaded"}
|
||||
* detail — the role=status result line) + the "Archive uploaded
|
||||
* — press Sync sources to import it." announce + loadSources
|
||||
* (the new/updated row lands with the Local badge; a re-upload
|
||||
@@ -93,8 +97,10 @@
|
||||
* (a reload mid-run re-attaches — no second upload), a terminal
|
||||
* run re-renders its result line / error banner (the safe name
|
||||
* was page-local — lastUploadName is null after a reload — so the
|
||||
* re-rendered line is the nameless "Uploaded — press Sync sources
|
||||
* to import it.").
|
||||
* re-rendered line is the nameless "Uploaded <stamp> — press
|
||||
* Sync sources to import it." — the stamp comes from the SERVER
|
||||
* payload, so the line is self-dating even with the name
|
||||
* unknown, phase 154).
|
||||
* • remove — a row's Remove button opens the page-local
|
||||
* confirmation modal (#remove-confirm-dialog, a real
|
||||
* role="alertdialog" — the native confirm() retired, phase 69):
|
||||
@@ -1096,7 +1102,10 @@ export async function mount(root) {
|
||||
* "Processing…" label for the WHOLE run (phase 90, A2 — the unpack
|
||||
* has no file-level progress: no file, no "(n/m)" counts, no title)
|
||||
* and settles it: success → the ready-for-sync line ("Uploaded
|
||||
* <name> — press Sync sources to import it.", fmtUploadResult off
|
||||
* <name> (<stamp>) — press Sync sources to import it." — the
|
||||
* phase-154 completion stamp, fmtUploadStamp off the status's
|
||||
* finished_at; the A1 date-qualified form is cited in
|
||||
* fmtUploadResult's comment, not restated — fmtUploadResult off
|
||||
* the status's {"message": "uploaded"} detail) in the role=status
|
||||
* result line + the "Archive uploaded — press Sync sources to
|
||||
* import it." announce + loadSources (NO second toast — it already
|
||||
@@ -1117,23 +1126,75 @@ export async function mount(root) {
|
||||
* (The phase-49 synchronous 200 paragraph is superseded by phase 64;
|
||||
* the phase-64 scan counts are superseded by phase 90.) */
|
||||
|
||||
/* The result line's text (phase 90, A2 — the no-count contract): the
|
||||
/* The result line's text (phase 90, A2 — the no-count contract,
|
||||
extended by phase 154 task 02 with the completion stamp): the
|
||||
status success detail is exactly {"message": "uploaded"} — the
|
||||
sync-style counts the phase-64 line rendered are gone (the scan —
|
||||
and its counts — belong to the Sync button, which renders them on
|
||||
the RAG page). `name` is the accepted 202's safe source name
|
||||
(lastUploadName) when the run started on this page; it is null
|
||||
after a reload or on the 409 re-attach (the line still points at
|
||||
the next step, only without the name). */
|
||||
function fmtUploadResult(detail, name) {
|
||||
the next step, only without the name). `when` is the PRECOMPUTED
|
||||
completion stamp (fmtUploadStamp(status) — the phase-154 A1
|
||||
date-qualified form, the rule cited in sources.js's fmtSyncTime,
|
||||
not restated); "" (a running / legacy / partial payload without
|
||||
timestamps) renders the byte-identical pre-phase-154 strings. */
|
||||
function fmtUploadResult(detail, name, when) {
|
||||
if (detail && detail.message === "uploaded") {
|
||||
return name
|
||||
? `Uploaded ${name} — press Sync sources to import it.`
|
||||
if (name) {
|
||||
return when
|
||||
? `Uploaded ${name} (${when}) — press Sync sources to import it.`
|
||||
: `Uploaded ${name} — press Sync sources to import it.`;
|
||||
}
|
||||
return when
|
||||
? `Uploaded ${when} — press Sync sources to import it.`
|
||||
: "Uploaded — press Sync sources to import it.";
|
||||
}
|
||||
return "The upload finished.";
|
||||
}
|
||||
|
||||
/* Phase 154 (task 02): the upload line's completion stamp —
|
||||
* PRECOMPUTED from the /upload/status payload (the per-page
|
||||
* duplication idiom: the SAME date-qualifier as sources.js's
|
||||
* fmtSyncTime, duplicated, not shared — the A1 rule — same local
|
||||
* day `HH:MM`, same year `M/D, h:MM AM/PM`, across years
|
||||
* `M/D/YYYY, h:MM AM/PM` — is cited there, not restated). Reads
|
||||
* status.finished_at first (the success state always has it on the
|
||||
* server), falls back to status.started_at (the defensive guard
|
||||
* for a legacy / partial payload — a corrupt finished_at degrades
|
||||
* to the started_at stamp rather than to no stamp), and yields
|
||||
* "" when both are missing/absent or every ISO is unparseable
|
||||
* (the pre-phase-154 byte-identical fallback — the line renders
|
||||
* the pre-stamp copy). */
|
||||
function fmtUploadStamp(status) {
|
||||
const s = status || {};
|
||||
let d = null;
|
||||
for (const iso of [s.finished_at, s.started_at]) {
|
||||
if (!iso) continue;
|
||||
const t = new Date(iso);
|
||||
if (!Number.isNaN(t.getTime())) {
|
||||
d = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (d === null) return "";
|
||||
const now = new Date();
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
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}`;
|
||||
}
|
||||
|
||||
/* Upload-success toast (phase 64 task 05, A2 — owner-locked): the
|
||||
* "successfully uploaded" confirmation, the phase-55 share-toast
|
||||
* pattern (frontend/assets/app.js) made page-local. A SINGLE node —
|
||||
@@ -1237,11 +1298,16 @@ export async function mount(root) {
|
||||
if (status.state === "success") {
|
||||
// The run finished (unpack + register only — phase 90): the
|
||||
// ready-for-sync line (fmtUploadResult reads the no-count
|
||||
// detail), the announcement, the row lands. NO toast here — it
|
||||
// detail + fmtUploadStamp's completion stamp — phase 154),
|
||||
// the announcement, the row lands. NO toast here — it
|
||||
// already fired at the 202 (A2).
|
||||
const detail = status.detail || {};
|
||||
if (uploadResult) {
|
||||
uploadResult.textContent = fmtUploadResult(detail, lastUploadName);
|
||||
uploadResult.textContent = fmtUploadResult(
|
||||
detail,
|
||||
lastUploadName,
|
||||
fmtUploadStamp(status)
|
||||
);
|
||||
uploadResult.hidden = false;
|
||||
}
|
||||
announce("Archive uploaded — press Sync sources to import it.");
|
||||
@@ -1276,10 +1342,11 @@ export async function mount(root) {
|
||||
* no second upload, no error); a finished run re-renders its result
|
||||
* line ONLY (no announce, no toast — the toast fired at the 202, A2;
|
||||
* the name is unknown after a reload — lastUploadName is null — so
|
||||
* the line is the nameless "Uploaded — press Sync sources to import
|
||||
* it.", phase 90); a failed run re-renders its error banner; idle
|
||||
* does nothing (and a blip is a no-op — the page boots honest
|
||||
* either way). */
|
||||
* the line is the nameless "Uploaded <stamp> — press Sync sources
|
||||
* to import it." — the stamp from the SERVER payload, so the line
|
||||
* is self-dating even with the name unknown, phase 154; phase 90);
|
||||
* a failed run re-renders its error banner; idle does nothing (and a
|
||||
* blip is a no-op — the page boots honest either way). */
|
||||
async function initUploadStatus() {
|
||||
if (!uploadBtn) return;
|
||||
let status;
|
||||
@@ -1299,9 +1366,15 @@ export async function mount(root) {
|
||||
if (status.state === "success") {
|
||||
// The last run's result line only — no announce, no toast (A2).
|
||||
// The safe name was page-local (lastUploadName is null after a
|
||||
// reload) — the line still points at the next step (phase 90).
|
||||
// reload) — the line still points at the next step (phase 90),
|
||||
// self-dating through fmtUploadStamp's completion stamp
|
||||
// (phase 154 — the stamp comes from the server payload).
|
||||
if (uploadResult) {
|
||||
uploadResult.textContent = fmtUploadResult(status.detail, lastUploadName);
|
||||
uploadResult.textContent = fmtUploadResult(
|
||||
status.detail,
|
||||
lastUploadName,
|
||||
fmtUploadStamp(status)
|
||||
);
|
||||
uploadResult.hidden = false;
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user