fix: dashboard — coverage pillar, active alerts env fallback

- Add coverage pillar to /api/health response and _serialize_health_score
- Add coverage to MeshHealth.pillars TypeScript interface
- Add Coverage PillarBar between Utilization and Behavior
- Active Alerts panel: show high-severity env events (immediate/priority)
  as fallback when mesh alerts are empty, with ENV badge

Issues 3 (Live Event Feed) and 4 (RF Propagation): diagnosed as
env feed configuration — SWPC adapter disabled, only ducting feed
loaded, /api/env/active returns 0 events. Not a code bug.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Johnson (via Claude) 2026-06-10 03:56:12 +00:00
commit 15f2b6c89a
6 changed files with 108 additions and 68 deletions

View file

@ -19,6 +19,7 @@ export interface MeshHealth {
pillars: {
infrastructure: number
utilization: number
coverage: number
behavior: number
power: number
}

View file

@ -696,6 +696,7 @@ export default function Dashboard() {
<div className="mt-6 space-y-3">
<PillarBar label="Infrastructure" value={health.pillars?.infrastructure ?? 0} />
<PillarBar label="Utilization" value={health.pillars?.utilization ?? 0} />
<PillarBar label="Coverage" value={health.pillars?.coverage ?? 0} />
<PillarBar label="Behavior" value={health.pillars?.behavior ?? 0} />
<PillarBar label="Power" value={health.pillars?.power ?? 0} />
</div>
@ -714,12 +715,48 @@ export default function Dashboard() {
<AlertItem key={i} alert={alert} />
))}
</div>
) : (
<div className="flex items-center gap-2 text-slate-500 py-4">
<CheckCircle size={16} className="text-green-500" />
<span>No active alerts</span>
</div>
)}
) : (() => {
const highSeverityEnv = envEvents
.filter(e => e.severity === 'immediate' || e.severity === 'priority')
.sort((a, b) => {
const ord: Record<string, number> = { immediate: 0, priority: 1 }
const diff = (ord[a.severity] ?? 2) - (ord[b.severity] ?? 2)
if (diff !== 0) return diff
return (b.fetched_at || 0) - (a.fetched_at || 0)
})
.slice(0, 5)
if (highSeverityEnv.length > 0) {
return (
<div className="space-y-3 max-h-48 overflow-y-auto">
{highSeverityEnv.map((ev, i) => {
const sevStyle = ev.severity === 'immediate'
? { bg: 'bg-red-500/10', border: 'border-red-500', icon: AlertCircle, iconColor: 'text-red-500' }
: { bg: 'bg-amber-500/10', border: 'border-amber-500', icon: AlertTriangle, iconColor: 'text-amber-500' }
const Icon = sevStyle.icon
return (
<div key={ev.event_id || i} className={`p-3 rounded-lg ${sevStyle.bg} border-l-2 ${sevStyle.border} flex items-start gap-3`}>
<Icon size={16} className={sevStyle.iconColor} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="px-1.5 py-0.5 rounded text-xs bg-slate-500/20 text-slate-400 border border-slate-500/30 font-mono">ENV</span>
<span className="text-xs text-slate-500">{ev.severity}</span>
</div>
<div className="text-sm text-slate-200 mt-1">{ev.headline}</div>
<div className="text-xs text-slate-500 mt-1">{ev.source} · {new Date(ev.fetched_at * 1000).toLocaleTimeString()}</div>
</div>
</div>
)
})}
</div>
)
}
return (
<div className="flex items-center gap-2 text-slate-500 py-4">
<CheckCircle size={16} className="text-green-500" />
<span>No active alerts</span>
</div>
)
})()}
</div>
{/* Quick Stats */}

View file

@ -15,6 +15,7 @@ def _serialize_health_score(score) -> dict:
"tier": score.tier,
"infrastructure": round(score.infrastructure, 1),
"utilization": round(score.utilization, 1),
"coverage": round(score.coverage, 1),
"behavior": round(score.behavior, 1),
"power": round(score.power, 1),
"infra_online": score.infra_online,
@ -73,6 +74,7 @@ async def get_health(request: Request):
"pillars": {
"infrastructure": round(score.infrastructure, 1),
"utilization": round(score.utilization, 1),
"coverage": round(score.coverage, 1),
"behavior": round(score.behavior, 1),
"power": round(score.power, 1),
},

View file

@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-CM6OazXs.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
<script type="module" crossorigin src="/assets/index-BCcZxs_h.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEgceSNC.css">
</head>
<body>
<div id="root"></div>