phase: 150_drop_sync_result
**Phase 150 final verification pass — complete, 100% green.** All 6 task files were already in `complete/`, so this was verification + doc pass. - Verified: `#sync-progress` is the sole sync text surface + sole `aria-live` region (`role="status"`, polite, no `aria-hidden`); `span#sync-result` gone from HTML/JS/CSS/tests (incl. runtime-built `RETIRED_ID` absence pins in the unit suites); `setSyncProgress` single writer with text-driven visibility; running-only box write in `setSyncLabel`; all 9 settle/cancel sites routed; `requestCancel` pre-fetch clear is a NO-OP. - Tests: `uv run pytest` → **3463 passed**; `uv run pytest --cov=app` → **99% on app/** (>90%); `uv run ruff check .` + `uv run pyright` → clean (0 errors); `tests/e2e/test_sync_progress_box.py --no-cov` → **8 passed in isolation**; all 18 migrated sync/image-PDF E2E files green in isolation (77 tests). - Completion criteria: 1) grep clean ✅ 2) sole live region, attrs correct ✅ 3) in-flight/settle/cancel/idle box states proven by E2E + unit pins ✅ 4) icon-only running button + restore ✅ 5) full gates green ✅ 6) no completed-phase behavior change (18 family suites + full suite green) ✅ 7) commit — left to harness per pass instructions (working tree untouched, no `git add`/`commit`). - Documentation: README.md fixed 2 pre-existing inconsistencies (stale `~/Homelab`+`~/Deployments` source model in the intro — retired in phase 61 — now describes the git/local/archive Sources model; "Requires AIPI_KEY" in retrieval-quality now states `BOR_LLM_API_KEY` in `.env` or `$AIPI_KEY`, matching the scripts' real convention). Everything else (URL table, env defaults vs `app/config.py`, sync/phase-150 copy, prod claims) verified accurate. AGENTS.md: verified consistent — no changes needed. - Notable: no code changes were required — the phase implementation was correct as left; no live infrastructure touched. - Next pending phase: `151_pdf_image_extraction` in `.agents/phases/todo/`.
This commit is contained in:
+160
-79
@@ -157,7 +157,7 @@
|
||||
* (the null prelude, "import") → today's "Syncing… <file> (n/m)"
|
||||
* byte-identical. The UPLOAD job (its status has no phase) keeps the
|
||||
* bare label. The untruncated label rides the button title (now set
|
||||
* for EVERY running label, not just file labels) + #sync-result as
|
||||
* for EVERY running label, not just file labels) + #sync-progress as
|
||||
* before (A4).
|
||||
*
|
||||
* Phase 98 (task 04) — the "Summary pending" markers: the catalog
|
||||
@@ -318,7 +318,7 @@
|
||||
* countText → the count part's textContent with the hidden toggle
|
||||
* (absent count → hidden + empty), then the UNTRUNCATED full label
|
||||
* — mainText + (countText ? " " + countText : "") — onto the button
|
||||
* title + #sync-result (A4 — the screen reader hears the whole
|
||||
* 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,
|
||||
@@ -370,22 +370,53 @@
|
||||
* sync/upload run is in flight, the button shows ONLY the spinning
|
||||
* icon (the `is-running` class — task 02's CSS hides the label parts;
|
||||
* the button IS the loading spinner, exactly the owner's ask), and a
|
||||
* NEW bordered text box under the button row (#sync-progress, the
|
||||
* presentational `aria-hidden` panel) shows the SAME full untruncated
|
||||
* live label — wrapping, never clipped, breathing room. #sync-result
|
||||
* REMAINS the sole aria-live announcer (one live region, no double
|
||||
* announce — the box is a visual duplicate for sighted users). The
|
||||
* wiring rides the SINGLE writer: setSyncLabel gains the `running`
|
||||
* flag (true on the running entry — the poll ticks, the 202/409
|
||||
* click, both re-attach entries; the settle default false) and writes
|
||||
* the full label into the box + reveals/hides it, and owns the
|
||||
* button's accessible name (full live label while running, the ship
|
||||
* value "Sync sources" restored on every settle). The `is-running`
|
||||
* NEW bordered text box under the button row (#sync-progress) shows
|
||||
* the SAME full untruncated live label — wrapping, never clipped,
|
||||
* breathing room. Phase 150 (task 02) folded the announcer into the
|
||||
* box — the previous live-region span was retired and the box is now
|
||||
* the SOLE sync text surface and the sole aria-live region (one live
|
||||
* region, no double announce). The wiring rides the SINGLE writer:
|
||||
* setSyncLabel gains the `running` flag (true on the running entry —
|
||||
* the poll ticks, the 202/409 click, both re-attach entries; the
|
||||
* settle default false) and writes the full label into the box +
|
||||
* reveals/hides it, and owns the button's accessible name (full live
|
||||
* label while running, the ship value "Sync sources" restored on
|
||||
* 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
|
||||
* control, the 409 double-trigger, the error banner/modal, and the
|
||||
* sync status API are all UNCHANGED.
|
||||
*
|
||||
* Phase 150 (task 02) — one box, one writer: the phase-149
|
||||
* live-region span that sat beside the box is retired —
|
||||
* #sync-progress is the SOLE sync text surface AND the sole
|
||||
* aria-live region (role="status" aria-live="polite", task 01).
|
||||
* setSyncProgress(text) is the ONLY function that writes the box:
|
||||
* the text lands on the inner #sync-progress-text span (textContent
|
||||
* only — the house rule, never HTML) and the box's visibility is
|
||||
* owned by this writer alone (hidden IFF the text is empty — the
|
||||
* global [hidden] rule does the display). Two content modes, decided
|
||||
* at the callsites (the phase-149 "the callsites are the only
|
||||
* decision sites" rule):
|
||||
* • running mode — setSyncLabel(main, count, chunk, running) 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
|
||||
* button, A2);
|
||||
* • result mode — the settle paths write the result line through
|
||||
* setSyncProgress: the counts line (fmtSyncResult — "N added ·
|
||||
* …"), "Sync cancelled", the cancel-failure copy ("Couldn't
|
||||
* cancel the sync — try again."); "" hides the box (the true
|
||||
* idle, the two upload settles — the upload's result line lives
|
||||
* on the Sources page, A3 — and the sync failure, whose banner +
|
||||
* modal carry the error).
|
||||
* requestCancel's pre-fetch clear is a NO-OP on the box (the run is
|
||||
* still in flight — blanking would hide the running surface for up
|
||||
* to one 2 s poll; the poll's `cancelled` branch settles it, and the
|
||||
* control's immediate disable keeps the never-stale contract). The
|
||||
* running label, the button title, and the button aria-label are
|
||||
* byte-identical to pre-phase; no copy changed.
|
||||
*/
|
||||
|
||||
import { fetchIsAdmin } from "./header.js";
|
||||
@@ -667,14 +698,14 @@ export async function mount(root) {
|
||||
* 5. sync cancelled → the phase-134 neutral terminal (task 02 —
|
||||
* A1: cancel is intent, not failure): settle
|
||||
* the main button retry-ready + "Sync
|
||||
* cancelled" on #sync-result, no banner / no
|
||||
* cancelled" on #sync-progress, no banner / no
|
||||
* modal (applySyncCancelled); the settle also
|
||||
* hides the Cancel control;
|
||||
* 6. upload success → settle "Sync sources" + catalog refresh
|
||||
* (loadTree — phase 90: an upload no longer
|
||||
* changes the KB, the re-read is a no-op safety
|
||||
* net); the upload's result line lives on the
|
||||
* Sources page, never in #sync-result (A3);
|
||||
* Sources page, never in #sync-progress (A3);
|
||||
* 7. upload failed → settle "Sync sources" — the failure is the
|
||||
* Sources page's error banner, never this page's (A3);
|
||||
* 8. both idle → retry-ready idle.
|
||||
@@ -697,13 +728,14 @@ export async function mount(root) {
|
||||
* nothing (the control stays disabled — the poll's branch 5 settles
|
||||
* everything; the run may already be terminal by then — the status
|
||||
* object, not the request, is authoritative); any other non-2xx /
|
||||
* network failure → the phase-55 neutral retry copy on #sync-result
|
||||
* + re-enable (the run is still going — the owner can retry).
|
||||
* network failure → the phase-55 neutral retry copy on
|
||||
* #sync-progress (the box) + re-enable (the run is still going —
|
||||
* the owner can retry).
|
||||
* The live label is the status endpoint's full source/relative/path
|
||||
* (A4) — or, in the post-import phases, the phase's own copy (Phase
|
||||
* 98 task 02): CSS ellipsizes #sync-label; the full untruncated
|
||||
* label also rides the button title (hover — set for EVERY running
|
||||
* label, not just file labels) and #sync-result (the aria-live
|
||||
* label, not just file labels) and #sync-progress (the aria-live
|
||||
* announcer — screen readers hear it). The load-time re-attach
|
||||
* (initSyncButton) re-enters a RUNNING sync the same way — with
|
||||
* whatever phase the status reports (a mid-summaries reload shows
|
||||
@@ -720,11 +752,14 @@ export async function mount(root) {
|
||||
* import phase after the file's first batch)), #sync-cancel-btn
|
||||
* (the Cancel control — phase 134 task 02, ship-hidden, revealed
|
||||
* only while a sync run is in flight), #sync-icon (the
|
||||
* spinner icon), #sync-result (aria-live result line — the SOLE
|
||||
* live region), #sync-progress (Phase 149 task 01: the full
|
||||
* untruncated live label's detail box under the button row —
|
||||
* presentational, aria-hidden, visible only while a run is in
|
||||
* flight; the .sync-progress-text span carries the label),
|
||||
* spinner icon), #sync-progress (Phase 149 task 01 + Phase 150:
|
||||
* the full untruncated live label's box under the button row —
|
||||
* the SOLE sync text surface AND the sole aria-live region
|
||||
* (role="status" aria-live="polite"); visible while a run is in
|
||||
* flight (the live label) and after a settle (the result line —
|
||||
* the counts line, "Sync cancelled", the cancel-failure copy),
|
||||
* hidden only when its text is empty; the .sync-progress-text
|
||||
* span carries the label),
|
||||
* #sync-error-banner / #sync-error-text (error banner).
|
||||
*/
|
||||
const syncBtn = root.querySelector("#sync-btn");
|
||||
@@ -738,16 +773,18 @@ export async function mount(root) {
|
||||
const syncLabelCount = syncLabel ? syncLabel.querySelector(".sync-label-count") : null;
|
||||
const syncLabelChunk = syncLabel ? syncLabel.querySelector(".sync-label-chunk") : null;
|
||||
const syncIcon = syncBtn ? syncBtn.querySelector(".sync-icon") : null;
|
||||
// Phase 149 (task 01): the sync detail box — the full UNTRUNCATED
|
||||
// live label in a bordered panel under the button row, visible ONLY
|
||||
// while a run is in flight (setSyncLabel reveals/hides it). The box
|
||||
// is presentational (aria-hidden): #sync-result stays the SOLE
|
||||
// aria-live announcer (one live region, no double announce).
|
||||
// Phase 149 (task 01) + Phase 150 (task 02): the sync detail box —
|
||||
// the full UNTRUNCATED live label in a bordered panel under the
|
||||
// button row, the SOLE sync text surface AND the sole aria-live
|
||||
// region for the sync area (the previous live-region span was
|
||||
// retired in phase 150 — one live region, no double announce).
|
||||
// Its text + visibility ride the single setSyncProgress writer
|
||||
// (the running label while a run is in flight; the settle result
|
||||
// line after it — the box hides only when its text is empty).
|
||||
const syncProgress = root.querySelector("#sync-progress");
|
||||
const syncProgressText = syncProgress
|
||||
? syncProgress.querySelector(".sync-progress-text")
|
||||
: null;
|
||||
const syncResult = root.querySelector("#sync-result");
|
||||
const syncErrorBanner = root.querySelector("#sync-error-banner");
|
||||
const syncErrorText = root.querySelector("#sync-error-text");
|
||||
|
||||
@@ -786,26 +823,29 @@ export async function mount(root) {
|
||||
* emptied + hidden=true (each span is hidden whenever there is
|
||||
* nothing of its kind to show). Then the UNTRUNCATED full label —
|
||||
* mainText + (countText ? " " + countText : "") + (chunkText ?
|
||||
* " " + chunkText : "") — rides the button title + #sync-result
|
||||
* (the aria-live announcer) exactly as before (A4 — the screen
|
||||
* reader hears the whole "Syncing… <file> (2/3) · chunk 14/38";
|
||||
* the visible spans carry main + count + chunk, the spaces between
|
||||
* them are the CSS gap). textContent only — the house rule, no
|
||||
* HTML strings. Every label state goes through here (the running
|
||||
* builders, the idle / settle labels, the 202/409 click, the
|
||||
* re-attach).
|
||||
* " " + chunkText : "") — rides the button title (A4 — the screen
|
||||
* reader hears the whole "Syncing… <file> (2/3) · chunk 14/38"
|
||||
* there, and in the #sync-progress box — the sole aria-live
|
||||
* region, phase 150 — while a run is in flight); the visible
|
||||
* spans carry main + count + chunk, the spaces between them are
|
||||
* the CSS gap. textContent only — the house rule, no HTML strings.
|
||||
* Every label state goes through here (the running builders, the
|
||||
* idle / settle labels, the 202/409 click, the re-attach).
|
||||
* Phase 149 (task 01): the fourth parameter — `running` (the
|
||||
* in-flight flag; the callsites are the ONLY decision sites: the
|
||||
* running entry passes true, the settle passes the default false).
|
||||
* The box (#sync-progress) gets the SAME untruncated `full` string
|
||||
* (textContent only) and is visible IFF `running` — the detail box
|
||||
* rides the writer, so it can never disagree with the button
|
||||
* title / aria / the announcer. The button's accessible name rides
|
||||
* here too: while running the icon-only button (task 02 hides the
|
||||
* label parts) reads the FULL 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). */
|
||||
* Phase 150 (task 02): while running, the SAME untruncated `full`
|
||||
* string goes into the box through the single box writer
|
||||
* (setSyncProgress — textContent only), so the box can never
|
||||
* disagree with the button title / aria; a non-running call leaves
|
||||
* the box alone — the idle / settle button labels must not fill
|
||||
* the panel, and the settle paths own the box's result line. The
|
||||
* button's accessible name rides here too: while running the
|
||||
* icon-only button (task 02 hides the label parts) reads the FULL
|
||||
* 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). */
|
||||
function setSyncLabel(mainText, countText = "", chunkText = "", running = false) {
|
||||
if (syncLabelMain) syncLabelMain.textContent = mainText;
|
||||
if (syncLabelCount) {
|
||||
@@ -831,18 +871,38 @@ export async function mount(root) {
|
||||
(countText ? ` ${countText}` : "") +
|
||||
(chunkText ? ` ${chunkText}` : "");
|
||||
if (syncBtn) syncBtn.title = full; // A4: the untruncated label rides the title, always
|
||||
if (syncResult) syncResult.textContent = full; // A4: the announcer hears the whole thing
|
||||
// Phase 149 (task 01): the detail box — the SAME untruncated label
|
||||
// (one source, two surfaces: sighted users read the box, the
|
||||
// announcer hears it), visible only while a run is in flight.
|
||||
if (syncProgressText) syncProgressText.textContent = full;
|
||||
if (syncProgress) syncProgress.hidden = !running;
|
||||
// Phase 150 (task 02): the box — the sole sync text surface and
|
||||
// the sole aria-live region — receives the running label through
|
||||
// the single box writer, and ONLY while a run is in flight (a
|
||||
// non-running call leaves the box alone: the idle / settle button
|
||||
// labels must not fill the panel).
|
||||
if (running) setSyncProgress(full);
|
||||
// The running button is the spinner (task 02 hides the label
|
||||
// parts) — its accessible name is the full live label; every
|
||||
// settle restores the ship value (see the function docstring).
|
||||
if (syncBtn) syncBtn.setAttribute("aria-label", running ? full : "Sync sources");
|
||||
}
|
||||
|
||||
/* Phase 150 (task 02): the SINGLE writer for the #sync-progress
|
||||
* box — the ONLY function that touches its text or visibility.
|
||||
* The text lands on the inner #sync-progress-text span (textContent
|
||||
* only — the house rule, never HTML) and the box's visibility is
|
||||
* owned by this writer alone: hidden IFF the text is empty (the
|
||||
* global [hidden] rule does the display). The content mode is
|
||||
* decided at the callsites (the phase-149 "the callsites are the
|
||||
* only decision sites" rule): the running label (setSyncLabel
|
||||
* passes `full` while a run is in flight) and the settle result
|
||||
* line (the counts line, "Sync cancelled", the cancel-failure
|
||||
* copy; "" hides the box — the true idle, the upload settles, the
|
||||
* sync failure). The box is the SOLE sync text surface and the
|
||||
* sole aria-live region for the sync area — one live region, no
|
||||
* double announce. */
|
||||
function setSyncProgress(text) {
|
||||
if (!syncProgressText || !syncProgress) return;
|
||||
syncProgressText.textContent = text;
|
||||
syncProgress.hidden = text === "";
|
||||
}
|
||||
|
||||
/* Phase 64 (task 04) + Phase 128 (task 01, the PARTS rework): the
|
||||
* live-file label. `kind` picks the prefix — "sync" → "Syncing…",
|
||||
* "upload" → "Importing" (the background run's word, A3). The
|
||||
@@ -856,7 +916,7 @@ export async function mount(root) {
|
||||
* import has started (total > 0). The builders return the PARTS
|
||||
* ({ main, count }); setSyncLabel writes them (the untruncated full
|
||||
* string — main + " " + count — rides the button title +
|
||||
* #sync-result, A4). Phase 98 (task 02): the SYNC job's
|
||||
* #sync-progress, A4). Phase 98 (task 02): the SYNC job's
|
||||
* post-import phases (overview / summaries) take their own label
|
||||
* via fmtSyncPhaseLabel (below) — this builder keeps that
|
||||
* prelude/import fall-through byte-identical and serves the UPLOAD
|
||||
@@ -891,7 +951,7 @@ export async function mount(root) {
|
||||
* batch (0/0) so the part hides. The UPLOAD job never passes a
|
||||
* status (its endpoint has no phase — D2) and keeps the bare
|
||||
* fmtSyncLabel parts. The untruncated full label rides the button
|
||||
* title + #sync-result like every running label (A4 —
|
||||
* title + #sync-progress like every running label (A4 —
|
||||
* setSyncLabel). */
|
||||
function fmtSyncPhaseLabel(status) {
|
||||
if (status.phase === "overview") {
|
||||
@@ -946,7 +1006,7 @@ export async function mount(root) {
|
||||
* Phase 128 (task 01): the builders return the PARTS and the single
|
||||
* writer (setSyncLabel) lands them — the main part (CSS-ellipsized),
|
||||
* the pinned count part (hidden when absent), and the FULL
|
||||
* untruncated label on the button title + #sync-result (A4 — set
|
||||
* untruncated label on the button title + #sync-progress (A4 — set
|
||||
* for EVERY running label; CSS ellipsizes the main part only).
|
||||
* Phase 129 (task 04): the third, pinned CHUNK part ("· chunk n/m"
|
||||
* — the in-flight file's embed-batch position) is destructured the
|
||||
@@ -1003,11 +1063,12 @@ export async function mount(root) {
|
||||
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
|
||||
// (absent), the label's full string rides the title + #sync-result
|
||||
// (A4, as with every label state). Phase 149 (task 01): the
|
||||
// default `running = false` keeps the detail box hidden in every
|
||||
// non-running state (idle, settled, error) and re-restores the
|
||||
// button's idle accessible name.
|
||||
// (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
|
||||
// non-running state (the settle paths own the box's result line
|
||||
// through setSyncProgress) and re-restores the button's idle
|
||||
// accessible name.
|
||||
setSyncLabel(label);
|
||||
}
|
||||
|
||||
@@ -1089,7 +1150,10 @@ export async function mount(root) {
|
||||
function applySyncSuccess(status) {
|
||||
const time = fmtSyncTime(status.finished_at);
|
||||
settleSyncButton(time ? `Synced ${time}` : "Synced");
|
||||
if (syncResult) syncResult.textContent = fmtSyncResult(status.detail);
|
||||
// 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).
|
||||
setSyncProgress(fmtSyncResult(status.detail));
|
||||
hideSyncError();
|
||||
emitSyncStatus(status);
|
||||
// Refresh the catalog live — the KB just changed.
|
||||
@@ -1104,7 +1168,9 @@ export async function mount(root) {
|
||||
syncBtn.setAttribute("aria-label", error);
|
||||
syncBtn.classList.add("is-error");
|
||||
}
|
||||
if (syncResult) syncResult.textContent = "";
|
||||
// Phase 150 (task 02): the error goes to the banner + the modal —
|
||||
// the box hides, empty (nothing to announce in the live region).
|
||||
setSyncProgress("");
|
||||
showSyncError(error);
|
||||
emitSyncStatus(status);
|
||||
showSyncModal(error);
|
||||
@@ -1114,21 +1180,26 @@ export async function mount(root) {
|
||||
* (A1: cancel is intent, not failure — no error banner, no modal,
|
||||
* no error styling): settle the main button retry-ready (which also
|
||||
* hides the Cancel control), announce "Sync cancelled" on the
|
||||
* aria-live result line, hide the error banner (a failed run's
|
||||
* banner must not survive the cancel), and emit the status frame
|
||||
* (the bor:sync-status detail is the status object, as for every
|
||||
* state). The settle's "Sync sources" label lands on #sync-result
|
||||
* FIRST and is superseded by the result line here — the main
|
||||
* button's title keeps "Sync sources" (the retry-ready name). */
|
||||
* aria-live box (#sync-progress — the sole live region, phase
|
||||
* 150), hide the error banner (a failed run's banner must not
|
||||
* survive the cancel), and emit the status frame (the
|
||||
* bor:sync-status detail is the status object, as for every
|
||||
* state). Phase 150 (task 02): the settle's "Sync sources" label
|
||||
* stays on the button only (the non-running setSyncLabel call no
|
||||
* longer writes the box) — the box shows the result line, nothing
|
||||
* else. */
|
||||
function applySyncCancelled(status) {
|
||||
settleSyncButton("Sync sources");
|
||||
if (syncResult) syncResult.textContent = "Sync cancelled";
|
||||
setSyncProgress("Sync cancelled"); // the box's result line (phase 150)
|
||||
hideSyncError();
|
||||
emitSyncStatus(status);
|
||||
}
|
||||
|
||||
function applySyncIdle(status) {
|
||||
settleSyncButton("Sync sources");
|
||||
// Phase 150 (task 02): the true idle — retry-ready, nothing to
|
||||
// announce: the box (the sole live region) hides, empty.
|
||||
setSyncProgress("");
|
||||
emitSyncStatus(status || { state: "idle" });
|
||||
}
|
||||
|
||||
@@ -1206,11 +1277,12 @@ export async function mount(root) {
|
||||
return;
|
||||
}
|
||||
// 6. upload success: settle + catalog refresh (A3 — the upload's
|
||||
// counts live on the Sources page; #sync-result stays empty).
|
||||
// counts live on the Sources page; #sync-progress stays
|
||||
// empty + hidden).
|
||||
if (uploadStatus && uploadStatus.state === "success") {
|
||||
stopSyncPolling();
|
||||
settleSyncButton("Sync sources");
|
||||
if (syncResult) syncResult.textContent = "";
|
||||
setSyncProgress(""); // the upload settle: the box hides (phase 150)
|
||||
hideSyncError();
|
||||
emitSyncStatus({ state: "idle" });
|
||||
loadTree();
|
||||
@@ -1221,7 +1293,7 @@ export async function mount(root) {
|
||||
if (uploadStatus && uploadStatus.state === "failed") {
|
||||
stopSyncPolling();
|
||||
settleSyncButton("Sync sources");
|
||||
if (syncResult) syncResult.textContent = "";
|
||||
setSyncProgress(""); // the upload settle: the box hides (phase 150)
|
||||
hideSyncError();
|
||||
emitSyncStatus({ state: "idle" });
|
||||
return;
|
||||
@@ -1253,7 +1325,8 @@ export async function mount(root) {
|
||||
if (r.status === 202 || r.status === 409) {
|
||||
// Phase 64: the run is just starting (model check / clone-pull) —
|
||||
// bare "Syncing…" until the first polled file (A4); entering the
|
||||
// running state also clears #sync-result with the same label.
|
||||
// running state reveals #sync-progress with the same label
|
||||
// (phase 150: the box IS the announcer).
|
||||
enterSyncRunningState("sync", null, 0, 0);
|
||||
hideSyncError();
|
||||
if (lastSyncState !== "running") emitSyncStatus({ state: "running" });
|
||||
@@ -1271,7 +1344,11 @@ export async function mount(root) {
|
||||
/* Phase 134 (task 02): the cancel request — the §7.4 never-stale
|
||||
* contract applies exactly as to the main button: the control
|
||||
* disables IMMEDIATELY (one request at a time — no double fire)
|
||||
* BEFORE the fetch, and #sync-result clears with it.
|
||||
* BEFORE the fetch. Phase 150 (task 02): there is no pre-fetch box
|
||||
* clear — the run is still in flight at that moment, so blanking
|
||||
* would hide the running surface for up to one 2 s poll; the
|
||||
* poll's `cancelled` branch settles the box, and the control's
|
||||
* immediate disable keeps the never-stale contract.
|
||||
* • 403 → the whoami-backstop branch (stop polling, hide the
|
||||
* main button, settle idle) — the same depth as every
|
||||
* other 403 in this module;
|
||||
@@ -1283,19 +1360,23 @@ export async function mount(root) {
|
||||
* already be terminal by the next tick — the status
|
||||
* object, not the request, is authoritative);
|
||||
* • any other non-2xx / network failure → the phase-55 neutral
|
||||
* retry copy on #sync-result + the control re-enables (the run
|
||||
* is still going — the owner can retry). */
|
||||
* retry copy on #sync-progress (the box) + the control
|
||||
* re-enables (the run is still going — the owner can retry). */
|
||||
async function requestCancel() {
|
||||
if (!syncCancelBtn) return;
|
||||
syncCancelBtn.disabled = true; // FIRST — never stale, one request at a time
|
||||
if (syncResult) syncResult.textContent = "";
|
||||
// Phase 150 (task 02): the pre-fetch clear is dropped — a NO-OP on
|
||||
// the box. The run is still in flight at this moment, so blanking
|
||||
// would hide the running surface for up to one 2 s poll; the
|
||||
// poll's `cancelled` branch settles it. The never-stale contract
|
||||
// is kept by the control's immediate disable above.
|
||||
let r;
|
||||
try {
|
||||
r = await fetch("/api/sync/cancel", { method: "POST" });
|
||||
} catch {
|
||||
// Network failure: the run is still going — the phase-55
|
||||
// neutral retry copy + the control re-enables (retryable).
|
||||
if (syncResult) syncResult.textContent = "Couldn't cancel the sync — try again.";
|
||||
setSyncProgress("Couldn't cancel the sync — try again.");
|
||||
syncCancelBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
@@ -1307,7 +1388,7 @@ export async function mount(root) {
|
||||
}
|
||||
if (r.status === 409) return; // race: the run ended — the poll settles
|
||||
if (r.ok) return; // 202: the poll's cancelled branch settles everything
|
||||
if (syncResult) syncResult.textContent = "Couldn't cancel the sync — try again.";
|
||||
setSyncProgress("Couldn't cancel the sync — try again.");
|
||||
syncCancelBtn.disabled = false; // the run is still going — retry
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user