phase: 150_drop_sync_result
Build and Push Containers / build-and-push-app (push) Successful in 2m24s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**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:
2026-10-02 02:35:03 -04:00
parent 9d4c813cae
commit edf6048a0d
59 changed files with 2719 additions and 476 deletions
+160 -79
View File
@@ -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
}
+24 -38
View File
@@ -2203,12 +2203,13 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
}
/* Phase 64: the live-file label ("Syncing… <file> (n/m)" / "Importing
<file> (n/m)") — a long source/relative/path ellipsizes inside the
pill; the full path lives in the button title + #sync-result (the
aria-live announcer). Phase 128 (task 02): the label is a FLEX ROW
pill; the full path lives in the button title + the #sync-progress
box (the aria-live announcer — phase 150: the SOLE sync text
surface). Phase 128 (task 02): the label is a FLEX ROW
of parts — .sync-label-main (the prefix + file/folder: min-width
0, the shrinkable item whose ellipsis engages) + .sync-label-count
(the pinned "(n/m)": flex 0 0 auto, NEVER clipped — the
.sync-result mono language, a position reads as data). Phase 129
.sync-progress-text mono language, a position reads as data). Phase 129
(task 04): a THIRD pinned part — .sync-label-chunk (the "· chunk
n/m" embed-batch position of the in-flight file, the same pinned
mono language; hidden in every state but the import phase after
@@ -2229,7 +2230,7 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
.sync-label-count {
flex: 0 0 auto; /* NEVER clipped — the (n/m) position is the point */
white-space: nowrap;
font-family: var(--mono); /* the .sync-result mono language (position reads as data) */
font-family: var(--mono); /* the .sync-progress-text mono language (position reads as data) */
}
.sync-label-chunk {
flex: 0 0 auto; /* NEVER clipped — the · chunk n/m position is the point (phase 129) */
@@ -2245,10 +2246,11 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
body 1.55 line-height, + the 0.75rem block padding + the 2px border,
border-box) so even a one-line label reads as a calm panel with
breathing room. The box is hidden-attr-driven (the global [hidden]
!important rule keeps it display:none at idle — it exists only while
a run is in flight). Mobile: a block in the same .container rides the
100%-width phone shell — full width with no override (task 03's E2E
asserts it at 360px). */
!important rule keeps it display:none while its text is empty —
phase 150: it shows while a run is in flight AND stays up after
settle to carry the result line). Mobile: a block in the same
.container rides the 100%-width phone shell — full width with no
override (task 03's E2E asserts it at 360px). */
.sync-progress {
background: var(--surface);
border: 1px solid var(--line);
@@ -2258,12 +2260,12 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
}
.sync-progress-text {
display: block;
font-family: var(--mono); /* the .sync-result mono language (position reads as data) */
font-size: 0.8rem; /* the .sync-result size */
font-family: var(--mono); /* the house mono language — position reads as data */
font-size: 0.8rem; /* the house small-data size */
line-height: 1.55; /* the house body line-height */
color: var(--ink-soft); /* the .sync-result ink — a PINNED identity pair on --surface
(8.2:1 on the built-in dark, AA in both built-ins; the
theme gate keeps any custom theme at >= 4.5:1) */
color: var(--ink-soft); /* PINNED identity pair on --surface (8.2:1 on the
built-in dark, AA in both built-ins; the theme gate
keeps any custom theme at >= 4.5:1) */
overflow-wrap: anywhere; /* THE POINT of the box — a long source/relative/path WRAPS
inside the panel; the pre-phase pill ellipsis is retired
for the live detail (never clipped) */
@@ -2280,15 +2282,6 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
neither. The idle "Sync sources" label (and the mobile ellipsis rules
for it) render exactly as pre-phase when the class is absent. */
.sync-btn.is-running .sync-label { display: none; }
/* Last-result announcer — soft ink, small mono */
.sync-result {
display: block;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 0.8rem;
white-space: nowrap;
padding-block: 0.5rem;
}
/* Sync failure modal — lazily created by sources.js, appended to <body>.
Error surface: --err-bg with --err-line border, title in --ink,
error text in --err-ink (9.1:1 on --err-bg). Backdrop dims with
@@ -2935,10 +2928,11 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
}
#archive-upload-file::file-selector-button:hover { border-color: var(--brand); color: var(--brand-ink); }
/* The upload's success line (role=status): the sync-result shape
("2 added · 1 pruned") — ink-soft on surface (>=4.5:1), the dashed
hint-box border marks it as a result, not an error; it drops onto
its own row under the input like the error line. */
/* The upload's success line (role=status): the .sync-progress-text
soft-ink result-line language ("2 added · 1 pruned") — ink-soft on
surface (>=4.5:1), the dashed hint-box border marks it as a result,
not an error; it drops onto its own row under the input like the
error line. */
#archive-upload-result {
flex-basis: 100%;
margin: 0;
@@ -3656,9 +3650,10 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
gap: 1.25rem;
flex: 1;
}
/* Action feedback line (role=status): the sync-result shape —
ink-soft on surface (>=4.5:1), small mono; the min-height holds
the layout so a delete's line never reflows the table. */
/* Action feedback line (role=status): the .sync-progress-text mono /
soft-ink language — ink-soft on surface (>=4.5:1), small mono; the
min-height holds the layout so a delete's line never reflows the
table. */
.history-status {
display: block;
min-height: 1.2em;
@@ -5636,15 +5631,6 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }
.history-refresh { width: 100%; }
.history-refresh svg { display: block; }
.sync-result {
position: absolute !important;
width: 1px; height: 1px;
margin: -1px; padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no