phase: 152_sync_progress_image_pass
All criteria verified. The CLI path is hook-less (only `app/api/sync.py` passes the hook), and the byte-identity is pinned by `test_on_image_progress_none_run_is_byte_identical`. ## Final Report — Phase 152 final verification pass - **Verified (no new code needed):** `SyncStatus.images_done/images_total` + reset/clear rules in `app/api/sync.py`; `on_image_progress` threaded through the importer's three PDF vision spans with the continuous per-file counter; the `_image_hook` closure; `sources.js` `· image n/m` label part (box + title + aria). - **C1** status climbs 1→N + UI shows `· image n/m`: ✅ unit 3/15→15/15, integration 1/15→7/15, E2E 1/4→4/4 (title/aria ride the full string) - **C2** never frozen: ✅ E2E asserts ≥3 distinct positions, starting at 1, non-decreasing, ≤ total, reaching it - **C3** cleared at overview/summaries + all terminals; per-file reset via `_hook`; no stale position for image-less files: ✅ unit pins + E2E settle (image part gone in box/title/aria, endpoint 0/0) - **C4** `/api/sync/status` additive-only: ✅ full-dict pin passes (all pre-existing keys unchanged + the two new 0/0 keys) - **C5** hook-less caller byte-identical: ✅ CLI passes no hook; `test_on_image_progress_none_run_is_byte_identical` green - **C6** `uv run pytest`: **3526 passed, exit 0** (95s); dedicated E2E in isolation: **2 passed** (95s); `uv run ruff check . && uv run pyright`: **clean**; coverage: **99% on `app/` (>90% gate)** - Regression: all five neighbouring E2E suites green in isolation (progress_box 8, chunk_progress 2, label_mobile 4, pdf_support 6, image_documents 4) - **C7** atomic commit: left to the harness (no git index/history touched per rules) - **Docs:** README.md — sync-label paragraph already updated this phase, accurately; all `.env.example` vars documented; routes/commands verified — **no changes needed**. AGENTS.md — all references (PLAN, user_stories, test_smoke.py, .env.example, gpg config, .gitignore) verified — **no changes needed** - **Defects found in prior work:** none (one prior env-leak defect was already fixed inside this phase's task 04, conftest) - No live-infrastructure touched; no ntfy used. **Next pending phase:** none — `todo/` held only this phase
This commit is contained in:
+93
-49
@@ -364,7 +364,8 @@
|
||||
*
|
||||
* Phase 149 (task 01) — the sync detail box + the icon-only running
|
||||
* button: the owner's report was that the full live label (a long
|
||||
* source/relative/path + the counts + the chunk part) is clipped in
|
||||
* source/relative/path + the counts + the chunk part + the phase-152
|
||||
* image part) is clipped in
|
||||
* the pill even on desktop — the mobile solution (stuffing the parts
|
||||
* in the button) had become the permanent surface. The fix: while a
|
||||
* sync/upload run is in flight, the button shows ONLY the spinning
|
||||
@@ -399,7 +400,8 @@
|
||||
* 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
|
||||
* • running mode — setSyncLabel(main, count, chunk, running, image)
|
||||
* 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
|
||||
@@ -749,7 +751,12 @@ export async function mount(root) {
|
||||
* pinned "(n/m)" position (hidden when absent); phase 129 (task
|
||||
* 04): + .sync-label-chunk, the pinned "· chunk n/m" embed-batch
|
||||
* position of the in-flight file (hidden in every state but the
|
||||
* import phase after the file's first batch)), #sync-cancel-btn
|
||||
* import phase after the file's first batch); phase 152 (task 03):
|
||||
* the in-flight PDF's "· image n/m" vision-image position gets NO
|
||||
* span of its own — the running button is the spinner (its parts
|
||||
* are hidden, phase 149), so the position rides only the composed
|
||||
* full string (the box / title / running aria-label)),
|
||||
* #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-progress (Phase 149 task 01 + Phase 150:
|
||||
@@ -811,29 +818,38 @@ export async function mount(root) {
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/* Phase 128 (task 01) + Phase 129 (task 04): 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); chunkText (phase 129) lands in the CHUNK part
|
||||
* (.sync-label-chunk — the pinned "· chunk n/m" embed-batch
|
||||
* position of the in-flight file). The COUNT and CHUNK parts share
|
||||
* one contract: present → its textContent + hidden=false, absent →
|
||||
* 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 (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 128 (task 01) + Phase 129 (task 04) + Phase 152 (task 03):
|
||||
* 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); chunkText (phase 129) lands in
|
||||
* the CHUNK part (.sync-label-chunk — the pinned "· chunk n/m"
|
||||
* embed-batch position of the in-flight file); imageText (phase
|
||||
* 152) is the in-flight PDF's "· image n/m" vision-image position —
|
||||
* it gets NO span of its own (the running button is the spinner —
|
||||
* its parts are hidden, phase 149), it rides ONLY the composed full
|
||||
* string. The COUNT and CHUNK parts share one contract: present →
|
||||
* its textContent + hidden=false, absent → 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 : "") +
|
||||
* (imageText ? " " + imageText : "") — rides the button title (A4 —
|
||||
* the screen reader hears the whole "Syncing… <file> (2/3) · chunk
|
||||
* 14/38 · image 3/15" 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).
|
||||
* Phase 152 (task 03): the fifth parameter — `imageText` (the "·
|
||||
* image n/m" part; the chunk and image parts are distinct and both
|
||||
* may show, and each disappears independently — the bare builders
|
||||
* and the non-running calls pass nothing, so both default "").
|
||||
* 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
|
||||
@@ -846,7 +862,7 @@ export async function mount(root) {
|
||||
* 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) {
|
||||
function setSyncLabel(mainText, countText = "", chunkText = "", running = false, imageText = "") {
|
||||
if (syncLabelMain) syncLabelMain.textContent = mainText;
|
||||
if (syncLabelCount) {
|
||||
if (countText) {
|
||||
@@ -866,10 +882,16 @@ export async function mount(root) {
|
||||
syncLabelChunk.hidden = true;
|
||||
}
|
||||
}
|
||||
// Phase 152 (task 03): the image part is the LAST piece of the
|
||||
// composed full string (after the chunk part) — the box, the
|
||||
// button title, and the running aria-label all carry it; the
|
||||
// button's label spans stay as-is (no new span — see the
|
||||
// docstring).
|
||||
const full =
|
||||
mainText +
|
||||
(countText ? ` ${countText}` : "") +
|
||||
(chunkText ? ` ${chunkText}` : "");
|
||||
(chunkText ? ` ${chunkText}` : "") +
|
||||
(imageText ? ` ${imageText}` : "");
|
||||
if (syncBtn) syncBtn.title = full; // A4: the untruncated label rides the title, always
|
||||
// Phase 150 (task 02): the box — the sole sync text surface and
|
||||
// the sole aria-live region — receives the running label through
|
||||
@@ -924,6 +946,8 @@ export async function mount(root) {
|
||||
* 04): this builder still returns NO chunk part — the pinned
|
||||
* "· chunk n/m" part is added by fmtSyncPhaseLabel's import
|
||||
* fall-through (the only site that sees the status's chunk
|
||||
* fields), and phase 152 (task 03) likewise adds the "· image
|
||||
* n/m" part there (the only site that sees the status's image
|
||||
* fields), so the upload label and the 202/409 click stay bare. */
|
||||
function fmtSyncLabel(kind, currentFile, done, total) {
|
||||
const prefix = kind === "upload" ? "Importing" : "Syncing…";
|
||||
@@ -932,30 +956,38 @@ export async function mount(root) {
|
||||
return { main, count };
|
||||
}
|
||||
|
||||
/* 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) and the CHUNK part stays empty (the summaries phase's own
|
||||
* (n/m) is its position — the status's chunk fields are cleared at
|
||||
* this transition); anything else (the null prelude, "import") →
|
||||
* the phase-64 fmtSyncLabel parts, byte-identical (same builder,
|
||||
* same fields, same order) + the phase-129 CHUNK part: "· chunk
|
||||
* n/m" ONLY while chunks_total > 0 (a file's embed batch is in
|
||||
* flight — n the 1-based first chunk of the batch, m the file's
|
||||
* total content chunks, batch granularity), empty before the first
|
||||
* batch (0/0) so the part hides. The UPLOAD job never passes a
|
||||
/* Phase 98 (task 02, D2) + Phase 128 (task 01, the PARTS rework)
|
||||
* + Phase 152 (task 03): 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) and the CHUNK and IMAGE parts stay
|
||||
* empty (the summaries phase's own (n/m) is its position — the
|
||||
* status's chunk + image fields are cleared at this transition);
|
||||
* anything else (the null prelude, "import") → the phase-64
|
||||
* fmtSyncLabel parts, byte-identical (same builder, same fields,
|
||||
* same order) + the phase-129 CHUNK part: "· chunk n/m" ONLY while
|
||||
* chunks_total > 0 (a file's embed batch is in flight — n the
|
||||
* 1-based first chunk of the batch, m the file's total content
|
||||
* chunks, batch granularity), empty before the first batch (0/0) so
|
||||
* the part hides + the phase-152 IMAGE part: "· image n/m" ONLY
|
||||
* while images_total > 0 (a PDF's vision span is in flight — n the
|
||||
* 1-based image about to be described, m the file's total images —
|
||||
* ONE continuous counter across the PDF's three vision spans, the
|
||||
* importer owns the continuity), empty before the first image
|
||||
* (0/0) so the part hides; the two positions are distinct and both
|
||||
* may show ("… · chunk 125/126 · image 3/15"), each disappearing
|
||||
* independently. 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-progress like every running label (A4 —
|
||||
* setSyncLabel). */
|
||||
function fmtSyncPhaseLabel(status) {
|
||||
if (status.phase === "overview") {
|
||||
return { main: "Writing KB overview…", count: "", chunk: "" };
|
||||
return { main: "Writing KB overview…", count: "", chunk: "", image: "" };
|
||||
}
|
||||
if (status.phase === "summaries") {
|
||||
const folder = status.current_summary ? ` ${status.current_summary}` : "";
|
||||
@@ -963,11 +995,14 @@ export async function mount(root) {
|
||||
main: `Summarizing folders…${folder}`,
|
||||
count: `(${status.summaries_done}/${status.summaries_total})`,
|
||||
chunk: "", // this phase's own (n/m) is its position — no chunk part
|
||||
image: "", // the image position belongs to the import phase only
|
||||
};
|
||||
}
|
||||
// null phase (the prelude) or "import" — today's byte-identical
|
||||
// sync label (the phase-64 contract, unchanged) + the phase-129
|
||||
// chunk part (hidden before the in-flight file's first batch).
|
||||
// chunk part (hidden before the in-flight file's first batch) +
|
||||
// the phase-152 image part (hidden before the in-flight PDF's
|
||||
// first vision describe call).
|
||||
const { main, count } = fmtSyncLabel(
|
||||
"sync", status.current_file, status.files_done, status.files_total
|
||||
);
|
||||
@@ -975,7 +1010,11 @@ export async function mount(root) {
|
||||
status.chunks_total > 0
|
||||
? `· chunk ${status.current_chunk}/${status.chunks_total}`
|
||||
: "";
|
||||
return { main, count, chunk };
|
||||
const image =
|
||||
status.images_total > 0
|
||||
? `· image ${status.images_done}/${status.images_total}`
|
||||
: "";
|
||||
return { main, count, chunk, image };
|
||||
}
|
||||
|
||||
function fmtSyncResult(detail) {
|
||||
@@ -1011,7 +1050,10 @@ export async function mount(root) {
|
||||
* Phase 129 (task 04): the third, pinned CHUNK part ("· chunk n/m"
|
||||
* — the in-flight file's embed-batch position) is destructured the
|
||||
* same way (absent from the bare builders — the upload job and the
|
||||
* 202/409 click — so the "" default keeps the part hidden there).
|
||||
* 202/409 click — so the "" default keeps the part hidden there);
|
||||
* phase 152 (task 03) destructures the fourth, IMAGE part ("·
|
||||
* image n/m" — the in-flight PDF's vision-image position) the same
|
||||
* way (it too rides only the composed full string — no span).
|
||||
* Same never-stale mechanics as before (disabled, aria-busy,
|
||||
* spinning icon, no is-error). Phase 149 (task 01): the writer call
|
||||
* passes `running = true` — the detail box reveals with the full
|
||||
@@ -1025,13 +1067,15 @@ export async function mount(root) {
|
||||
if (!syncBtn) return;
|
||||
syncBtn.disabled = true;
|
||||
syncBtn.setAttribute("aria-busy", "true");
|
||||
const { main, count, chunk = "" } =
|
||||
const { main, count, chunk = "", image = "" } =
|
||||
kind === "sync" && status
|
||||
? fmtSyncPhaseLabel(status)
|
||||
: fmtSyncLabel(kind, currentFile, done, total);
|
||||
// the parts + the A4 untruncated title / announcer + the phase-149
|
||||
// detail box (revealed, full label) + the running aria-label
|
||||
setSyncLabel(main, count, chunk, true);
|
||||
// detail box (revealed, full label — phase 152: with the image
|
||||
// part when a PDF's vision span is in flight) + the running
|
||||
// aria-label
|
||||
setSyncLabel(main, count, chunk, true, image);
|
||||
syncBtn.classList.remove("is-error");
|
||||
if (syncIcon) syncIcon.classList.add("is-spinning");
|
||||
// Phase 149 (task 01): the icon-only running button — the class
|
||||
|
||||
+3
-2
@@ -483,8 +483,9 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- Phase 149 (task 01): the sync detail box — the full UNTRUNCATED live
|
||||
label ("Syncing… <source/rel/path> (n/m) · chunk n/m") in a bordered
|
||||
panel under the button row. Phase 150 (task 01) folded the
|
||||
label ("Syncing… <source/rel/path> (n/m) · chunk n/m" — phase 152
|
||||
appends "· image n/m" while a PDF's vision span is in flight)
|
||||
in a bordered panel under the button row. Phase 150 (task 01) folded the
|
||||
announcer in: the previous live-region span beside it is retired,
|
||||
and THIS box is now the SOLE sync text surface AND the
|
||||
SOLE aria-live region for the sync area
|
||||
|
||||
Reference in New Issue
Block a user