Skip to content

Commit 059b8ed

Browse files
committed
Add Health probes tab + fleet overview chip (#17)
- New HealthProbes component: per-agent CRUD with HTTP/TCP probes. States rendered as green/red/dashed icons next to each row. - New "Health" tab in the per-agent navigation. - FleetOverview gains a per-host probe chip showing N green / M total, polled from /api/health-probes/snapshot every 10s.
1 parent f472ba4 commit 059b8ed

4 files changed

Lines changed: 435 additions & 6 deletions

File tree

src/app/page.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Containers from '@/components/Containers';
1313
import AptManager from '@/components/AptManager';
1414
import FleetOverview from '@/components/FleetOverview';
1515
import Deploy from '@/components/Deploy';
16+
import HealthProbes from '@/components/HealthProbes';
1617
import {
1718
LayoutDashboardIcon,
1819
FileCode2Icon,
@@ -26,11 +27,12 @@ import {
2627
PackageIcon,
2728
RocketIcon,
2829
ActivityIcon,
30+
ActivitySquareIcon,
2931
} from 'lucide-react';
3032

31-
type Tab = 'dashboard' | 'containers' | 'deploy' | 'updates' | 'config';
33+
type Tab = 'dashboard' | 'containers' | 'deploy' | 'updates' | 'health' | 'config';
3234

33-
const TABS: Tab[] = ['dashboard', 'containers', 'deploy', 'updates', 'config'];
35+
const TABS: Tab[] = ['dashboard', 'containers', 'deploy', 'updates', 'health', 'config'];
3436

3537
export default function Home() {
3638
return (
@@ -250,6 +252,12 @@ function HomeBody() {
250252
icon={<PackageIcon className="w-4 h-4 mr-2" />}
251253
label="Updates"
252254
/>
255+
<TabButton
256+
active={activeTab === 'health'}
257+
onClick={() => setActiveTab('health')}
258+
icon={<ActivitySquareIcon className="w-4 h-4 mr-2" />}
259+
label="Health"
260+
/>
253261
<TabButton
254262
active={activeTab === 'config'}
255263
onClick={() => setActiveTab('config')}
@@ -286,6 +294,10 @@ function HomeBody() {
286294
<div className="flex-1 overflow-y-auto p-6 max-w-4xl mx-auto w-full">
287295
<AptManager agentId={selectedAgent} />
288296
</div>
297+
) : activeTab === 'health' ? (
298+
<div className="flex-1 overflow-y-auto p-6 max-w-4xl mx-auto w-full">
299+
<HealthProbes agentId={selectedAgent} />
300+
</div>
289301
) : (
290302
<div className="flex-1 overflow-hidden">
291303
<ConfigEditor agentId={selectedAgent} />

src/components/FleetOverview.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client';
22

3-
import { useMemo, useState } from 'react';
3+
import { useEffect, useMemo, useState } from 'react';
44
import { useWebSocket } from './providers/WebSocketProvider';
55
import { useFleetSnapshots, AgentSnapshot } from './providers/FleetSnapshotsProvider';
6+
import type { HealthSnapshotRow } from '@/lib/types';
67
import {
78
ServerIcon,
89
CpuIcon,
@@ -13,6 +14,7 @@ import {
1314
RefreshCwIcon,
1415
SearchIcon,
1516
XIcon,
17+
ActivitySquareIcon,
1618
} from 'lucide-react';
1719

1820
function formatBytes(kib: number): string {
@@ -45,6 +47,30 @@ export default function FleetOverview({
4547
const { agents } = useWebSocket();
4648
const { snapshots, refresh } = useFleetSnapshots();
4749
const [search, setSearch] = useState('');
50+
const [healthByAgent, setHealthByAgent] = useState<Record<string, HealthSnapshotRow>>({});
51+
52+
useEffect(() => {
53+
let cancelled = false;
54+
const load = async () => {
55+
try {
56+
const res = await fetch('/api/health-probes/snapshot', { credentials: 'include' });
57+
if (!res.ok) return;
58+
const rows: HealthSnapshotRow[] = await res.json();
59+
if (cancelled) return;
60+
const map: Record<string, HealthSnapshotRow> = {};
61+
for (const r of rows) map[r.agent_id] = r;
62+
setHealthByAgent(map);
63+
} catch {
64+
/* ignore — chip just won't render */
65+
}
66+
};
67+
void load();
68+
const t = setInterval(load, 10_000);
69+
return () => {
70+
cancelled = true;
71+
clearInterval(t);
72+
};
73+
}, []);
4874

4975
const totals = useMemo(() => {
5076
let cpu = 0;
@@ -231,7 +257,10 @@ export default function FleetOverview({
231257
onClick={() => onSelectAgent?.(agentId)}
232258
className="w-full text-left bg-slate-900 border border-slate-800 hover:border-slate-700 rounded-md px-4 py-3 transition-colors"
233259
>
234-
<HostRow snapshot={snap ?? { agentId, hostname: agentId.replace(/-id$/, '') }} />
260+
<HostRow
261+
snapshot={snap ?? { agentId, hostname: agentId.replace(/-id$/, '') }}
262+
health={healthByAgent[agentId]}
263+
/>
235264
</button>
236265
</li>
237266
);
@@ -359,7 +388,13 @@ function SmallStat({
359388
);
360389
}
361390

362-
function HostRow({ snapshot }: { snapshot: AgentSnapshot }) {
391+
function HostRow({
392+
snapshot,
393+
health,
394+
}: {
395+
snapshot: AgentSnapshot;
396+
health?: HealthSnapshotRow;
397+
}) {
363398
const stats = snapshot.stats;
364399
const services = snapshot.services;
365400
const docker = snapshot.docker;
@@ -414,6 +449,21 @@ function HostRow({ snapshot }: { snapshot: AgentSnapshot }) {
414449
{failed} failed
415450
</span>
416451
)}
452+
{health && health.total > 0 && (
453+
<span
454+
className={`inline-flex items-center gap-1 text-xs rounded-full px-2 py-0.5 border ${
455+
health.red > 0
456+
? 'text-red-300 bg-red-500/10 border-red-500/30'
457+
: health.unknown > 0
458+
? 'text-slate-300 bg-slate-700/30 border-slate-600/40'
459+
: 'text-emerald-300 bg-emerald-500/10 border-emerald-500/30'
460+
}`}
461+
title={`${health.green} green / ${health.red} red / ${health.unknown} pending`}
462+
>
463+
<ActivitySquareIcon className="w-3 h-3" />
464+
{health.green}/{health.total}
465+
</span>
466+
)}
417467
<Field
418468
label="Containers"
419469
value={

0 commit comments

Comments
 (0)