phase: 128_sync_label_mobile
Everything verified and 100% green. The earlier rejection was a report-format issue, not a test failure — all work is in the working tree. Final report: **Phase 128 final pass — verified (all 3 tasks complete; no code defects found)** - Verified: `#sync-label` parts markup, `setSyncLabel` writer, part builders (A4 untruncated title/aria-live), flex-row CSS + mobile media query — all match design 1:1. - `uv run pytest --cov=app --cov-report=term`: 3024 passed; TOTAL coverage **99%** (>90% gate). - `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`: 4 passed (119s, isolated). - `uv run ruff check .`: clean; `uv run pyright`: 0 errors. - Criteria: (1) mobile 390×844 label in-viewport, count visible in import+summaries — ✅ E2E; (2) desktop strings/cap/title unchanged — ✅ E2E+unit; (3) E2E isolated green — ✅; (4) full suite green — ✅. - Docs: README.md fixed 5 pre-existing gaps (PDF phase-126 section + `pdf` token, hidden-folders toggle ph.105, document dates ph.106, private git-source masked tokens ph.121, doc-draft Discard ph.115); AGENTS.md verified consistent — no changes. - Notable: double-`-q` (addopts `-q` + CLI `-q`) suppresses pytest's summary line — standard `-qq` behavior, not a bug; exit codes confirm green. - Next pending phase: `129_sync_chunk_progress`.
This commit is contained in:
+121
-43
@@ -300,6 +300,34 @@
|
||||
* The glyph is static SVG (aria-hidden — the alt text is the
|
||||
* accessible content); the box + img are properties only, never
|
||||
* innerHTML with document-derived data (the house rule).
|
||||
*
|
||||
* Phase 128 (task 01) — the sync label becomes PARTS: the running
|
||||
* label's string is split into a shrinks-to-fit MAIN part (prefix +
|
||||
* the file/folder path — the part CSS may ellipsize) and a pinned
|
||||
* COUNT part ("(n/m)" — the position, NEVER clipped), so on a phone
|
||||
* the file path ellipsizes to fit while the count stays on-screen
|
||||
* (the phase-64 bug: with max-width lifted, the inline-block sized to
|
||||
* its full content and the count ran off-screen). #sync-label now
|
||||
* ships TWO child spans — .sync-label-main (the initial idle text
|
||||
* lives there so the pre-JS label renders) + .sync-label-count
|
||||
* (hidden + empty by default) — and the label builders no longer
|
||||
* return one string: fmtSyncLabel / fmtSyncPhaseLabel return the
|
||||
* PARTS ({ main, count } — the same decision tree, the same byte-
|
||||
* identical copy), and ONE writer — setSyncLabel(mainText,
|
||||
* countText) — lands them: mainText → the main part's textContent,
|
||||
* 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
|
||||
* 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,
|
||||
* 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>",
|
||||
* "Syncing… <file> (n/m)", "Importing", "Writing KB overview…",
|
||||
* "Summarizing folders… <folder> (n/m)"); textContent only — no HTML
|
||||
* strings (the house rule).
|
||||
*/
|
||||
|
||||
import { fetchIsAdmin } from "./header.js";
|
||||
@@ -598,12 +626,17 @@ export async function mount(root) {
|
||||
* upload the same way; a terminal upload is a no-op there (the
|
||||
* boot-time loadTree() already shows the current catalog).
|
||||
*
|
||||
* Elements: #sync-btn (the button), #sync-label (the text),
|
||||
* #sync-icon (the spinner icon), #sync-result (aria-live result
|
||||
* line), #sync-error-banner / #sync-error-text (error banner).
|
||||
* Elements: #sync-btn (the button), #sync-label (the text —
|
||||
* Phase 128 (task 01): the PARTS — .sync-label-main, the
|
||||
* shrinks-to-fit prefix + file/folder, + .sync-label-count, the
|
||||
* pinned "(n/m)" position (hidden when absent)), #sync-icon (the
|
||||
* spinner icon), #sync-result (aria-live result line),
|
||||
* #sync-error-banner / #sync-error-text (error banner).
|
||||
*/
|
||||
const syncBtn = root.querySelector("#sync-btn");
|
||||
const syncLabel = syncBtn ? syncBtn.querySelector(".sync-label") : null;
|
||||
const syncLabelMain = syncLabel ? syncLabel.querySelector(".sync-label-main") : null;
|
||||
const syncLabelCount = syncLabel ? syncLabel.querySelector(".sync-label-count") : null;
|
||||
const syncIcon = syncBtn ? syncBtn.querySelector(".sync-icon") : null;
|
||||
const syncResult = root.querySelector("#sync-result");
|
||||
const syncErrorBanner = root.querySelector("#sync-error-banner");
|
||||
@@ -632,45 +665,86 @@ export async function mount(root) {
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/* Phase 64 (task 04): the live-file label. `kind` picks the prefix —
|
||||
* "sync" → "Syncing…", "upload" → "Importing" (the background run's
|
||||
* word, A3). The current file — the status endpoint's full
|
||||
* source/relative/path (A4) — is appended while one is being processed;
|
||||
* the BARE prefix shows during the clone/pull (sync), before any file
|
||||
* is indexed. Phase 90: the upload run is unpack + register only (no
|
||||
* scan), so its status never carries a file or counts — the
|
||||
* "Importing" label is always the bare one. The counts appear only
|
||||
* once the import has started (total > 0). CSS ellipsizes the button
|
||||
* label; the same untruncated text goes to the button title +
|
||||
* #sync-result (the aria-live announcer). Phase 98 (task 02): the
|
||||
* SYNC job's post-import phases (overview / summaries) take their
|
||||
* own label via fmtSyncPhaseLabel (below) — this builder keeps that
|
||||
/* Phase 128 (task 01): the label PARTS writer — the SINGLE point
|
||||
* that writes #sync-label. mainText lands in the MAIN part
|
||||
* (.sync-label-main — the shrinks-to-fit prefix + file/folder, the
|
||||
* part CSS ellipsizes); countText lands in the COUNT part
|
||||
* (.sync-label-count — the pinned "(n/m)" position, NEVER clipped):
|
||||
* present → its textContent + hidden=false, absent → emptied +
|
||||
* hidden=true (the count span is hidden whenever there is no
|
||||
* "(n/m)" to show). Then the UNTRUNCATED full label —
|
||||
* mainText + (countText ? " " + countText : "") — rides the button
|
||||
* title + #sync-result (the aria-live announcer) exactly as before
|
||||
* (A4 — the screen reader hears the whole thing; the visible span
|
||||
* carries main + count, the space between them is 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). */
|
||||
function setSyncLabel(mainText, countText = "") {
|
||||
if (syncLabelMain) syncLabelMain.textContent = mainText;
|
||||
if (syncLabelCount) {
|
||||
if (countText) {
|
||||
syncLabelCount.textContent = countText;
|
||||
syncLabelCount.hidden = false;
|
||||
} else {
|
||||
syncLabelCount.textContent = "";
|
||||
syncLabelCount.hidden = true;
|
||||
}
|
||||
}
|
||||
const full = countText ? `${mainText} ${countText}` : mainText;
|
||||
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 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
|
||||
* current file — the status endpoint's full source/relative/path
|
||||
* (A4) — lands in the MAIN part while one is being processed; the
|
||||
* BARE prefix shows during the clone/pull (sync), before any file
|
||||
* is indexed. Phase 90: the upload run is unpack + register only
|
||||
* (no scan), so its status never carries a file or counts — the
|
||||
* "Importing" label is always the bare one. The counts — the PINNED
|
||||
* count part, never clipped (Phase 128) — appear only once the
|
||||
* 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
|
||||
* 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
|
||||
* job unchanged (its status has no phase — D2). */
|
||||
function fmtSyncLabel(kind, currentFile, done, total) {
|
||||
const prefix = kind === "upload" ? "Importing" : "Syncing…";
|
||||
let label = currentFile ? `${prefix} ${currentFile}` : prefix;
|
||||
if (total > 0) label += ` (${done}/${total})`;
|
||||
return label;
|
||||
const main = currentFile ? `${prefix} ${currentFile}` : prefix;
|
||||
const count = total > 0 ? `(${done}/${total})` : "";
|
||||
return { main, count };
|
||||
}
|
||||
|
||||
/* Phase 98 (task 02, D2): the SYNC job's phase-aware running label
|
||||
* — the post-import span where the file count sits still (the
|
||||
* owner's "the number pauses for a really long time"): "overview"
|
||||
* → "Writing KB overview…"; "summaries" → "Summarizing folders…
|
||||
* <source/folder> (n/m)" — the folder part is omitted while
|
||||
* current_summary is null (the phase's first poll); anything else
|
||||
* (the null prelude, "import") → the phase-64 fmtSyncLabel
|
||||
* fall-through, byte-identical (same builder, same fields, same
|
||||
* order). The UPLOAD job never passes a status (its endpoint has no
|
||||
* phase — D2) and keeps the bare fmtSyncLabel label. The untruncated
|
||||
* result rides the button title + #sync-result like every running
|
||||
* label (A4). */
|
||||
/* Phase 98 (task 02, D2) + Phase 128 (task 01, the PARTS rework):
|
||||
* the SYNC job's phase-aware running label — the post-import span
|
||||
* where the file count sits still (the owner's "the number pauses
|
||||
* for a really long time"): "overview" → "Writing KB overview…"
|
||||
* (no count part); "summaries" → main "Summarizing folders…
|
||||
* <source/folder>" + count "(n/m)" — the folder part is omitted
|
||||
* while current_summary is null (the phase's first poll) and the
|
||||
* COUNT IS ALWAYS EMITTED in this phase (the hook always carries a
|
||||
* total); anything else (the null prelude, "import") → the
|
||||
* phase-64 fmtSyncLabel parts, byte-identical (same builder, same
|
||||
* fields, same order). 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 — setSyncLabel). */
|
||||
function fmtSyncPhaseLabel(status) {
|
||||
if (status.phase === "overview") return "Writing KB overview…";
|
||||
if (status.phase === "overview") {
|
||||
return { main: "Writing KB overview…", count: "" };
|
||||
}
|
||||
if (status.phase === "summaries") {
|
||||
const folder = status.current_summary ? ` ${status.current_summary}` : "";
|
||||
return `Summarizing folders…${folder} (${status.summaries_done}/${status.summaries_total})`;
|
||||
return {
|
||||
main: `Summarizing folders…${folder}`,
|
||||
count: `(${status.summaries_done}/${status.summaries_total})`,
|
||||
};
|
||||
}
|
||||
// null phase (the prelude) or "import" — today's byte-identical
|
||||
// sync label (the phase-64 contract, unchanged).
|
||||
@@ -702,26 +776,26 @@ export async function mount(root) {
|
||||
* is phase-aware (fmtSyncPhaseLabel: "Writing KB overview…" /
|
||||
* "Summarizing folders… <folder> (n/m)"); the UPLOAD job and the
|
||||
* 202/409 click (no status yet) keep the bare fmtSyncLabel label.
|
||||
* Same mechanics as before (disabled, aria-busy, spinning icon, no
|
||||
* is-error) plus: the FULL untruncated label on the button title
|
||||
* (set for EVERY running label — the phase-64 file-only rule is
|
||||
* adjusted for the phase labels) and in #sync-result (the aria-live
|
||||
* announcer reads it; CSS ellipsizes the button's label span only).
|
||||
* 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
|
||||
* for EVERY running label; CSS ellipsizes the main part only). Same
|
||||
* never-stale mechanics as before (disabled, aria-busy, spinning
|
||||
* icon, no is-error).
|
||||
*/
|
||||
function enterSyncRunningState(kind, currentFile, done, total, status) {
|
||||
if (!syncBtn) return;
|
||||
syncBtn.disabled = true;
|
||||
syncBtn.setAttribute("aria-busy", "true");
|
||||
const label =
|
||||
const { main, count } =
|
||||
kind === "sync" && status
|
||||
? fmtSyncPhaseLabel(status)
|
||||
: fmtSyncLabel(kind, currentFile, done, total);
|
||||
syncBtn.title = label; // A4: the untruncated label rides the title, always
|
||||
setSyncLabel(main, count); // the parts + the A4 untruncated title / announcer
|
||||
syncBtn.setAttribute("aria-label", "Sync sources");
|
||||
syncBtn.classList.remove("is-error");
|
||||
if (syncIcon) syncIcon.classList.add("is-spinning");
|
||||
if (syncLabel) syncLabel.textContent = label;
|
||||
if (syncResult) syncResult.textContent = label;
|
||||
}
|
||||
|
||||
function settleSyncButton(label) {
|
||||
@@ -732,7 +806,11 @@ export async function mount(root) {
|
||||
syncBtn.setAttribute("aria-label", "Sync sources");
|
||||
syncBtn.classList.remove("is-error");
|
||||
if (syncIcon) syncIcon.classList.remove("is-spinning");
|
||||
if (syncLabel) syncLabel.textContent = label;
|
||||
// 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).
|
||||
setSyncLabel(label);
|
||||
}
|
||||
|
||||
function showSyncError(detail) {
|
||||
|
||||
Reference in New Issue
Block a user