import { useState, useMemo, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router'; import { ExternalLink, RefreshCw, Pencil } from 'lucide-react'; import { StatCard, StatusDot, Badge, MonoText, GroupCard, DataTable, EventFeed, LogViewer, ButtonGroup, SectionHeader, Toggle, Select, Button, useToast, Alert, ConfirmDialog, } from '@cameleer/design-system'; import type { Column, FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system'; import styles from './AgentHealth.module.css'; import sectionStyles from '../../styles/section-card.module.css'; import logStyles from '../../styles/log-panel.module.css'; import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useApplicationLogs } from '../../api/queries/logs'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import { useCatalog, useDismissApp } from '../../api/queries/catalog'; import { useIsAdmin } from '../../auth/auth-store'; import { useEnvironmentStore } from '../../api/environment-store'; import type { ConfigUpdateResponse } from '../../api/queries/commands'; import type { AgentInstance } from '../../api/types'; import { timeAgo } from '../../utils/format-utils'; import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils'; // ── Helpers ────────────────────────────────────────────────────────────────── function formatErrorRate(rate?: number): string { if (rate == null) return '\u2014'; return `${(rate * 100).toFixed(1)}%`; } type NormStatus = 'live' | 'stale' | 'dead'; function normalizeStatus(status: string): NormStatus { return status.toLowerCase() as NormStatus; } function statusColor(s: NormStatus): 'success' | 'warning' | 'error' { if (s === 'live') return 'success'; if (s === 'stale') return 'warning'; return 'error'; } // ── Data grouping ──────────────────────────────────────────────────────────── interface AppGroup { appId: string; instances: AgentInstance[]; liveCount: number; staleCount: number; deadCount: number; totalTps: number; totalActiveRoutes: number; totalRoutes: number; } function groupByApp(agentList: AgentInstance[]): AppGroup[] { const map = new Map(); for (const a of agentList) { const app = a.applicationId; const list = map.get(app) ?? []; list.push(a); map.set(app, list); } return Array.from(map.entries()).map(([appId, instances]) => ({ appId, instances, liveCount: instances.filter((i) => normalizeStatus(i.status) === 'live').length, staleCount: instances.filter((i) => normalizeStatus(i.status) === 'stale').length, deadCount: instances.filter((i) => normalizeStatus(i.status) === 'dead').length, totalTps: instances.reduce((s, i) => s + (i.tps ?? 0), 0), totalActiveRoutes: instances.reduce((s, i) => s + (i.activeRoutes ?? 0), 0), totalRoutes: instances.reduce((s, i) => s + (i.totalRoutes ?? 0), 0), })); } function appHealth(group: AppGroup): 'success' | 'warning' | 'error' { if (group.deadCount > 0) return 'error'; if (group.staleCount > 0) return 'warning'; return 'success'; } // ── Detail sub-components ──────────────────────────────────────────────────── const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ { value: 'error', label: 'Error', color: 'var(--error)' }, { value: 'warn', label: 'Warn', color: 'var(--warning)' }, { value: 'info', label: 'Info', color: 'var(--success)' }, { value: 'debug', label: 'Debug', color: 'var(--running)' }, { value: 'trace', label: 'Trace', color: 'var(--text-muted)' }, ]; const LOG_SOURCE_ITEMS: ButtonGroupItem[] = [ { value: 'app', label: 'App' }, { value: 'agent', label: 'Agent' }, ]; // ── AgentHealth page ───────────────────────────────────────────────────────── export default function AgentHealth() { const { appId } = useParams(); const navigate = useNavigate(); const { toast } = useToast(); const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: agents } = useAgents(undefined, appId, selectedEnv); const { data: appConfig } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); const isAdmin = useIsAdmin(); const selectedEnvForCatalog = useEnvironmentStore((s) => s.environment); const { data: catalogApps } = useCatalog(selectedEnvForCatalog); const dismissApp = useDismissApp(); const catalogEntry = catalogApps?.find((a) => a.slug === appId); const [confirmDismissOpen, setConfirmDismissOpen] = useState(false); const [configEditing, setConfigEditing] = useState(false); const [configDraft, setConfigDraft] = useState>({}); const startConfigEdit = useCallback(() => { if (!appConfig) return; setConfigDraft({ applicationLogLevel: appConfig.applicationLogLevel ?? 'INFO', agentLogLevel: appConfig.agentLogLevel ?? 'INFO', engineLevel: appConfig.engineLevel ?? 'REGULAR', payloadCaptureMode: appConfig.payloadCaptureMode ?? 'NONE', metricsEnabled: appConfig.metricsEnabled, }); setConfigEditing(true); }, [appConfig]); const saveConfigEdit = useCallback(() => { if (!appConfig) return; const updated = { ...appConfig, ...configDraft }; updateConfig.mutate({ config: updated, environment: selectedEnv }, { onSuccess: (saved: ConfigUpdateResponse) => { setConfigEditing(false); setConfigDraft({}); if (saved.pushResult.success) { toast({ title: 'Config updated', description: `${appId} (v${saved.config.version}) — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents`, variant: 'success' }); } else { const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut]; toast({ title: 'Config updated — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 }); } }, onError: () => { toast({ title: 'Config update failed', variant: 'error', duration: 86_400_000 }); }, }); }, [appConfig, configDraft, updateConfig, toast, appId]); const [eventSortAsc, setEventSortAsc] = useState(false); const [eventRefreshTo, setEventRefreshTo] = useState(); const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo, selectedEnv); const [logSearch, setLogSearch] = useState(''); const [logLevels, setLogLevels] = useState>(new Set()); const [logSource, setLogSource] = useState(''); // '' = all, 'app', 'agent' const [logSortAsc, setLogSortAsc] = useState(false); const [logRefreshTo, setLogRefreshTo] = useState(); const { data: rawLogs } = useApplicationLogs(appId, undefined, { toOverride: logRefreshTo, source: logSource || undefined }); const logEntries = useMemo(() => { const mapped = (rawLogs || []).map((l) => ({ timestamp: l.timestamp ?? '', level: mapLogLevel(l.level), message: l.message ?? '', })); return logSortAsc ? mapped.toReversed() : mapped; }, [rawLogs, logSortAsc]); const logSearchLower = logSearch.toLowerCase(); const filteredLogs = logEntries .filter((l) => logLevels.size === 0 || logLevels.has(l.level)) .filter((l) => !logSearchLower || l.message.toLowerCase().includes(logSearchLower)); const agentList = agents ?? []; const groups = useMemo(() => groupByApp(agentList), [agentList]); // Aggregate stats const totalInstances = agentList.length; const liveCount = agentList.filter((a) => normalizeStatus(a.status) === 'live').length; const staleCount = agentList.filter((a) => normalizeStatus(a.status) === 'stale').length; const deadCount = agentList.filter((a) => normalizeStatus(a.status) === 'dead').length; const totalTps = agentList.reduce((s, a) => s + (a.tps ?? 0), 0); const totalActiveRoutes = agentList.reduce((s, a) => s + (a.activeRoutes ?? 0), 0); const totalRoutes = agentList.reduce((s, a) => s + (a.totalRoutes ?? 0), 0); // Map events to FeedEvent const feedEvents: FeedEvent[] = useMemo(() => { const mapped = (events ?? []).map((e: { id: number; instanceId: string; eventType: string; detail: string; timestamp: string }) => ({ id: String(e.id), severity: eventSeverity(e.eventType), icon: eventIcon(e.eventType), message: `${e.instanceId}: ${e.eventType}${e.detail ? ' \u2014 ' + e.detail : ''}`, timestamp: new Date(e.timestamp), })); return eventSortAsc ? mapped.toReversed() : mapped; }, [events, eventSortAsc]); // Column definitions for the instance DataTable const instanceColumns: Column[] = useMemo( () => [ { key: 'status', header: '', width: '12px', render: (_val, row) => , }, { key: '_inspect', header: '', width: '36px', render: (_val, row) => ( ), }, { key: 'name', header: 'Instance', render: (_val, row) => ( {row.displayName ?? row.instanceId} ), }, { key: 'state', header: 'State', render: (_val, row) => { const ns = normalizeStatus(row.status); return ; }, }, { key: 'uptime', header: 'Uptime', render: (_val, row) => ( {formatUptime(row.uptimeSeconds)} ), }, { key: 'tps', header: 'TPS', render: (_val, row) => ( {row.tps != null ? `${row.tps.toFixed(1)}/s` : '\u2014'} ), }, { key: 'errorRate', header: 'Errors', render: (_val, row) => ( {formatErrorRate(row.errorRate)} ), }, { key: 'lastHeartbeat', header: 'Heartbeat', render: (_val, row) => { const ns = normalizeStatus(row.status); return ( {timeAgo(row.lastHeartbeat)} ); }, }, ], [], ); function handleInstanceClick(inst: AgentInstance) { navigate(`/runtime/${inst.applicationId}/${inst.instanceId}`); } const isFullWidth = !!appId; return (
{/* No agents warning + dismiss — shown when app-scoped, no agents, admin */} {appId && agentList.length === 0 && isAdmin && ( <> {catalogEntry?.managed ? 'Managed app' : 'Discovered app'} — {(catalogEntry?.exchangeCount ?? 0).toLocaleString()} exchanges recorded. {' '}You can dismiss this application to remove it and all associated data.
setConfirmDismissOpen(false)} onConfirm={() => { setConfirmDismissOpen(false); dismissApp.mutate(appId, { onSuccess: () => { toast({ title: 'Application dismissed', description: `${appId} and all associated data have been deleted`, variant: 'success' }); navigate('/runtime'); }, onError: (err) => { toast({ title: 'Dismiss failed', description: err.message, variant: 'error', duration: 86_400_000 }); }, }); }} title="Dismiss Application" message={`This will permanently delete "${appId}" and all associated data (${(catalogEntry?.exchangeCount ?? 0).toLocaleString()} exchanges, logs, diagrams). This action cannot be undone.`} confirmText={appId} confirmLabel="Dismiss" variant="danger" loading={dismissApp.isPending} /> )} {/* Stat strip */}
0 ? 'warning' : 'amber'} detail={ {liveCount} live {staleCount} stale {deadCount} dead } /> {groups.filter((g) => g.deadCount === 0 && g.staleCount === 0).length} healthy {groups.filter((g) => g.staleCount > 0 && g.deadCount === 0).length} degraded {groups.filter((g) => g.deadCount > 0).length} critical } /> {totalActiveRoutes}/{totalRoutes} } accent={totalActiveRoutes === 0 ? 'error' : totalActiveRoutes < totalRoutes ? 'warning' : 'success'} detail={totalActiveRoutes < totalRoutes ? `${totalRoutes - totalActiveRoutes} suspended` : 'all routes active'} /> 0 ? 'error' : 'success'} detail={deadCount > 0 ? 'requires attention' : 'all healthy'} />
0 ? 'error' : staleCount > 0 ? 'warning' : 'success'} variant="filled" />
{/* Application config bar */} {appId && appConfig && (
{configEditing ? ( <>
App Log Level setConfigDraft(d => ({ ...d, agentLogLevel: e.target.value }))} options={[ { value: 'ERROR', label: 'ERROR' }, { value: 'WARN', label: 'WARN' }, { value: 'INFO', label: 'INFO' }, { value: 'DEBUG', label: 'DEBUG' }, { value: 'TRACE', label: 'TRACE' }, ]} />
Engine Level setConfigDraft(d => ({ ...d, payloadCaptureMode: e.target.value }))} options={[ { value: 'NONE', label: 'None' }, { value: 'INPUT', label: 'Input' }, { value: 'OUTPUT', label: 'Output' }, { value: 'BOTH', label: 'Both' }, ]} />
Metrics setConfigDraft(d => ({ ...d, metricsEnabled: (e.target as HTMLInputElement).checked }))} />
) : ( <>
App Log Level
Agent Log Level
Engine Level
Payload Capture
Metrics
)}
)} {/* Group cards grid */}
{groups.map((group) => ( } meta={
{group.totalTps.toFixed(1)} msg/s {group.totalActiveRoutes}/{group.totalRoutes} routes
} footer={ group.deadCount > 0 ? (
Single point of failure —{' '} {group.deadCount === group.instances.length ? 'no redundancy' : `${group.deadCount} dead instance${group.deadCount > 1 ? 's' : ''}`}
) : undefined } > columns={instanceColumns as Column[]} data={group.instances.map(i => ({ ...i, id: i.instanceId }))} onRowClick={handleInstanceClick} pageSize={50} flush />
))}
{/* Log + Timeline side by side */}
Application Log
{logEntries.length} entries
setLogSearch(e.target.value)} aria-label="Search logs" /> {logSearch && ( )}
setLogSource(v.size === 0 ? '' : [...v][0])} /> {logLevels.size > 0 && ( )}
{filteredLogs.length > 0 ? ( ) : (
{logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No log entries available'}
)}
Timeline
{feedEvents.length} events
{feedEvents.length > 0 ? ( ) : (
No events in the selected time range.
)}
); }