# Live Sync Updates (HTMX) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. **Goal:** "Sync now" / "Sync all now" on the dashboard, and "Sync now" on the account page, update the affected rider row(s) or status block in place and show an auto-dismissing toast, instead of navigating to a separate result page. **Architecture:** Vendor htmx (self-hosted) as the swap mechanism. Extract the dashboard row and the account status block into reusable Jinja partials so the same markup renders both the initial page and the post-sync response. Routes always return their own primary target's current state plus zero-or-more out-of-band updates plus exactly one out-of-band toast, so every swap is safe even on a no-op path. **Tech Stack:** htmx v2.0.10 (vendored static file, no build step), plain CSS, a small addition to the existing vanilla `app.js`. **Spec:** `docs/superpowers/specs/2026-08-16-live-sync-updates-design.md` ## Global Constraints - No new Python dependencies; htmx is a single vendored static JS file (spec §3). - `fragments/sync_result.html`, the activity retry route, and the Garmin MFA route are untouched — out of scope (spec §2). - Every htmx POST route returns its own primary swap target's current state (never empty) plus exactly one toast; `/sync-all` additionally returns one OOB row per outcome with a resolvable `user_id` (spec §3). - `POST /users/{id}/sync` and `POST /account/sync` return HTTP 200 for the "already running" case now (previously 409) — existing tests for that behavior must be updated to match, per spec §2. - htmx's own swap/toast behavior has no meaningful Python-level test; it is verified manually via chrome-devtools, matching how the next-sync countdown was verified (spec §4). --- ### Task 1: `UserRepository.dashboard_row` **Files:** - Modify: `app/db/repositories.py` - Test: `tests/db/test_repositories.py` **Interfaces:** - Produces: `UserRepository.dashboard_row(user_id: int) -> UserDashboardRow | None`, used by Task 4 and Task 5's routes. - [x] **Step 1: Write the failing tests** Add to `tests/db/test_repositories.py`: ```python def test_dashboard_row_returns_row_for_known_user(user_repository) -> None: user = _make_user(user_repository, "Alex") row = user_repository.dashboard_row(user.id) assert row is not None assert row.id == user.id assert row.name == "Alex" def test_dashboard_row_returns_none_for_unknown_user(user_repository) -> None: row = user_repository.dashboard_row(999) assert row is None ``` - [x] **Step 2: Run to verify failure** Run: `.venv/Scripts/python -m pytest tests/db/test_repositories.py -k dashboard_row -v` Expected: both FAIL with `AttributeError: 'UserRepository' object has no attribute 'dashboard_row'`. - [x] **Step 3: Extract the shared row builder and add `dashboard_row`** In `app/db/repositories.py`, replace the body of `dashboard_rows` with a call to a new private helper, and add `dashboard_row`: ```python def dashboard_rows(self) -> list[UserDashboardRow]: return [self._build_dashboard_row(user) for user in self.list_all()] def dashboard_row(self, user_id: int) -> UserDashboardRow | None: user = self.get(user_id) if user is None: return None return self._build_dashboard_row(user) def _build_dashboard_row(self, user: SyncUser) -> UserDashboardRow: last_run = self.session.scalar( select(SyncRun).where(SyncRun.user_id == user.id).order_by(SyncRun.started_at.desc()).limit(1) ) last_activity = self.session.scalar( select(Activity).where(Activity.user_id == user.id).order_by(Activity.created_at.desc()).limit(1) ) return UserDashboardRow( id=user.id, name=user.name, enabled=user.enabled, health_state=user.health_state.value, action_reason=user.action_reason, last_sync_at=last_run.finished_at if last_run else None, last_activity_name=last_activity.activity_name if last_activity else None, last_activity_status=last_activity.status.value if last_activity else None, ) ``` This is a pure refactor of the existing `dashboard_rows` loop body — behavior for `dashboard_rows()` itself must not change. - [x] **Step 4: Run to verify pass** Run: `.venv/Scripts/python -m pytest tests/db/test_repositories.py -v` Expected: all tests pass, including the two new ones and the existing `dashboard_rows`-adjacent coverage (none currently exists directly, but nothing regresses). - [x] **Step 5: Commit** ```bash git add app/db/repositories.py tests/db/test_repositories.py git commit -m "Add UserRepository.dashboard_row for single-row refresh" ``` --- ### Task 2: Vendor htmx and wire up base.html + toast/loading CSS **Files:** - Create: `app/web/static/htmx.min.js` (vendored, v2.0.10) - Modify: `app/web/templates/base.html` - Modify: `app/web/static/style.css` - Test: none (static asset + markup/CSS; full suite re-run at the end of this task to confirm no regressions) **Interfaces:** - Produces: the `#toast-container` element and `.toast`/`.toast-success`/`.toast-danger`/`.toast-info` classes that Task 3 and Task 5's `fragments/toast.html` renders into; the `.htmx-request` dimming rule. - [x] **Step 1: Vendor htmx** Download `https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js` and save it verbatim as `app/web/static/htmx.min.js` (already fetched once this session — reuse that content; if re-fetching, confirm the response is the same v2.0.10 minified build before saving). - [x] **Step 2: Load htmx and add the toast container in `base.html`** ```html