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

@@ -1000,6 +1000,7 @@ async def get_run_orders_filtered(run_id: str, status_filter: str = "all",
"error": status_counts.get(OrderStatus.ERROR.value, 0),
"already_imported": status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0),
"cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0),
"malformed": status_counts.get(OrderStatus.MALFORMED.value, 0),
"total": sum(status_counts.values())
}
}
@@ -1135,6 +1136,7 @@ async def get_orders(page: int = 1, per_page: int = 50,
"skipped": status_counts.get(OrderStatus.SKIPPED.value, 0),
"error": status_counts.get(OrderStatus.ERROR.value, 0),
"cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0),
"malformed": status_counts.get(OrderStatus.MALFORMED.value, 0),
"total": sum(status_counts.values()),
"uninvoiced_sqlite": uninvoiced_sqlite,
"uninvoiced_old": uninvoiced_old,