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

@@ -137,6 +137,8 @@ async function loadRunOrders(runId, statusFilter, page) {
document.getElementById('countError').textContent = counts.error || 0;
const alreadyEl = document.getElementById('countAlreadyImported');
if (alreadyEl) alreadyEl.textContent = counts.already_imported || 0;
const malEl = document.getElementById('countMalformed');
if (malEl) malEl.textContent = counts.malformed || 0;
const tbody = document.getElementById('runOrdersBody');
const orders = data.orders || [];
@@ -238,7 +240,8 @@ async function loadRunOrders(runId, statusFilter, page) {
{ label: 'Imp.', count: counts.imported || 0, value: ORDER_STATUS.IMPORTED, active: currentFilter === ORDER_STATUS.IMPORTED, colorClass: 'fc-green' },
{ label: 'Deja', count: counts.already_imported || 0, value: ORDER_STATUS.ALREADY_IMPORTED, active: currentFilter === ORDER_STATUS.ALREADY_IMPORTED, colorClass: 'fc-blue' },
{ label: 'Omise', count: counts.skipped || 0, value: ORDER_STATUS.SKIPPED, active: currentFilter === ORDER_STATUS.SKIPPED, colorClass: 'fc-yellow' },
{ label: 'Erori', count: counts.error || 0, value: ORDER_STATUS.ERROR, active: currentFilter === ORDER_STATUS.ERROR, colorClass: 'fc-red' }
{ label: 'Erori', count: counts.error || 0, value: ORDER_STATUS.ERROR, active: currentFilter === ORDER_STATUS.ERROR, colorClass: 'fc-red' },
{ label: 'Defecte', count: counts.malformed || 0, value: ORDER_STATUS.MALFORMED, active: currentFilter === ORDER_STATUS.MALFORMED, colorClass: 'fc-orange' }
], (val) => filterOrders(val));
// Orders pagination