# Live Sync Updates (HTMX) — Design Date: 2026-08-16 Status: Draft for user review ## 1. Goal Replace the full-page navigation that currently happens after clicking "Sync now" / "Sync all now" with an in-place update: the affected rider row(s) refresh with their new status, and a short toast reports the outcome — without leaving the dashboard or account page. ## 2. Scope ### In scope - Vendoring htmx (v2.0.10, self-hosted, no CDN) as the swap mechanism. - Dashboard: per-rider "Sync now" and "Sync all now". - Account page (self-service): "Sync now". - A toast notification system (one at a time, auto-dismissing) built on htmx out-of-band swaps. - Removing the HTTP 409 special case for "sync already running" — it becomes a normal toast instead of a distinct error page/status. ### Out of scope (unchanged in this pass) - Activity retry button (`/activities/{id}/retry`) — still navigates to the old `fragments/sync_result.html` page. - Garmin MFA form (`/users/{id}/garmin-mfa`) — still navigates to the old page; MFA failure often needs a fresh code anyway, so the extra step is less costly there. - Live-updating the "Recent sync runs" table on the account/user detail pages — a completed sync's new row only appears after the next full page load. - Any change to `SyncManager`, `SyncOutcome`, or scheduler behavior. ## 3. Architecture ### htmx `app/web/static/htmx.min.js` (vendored, v2.0.10) is loaded in `base.html` via ``, alongside the existing `app.js`. ### The "always return current state" rule Every htmx-driven POST route in scope returns two things in one response body: 1. The current, freshly-reloaded state of its own primary swap target (even on a no-op path like "sync already running", or on the `/sync-all` form itself, which always re-renders unchanged). This makes every swap safe/idempotent — the target is never replaced with nothing. 2. Exactly one out-of-band toast fragment (`fragments/toast.html`, `hx-swap-oob="true"` on `#toast-container`) describing what happened. `/sync-all` additionally emits one out-of-band row update (`fragments/user_row.html` rendered with `oob=True`) per rider whose outcome carries a known `user_id` — riders unaffected by that run (e.g. disabled) are left alone. ### Shared row partial `app/web/templates/fragments/user_row.html` renders one `