| 1 | "use client"; |
| 2 | |
| 3 | import type { HealthEngine } from "@/lib/types"; |
| 4 | |
| 5 | interface HealthPanelProps { |
| 6 | engines: HealthEngine[]; |
| 7 | ok: boolean; |
| 8 | } |
| 9 | |
| 10 | function stalenessLabel(h: number): string { |
| 11 | if (h < 1) return `${Math.round(h * 60)}m`; |
| 12 | if (h < 24) return `${h.toFixed(1)}h`; |
| 13 | return `${Math.round(h / 24)}d`; |
| 14 | } |
| 15 | |
| 16 | function stalenessClass(h: number, status: "up" | "down"): string { |
| 17 | if (status === "down") return "text-red-400"; |
| 18 | if (h < 2) return "text-teal-bio"; |
| 19 | if (h < 12) return "text-text-secondary"; |
| 20 | return "text-text-muted"; |
| 21 | } |
| 22 | |
| 23 | function EngineRow({ engine }: { engine: HealthEngine }) { |
| 24 | const isUp = engine.status === "up"; |
| 25 | const stale = engine.staleness_h > 12; |
| 26 | |
| 27 | return ( |
| 28 | <div className="flex items-center gap-3 py-2 border-b border-surface-2 last:border-0"> |
| 29 | {} |
| 30 | <div |
| 31 | className={[ |
| 32 | "w-2 h-2 rounded-full flex-shrink-0", |
| 33 | isUp && !stale ? "bg-teal-bio animate-pulse-slow" : "", |
| 34 | isUp && stale ? "bg-text-muted" : "", |
| 35 | !isUp ? "bg-red-500" : "", |
| 36 | ].join(" ")} |
| 37 | /> |
| 38 | |
| 39 | {} |
| 40 | <span |
| 41 | className={[ |
| 42 | "font-mono text-label-sm uppercase tracking-widest flex-1", |
| 43 | isUp ? "text-text-primary" : "text-text-muted", |
| 44 | ].join(" ")} |
| 45 | > |
| 46 | {engine.engine} |
| 47 | </span> |
| 48 | |
| 49 | {} |
| 50 | <span className={`font-mono text-label-sm ${stalenessClass(engine.staleness_h, engine.status)}`}> |
| 51 | {isUp ? stalenessLabel(engine.staleness_h) : "down"} |
| 52 | </span> |
| 53 | </div> |
| 54 | ); |
| 55 | } |
| 56 | |
| 57 | export default function HealthPanel({ engines, ok }: HealthPanelProps) { |
| 58 | const upCount = engines.filter((e) => e.status === "up").length; |
| 59 | const freshCount = engines.filter((e) => e.status === "up" && e.staleness_h < 2).length; |
| 60 | |
| 61 | return ( |
| 62 | <div className="border border-surface-2 p-4"> |
| 63 | {} |
| 64 | <div className="flex items-center justify-between mb-4"> |
| 65 | <div className="flex items-center gap-2"> |
| 66 | <div className={`w-2 h-2 rounded-full ${ok ? "bg-teal-bio animate-pulse-slow" : "bg-red-500"}`} /> |
| 67 | <span className="font-mono text-label-sm uppercase tracking-widest text-text-primary"> |
| 68 | Organism health |
| 69 | </span> |
| 70 | </div> |
| 71 | <span className="font-mono text-label-sm text-text-muted"> |
| 72 | {upCount}/{engines.length} up |
| 73 | </span> |
| 74 | </div> |
| 75 | |
| 76 | {} |
| 77 | <div> |
| 78 | {engines.map((eng) => ( |
| 79 | <EngineRow key={eng.engine} engine={eng} /> |
| 80 | ))} |
| 81 | </div> |
| 82 | |
| 83 | {} |
| 84 | <div className="mt-4 pt-3 border-t border-surface-2 flex justify-between items-center"> |
| 85 | <span className="font-mono text-label-sm text-text-muted"> |
| 86 | {freshCount} nodes fresh |
| 87 | </span> |
| 88 | <span className={`font-mono text-label-sm ${ok ? "text-teal-bio" : "text-red-400"}`}> |
| 89 | {ok ? "HEALTHY" : "DEGRADED"} |
| 90 | </span> |
| 91 | </div> |
| 92 | </div> |
| 93 | ); |
| 94 | } |