feat(sync): /api/sync/health endpoint + dashboard health pill + MALFORMED UI

Backend:
- GET /api/sync/health returns {last_sync_at, last_sync_status,
  last_halt_reason, recent_phase_failures, escalation_phase, is_healthy}.
  healthy when last run was completed (or none yet), no phase has
  tripped the 3-in-a-row escalation, and recent failures <= 1.
- Dashboard + run-level endpoints include `malformed` count so the
  Defecte pill can render.

Frontend:
- Health pill in .sync-card-controls with three states — healthy
  (success green, check icon), warning (amber, triangle), escalated
  (error red, x-octagon + glow). Tooltip exposes the halt reason and
  the top phases with recent failures.
- Status-dot + badge add MALFORMED treatment via --compare orange,
  distinct from ERROR red. DESIGN.md notes the diagnostic rationale
  (ERROR = runtime, MALFORMED = payload source issue).
- Defecte filter pill on dashboard + logs pages. Mobile segmented
  control includes Defecte count. Counts wired to the malformed key.
- startSync() shows a native confirm modal when state is
  halted_escalation — operator override still possible, not silenced.
- ORDER_STATUS.MALFORMED mirror added to shared.js.
- Cache-bust: style.css v46, shared.js v47, dashboard.js v52,
  logs.js v16.

5 endpoint tests cover empty state, completed, failed, escalated,
single-failure warning. Full CI: 257 unit + 33 e2e green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-22 09:14:46 +00:00
parent 41b142effb
commit bb6f3a3b87
11 changed files with 290 additions and 5 deletions

View File

@@ -160,6 +160,52 @@ async def sync_status():
return result
@router.get("/api/sync/health")
async def sync_health():
"""Aggregated sync health snapshot used by the dashboard pill.
Fields:
last_sync_at ISO timestamp of most recent run start (or null).
last_sync_status completed | failed | running | halted_escalation | null.
last_halt_reason error_message from that run (only populated on
failed / halted_escalation).
recent_phase_failures {phase: count} across the last 3 runs.
escalation_phase the phase that tripped the 3-in-a-row halt, or null.
is_healthy completed last + <=1 recent phase failure.
"""
db = await sqlite_service.get_sqlite()
try:
cursor = await db.execute(
"SELECT run_id, started_at, status, error_message "
"FROM sync_runs ORDER BY started_at DESC LIMIT 1"
)
last_row = await cursor.fetchone()
finally:
await db.close()
last = dict(last_row) if last_row else {}
last_status = last.get("status")
halt_reason = last.get("error_message") if last_status in ("failed", "halted_escalation") else None
counts = await sqlite_service.get_recent_phase_failures(limit=3)
escalation_phase = next((p for p, c in counts.items() if c >= 3), None)
is_healthy = (
last_status in (None, "completed")
and escalation_phase is None
and sum(counts.values()) <= 1
)
return {
"last_sync_at": last.get("started_at"),
"last_sync_status": last_status,
"last_halt_reason": halt_reason,
"recent_phase_failures": counts,
"escalation_phase": escalation_phase,
"is_healthy": is_healthy,
}
@router.get("/api/sync/history")
async def sync_history(page: int = 1, per_page: int = 20):
"""Get sync run history."""