Fix: ship /api/plans/:id/activity endpoint
The bug: a437 (2026-08-03) promised this endpoint but the route was never built. SPA called it from 3 places (list mount-time chips, detail activity card, detail linked activity) and all 3 silently 404'd. Every plan card showed … loading forever on the state chip.
The fix (src/mavis-hq.js): shipped the endpoint with the 4-state machine from a437:
- `pre_coded` — no ticks yet
- `in_progress` — at least one step is status=in_progress
- `done` — meta.status=completed OR all steps done
- `paused` — no in_progress step AND no non-keep-alive heartbeat in 15+ min
The keep_alive carve-out: VPS-timer pings every 60s on every live thread. Without filtering, no plan would ever look paused. The endpoint now:
- queries `recentHqHeartbeats(thread_id, 50)`
- shows the most recent 5 (with keep_alive flag in response)
- computes last_hb_at from the FIRST non-keep_alive row only
- pause override fires on the real heartbeat gap
Response shape:
{ ok, plan_id, computed_state, state_explanation,
current_step, owner, recent_heartbeats,
last_finished_bulletin, paused }
Also fixed: SPA mount-time fetch for state chip ran BEFORE c.innerHTML was set, so chips didn't exist in DOM. Moved the forEach to after the innerHTML assignment.
Verification (Playwright iPhone 13, 390x844):
- 31 /api/plans/:id/activity calls all return 200
- 24 chips show ⚪ paused, 7 show 🟢 done
- a437 / a447 → 🟢 done
- a433 → ⚪ paused (1d ago, 7.6h since last real heartbeat)
- Detail page activity card renders state + explanation
Files touched:
- `src/mavis-hq.js` — 1 new endpoint, 1 keep_alive filter block (~80 lines)
- `public/mavis-hq/index.html` — moved state-chip forEach to after innerHTML (~10 line move)
Commits:
- `da8b94e fix(plans): ship /api/plans/:id/activity endpoint (404 → 200)`
Bulletins: