phase: 134_sync_cancel
Build and Push Containers / build-and-push-app (push) Successful in 2m26s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**Phase 134 final verification pass — all green** (all 3 task files already in `complete/`; no implementation was missing)

- Verified: 5-state `SyncStatus` + `cancelled` terminal (error None, final counts kept), `POST /api/sync/cancel` (202/409/403), cooperative `_check_cancelled` boundaries, shutdown `CancelledError` contract preserved, `#sync-cancel-btn` show/hide + neutral "Sync cancelled" settle, 8-branch poll tree, re-attach — code, unit, integration, and E2E layers all present
- `uv run pytest --cov=app --cov-report=term-missing` → **3150 passed**, `app/` coverage **99%** (gate: >90%)
- `uv run pytest tests/e2e/test_sync_cancel.py -v --no-cov` (isolated) → **3 passed** (47.6 s)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings

**Completion criteria:** ① cancel endpoint 202/409/403 ✅ (unit + E2E) ② `cancelled` terminal reported by status, error null, counts kept ✅ (integration `test_cancel_mid_run_lands_in_cancelled_then_resyncs_clean`) ③ unrequested-cancel pin ✅ (`test_unrequested_cancel_is_never_swallowed_and_never_converts`) ④ UI lifecycle + clean re-sync (sentinel row, full 40-file import) ✅ (E2E) ⑤ suite/coverage/E2E gates ✅ ⑥ uncancelled runs byte-identical ✅ (all 3150 pre-existing tests green untouched)

- Docs: README.md reviewed in full against code (pages, auth endpoints, every config default vs `app/config.py`, palette 9+8, fail-loud, cancel section) — consistent, no changes needed. AGENTS.md: fixed one pre-existing inaccuracy (rule 2 "move its file" → "move its directory" — phases are directories); everything else verified accurate
- No live infrastructure touched; no deviations

**Next pending phase:** `135_git_source_branch`
This commit is contained in:
2026-09-28 18:08:11 -04:00
parent 25de518dc9
commit fcac876d40
26 changed files with 2535 additions and 30 deletions
+36
View File
@@ -2130,6 +2130,41 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
border-color: var(--err-line);
}
.sync-btn.is-error:hover { background: var(--err-bg); color: var(--err-ink); }
/* Phase 134 (task 02): the Cancel control — the in-flight sync run's
cooperative stop (visible ONLY while a SYNC run is in flight; the
upload job is not cancellable in this phase — sources.js owns the
show/hide, the same never-stale contract as the main button). The
SECONDARY pill language of the house button family (the
.kb-summary-cancel pair): transparent fill, 1px --line border,
--ink-soft text (5.1:1 on --surface, AA), the global 3px
:focus-visible ring, the >=44px touch floor. :disabled = the cancel
request in flight (one request at a time — never stale); the hover
is guarded on :not(:disabled) so a disabled button under the cursor
never reads as actionable. The [hidden] rule keeps the attribute
authoritative over the display above (the .kb-summary-edit pattern).
No layout regression on the sync-control row: the row already
flexes with a 1rem gap (desktop) and wraps (mobile, where the pill
goes full-width like the main button — the 360px case). */
.sync-cancel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
font: inherit;
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
}
.sync-cancel-btn:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
.sync-cancel-btn:disabled { opacity: 0.6; cursor: default; } /* the cancel request is in flight */
.sync-cancel-btn[hidden] { display: none; } /* the hidden attr must beat the display above */
.sync-icon { width: 16px; height: 16px; display: block; flex: 0 0 auto; }
.sync-btn .sync-icon.is-spinning { animation: spin 1s linear infinite; }
@media (prefers-reduced-motion: reduce) {
@@ -5421,6 +5456,7 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
the accessible names. */
.page-head-row { flex-wrap: wrap; }
.sync-btn { width: 100%; }
.sync-cancel-btn { width: 100%; } /* phase 134: the Cancel pill stacks full-width under the Sync pill (the row already wraps) */
.sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }
.history-refresh { width: 100%; }
.history-refresh svg { display: block; }