'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, Trash2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { formatDateTime } from '@/lib/utils'; import type { NodeHealthEvent, NodeItem, NodeSystemProfile, } from '@/lib/services/openflare'; import { NodeService } from '@/lib/services/openflare'; import { CapacityTrendChart } from '../../components/dashboard/capacity-trend-chart'; import { TrafficTrendChart } from '../../components/dashboard/traffic-trend-chart'; import { DiskIOTrendChart } from './disk-io-trend-chart'; import { DistributionList } from './distribution-list'; import { NetworkTrendChart } from './network-trend-chart'; import { NodeStatusBadge } from './node-status-badge'; import { formatBytes, formatMetricCount, formatPercent, formatRelativeTime, formatUptime, formatUsageRatio, getErrorMessage, getHealthEventLabel, getHealthEventTone, getNodeStatusLabel, getNodeStatusTone, getOpenrestyStatusLabel, getOpenrestyStatusTone, isMeaningfulTime, } from './node-utils'; type HealthEventFilter = 'all' | 'active' | 'resolved'; type NodeObservabilityVariant = 'edge' | 'compact'; function MetricBar({ label, value, progress, hint, }: { label: string; value: string; progress?: number | null; hint?: string; }) { return (

{label}

{hint ? (

{hint}

) : null}

{value}

{progress !== null && progress !== undefined ? (
) : null}
); } function SummaryStat({ label, value, hint, }: { label: string; value: string; hint: string; }) { return ( {label} {value}

{hint}

); } function SystemProfileCard({ profile, nodeName, }: { profile: NodeSystemProfile; nodeName: string; }) { const t = useTranslations('nodes'); return (

{t('obs.hostname')}

{profile.hostname || nodeName}

{t('obs.os')}

{profile.os_name || 'unknown'} {profile.os_version ? ` ${profile.os_version}` : ''}

{t('obs.kernelArch')}

{profile.kernel_version || 'unknown'} ·{' '} {profile.architecture || 'unknown'}

{t('obs.uptime')}

{formatUptime(profile.uptime_seconds, t)}

{t('obs.cpu')}

{profile.cpu_model || 'unknown'}

{t('obs.cpuCores', { count: profile.cpu_cores || 0 })}

{t('obs.totalMemory')}

{formatBytes(profile.total_memory_bytes)}

{t('obs.totalStorage')}

{formatBytes(profile.total_disk_bytes)}

{t('obs.reportedAt')}

{isMeaningfulTime(profile.reported_at) ? formatDateTime(profile.reported_at) : '—'}

); } function HealthEventTimeline({ events, allEventsCount, healthEventFilter, onFilterChange, onCleanup, cleanupPending, }: { events: NodeHealthEvent[]; allEventsCount: number; healthEventFilter: HealthEventFilter; onFilterChange: (filter: HealthEventFilter) => void; onCleanup: () => void; cleanupPending: boolean; }) { const t = useTranslations('nodes'); return (
{t('obs.eventsTitle')} {t('obs.eventsDesc')}
{allEventsCount > 0 ? ( <>
{( [ ['all', t('obs.filterAll')], ['active', t('obs.filterActive')], ['resolved', t('obs.filterResolved')], ] as const ).map(([filter, label]) => ( ))}
{events.length === 0 ? (

{t('obs.filterEmpty')}

) : null} {events.slice(0, 8).map((event) => (

{event.message || t('obs.noMessage')}

{t('obs.firstTriggered')} {isMeaningfulTime(event.first_triggered_at) ? ` ${formatDateTime(event.first_triggered_at)}` : ' —'}

{t('obs.lastTriggered')} {isMeaningfulTime(event.last_triggered_at) ? ` ${formatDateTime(event.last_triggered_at)}` : ' —'}

{t('obs.resolvedAt')} {isMeaningfulTime(event.resolved_at) && event.resolved_at ? ` ${formatDateTime(event.resolved_at)}` : ' —'}

))} ) : (

{t('obs.noEvents')}

)}
); } export function NodeObservability({ nodeId, node, variant = 'edge', connectionHint, }: { nodeId: number; node?: NodeItem; variant?: NodeObservabilityVariant; connectionHint?: string; }) { const t = useTranslations('nodes'); const tc = useTranslations('common'); const resolvedConnectionHint = connectionHint ?? t('edge.connectionHint'); const queryClient = useQueryClient(); const [healthEventFilter, setHealthEventFilter] = useState('all'); const [cleanupOpen, setCleanupOpen] = useState(false); const observabilityQuery = useQuery({ queryKey: ['openflare', 'node-observability', nodeId], queryFn: () => NodeService.getObservability(nodeId, { hours: 24, limit: 48 }), refetchInterval: 30000, }); const cleanupMutation = useMutation({ mutationFn: () => NodeService.cleanupHealthEvents(nodeId), onSuccess: async (result) => { toast.success( result.deleted_count > 0 ? t('obs.cleaned', { count: result.deleted_count }) : t('obs.nothingToClean'), ); setCleanupOpen(false); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['openflare', 'node-observability', nodeId], }), queryClient.invalidateQueries({ queryKey: ['openflare', 'dashboard', 'overview'], }), ]); }, onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))), }); const observability = observabilityQuery.data ?? null; const profile = observability?.profile ?? null; const latestMetric = observability?.metric_snapshots?.[0] ?? null; const activeHealthEvents = useMemo( () => observability?.health_events.filter( (event) => event.status === 'active', ) ?? [], [observability?.health_events], ); const resolvedHealthEvents = useMemo( () => observability?.health_events.filter( (event) => event.status === 'resolved', ) ?? [], [observability?.health_events], ); const filteredHealthEvents = useMemo(() => { switch (healthEventFilter) { case 'active': return activeHealthEvents; case 'resolved': return resolvedHealthEvents; default: return observability?.health_events ?? []; } }, [ activeHealthEvents, healthEventFilter, observability?.health_events, resolvedHealthEvents, ]); const trafficSummary = observability?.analytics?.traffic ?? null; const healthSummary = observability?.analytics?.health ?? null; const distributions = observability?.analytics?.distributions; const statusCodeDistribution = useMemo( () => (distributions?.status_codes ?? []).map((item) => ({ label: item.key, value: item.value, })), [distributions?.status_codes], ); const topDomains = useMemo( () => (distributions?.top_domains ?? []).map((item) => ({ label: item.key, value: item.value, })), [distributions?.top_domains], ); const topSourceCountry = distributions?.source_countries?.[0] ?? null; const latestHealthEvent = activeHealthEvents[0] ?? null; const dominantStatusCode = statusCodeDistribution[0] ?? null; const dominantDomain = topDomains[0] ?? null; const memoryUsageRatio = formatUsageRatio( latestMetric?.memory_used_bytes, latestMetric?.memory_total_bytes, ); const storageUsageRatio = formatUsageRatio( latestMetric?.storage_used_bytes, latestMetric?.storage_total_bytes, ); const trends = observability?.trends; const nodeName = node?.name ?? observability?.node_id ?? String(nodeId); if (observabilityQuery.isLoading) { return ( {t('obs.loading')} ); } if (observabilityQuery.isError) { return (

{getErrorMessage(observabilityQuery.error, t('requestFailed'))}

); } if (variant === 'compact') { return (
{t('obs.profileTitle')} {t('obs.profileDesc')} {profile ? ( ) : (

{t('obs.noProfile')}

)}
{t('obs.snapshotTitle')} {t('obs.snapshotDesc')} {latestMetric ? (
) : (

{t('obs.noSnapshot')}

)}
setCleanupOpen(true)} cleanupPending={cleanupMutation.isPending} /> {t('obs.cleanupTitle')} {t('obs.cleanupDesc')} {tc('cancel')} cleanupMutation.mutate()} > {cleanupMutation.isPending ? t('obs.cleaning') : t('obs.confirmCleanup')}
); } return (
{t('obs.systemInfo')} {profile ? ( ) : (

{t('obs.noProfileLong')}

)}
{t('obs.realtimeResources')} {latestMetric ? (
) : (

{t('obs.noSnapshotLong')}

)}
{t('obs.networkTraffic')} {latestMetric ? ( <> {node ? (
) : null}

{t('obs.openrestyConn')}

{t('obs.currentConn')} {latestMetric.openresty_connections ?? 0}

{t('obs.throughputHint')}

{t('obs.windowVisitors')}

{formatMetricCount(trafficSummary?.unique_visitor_count)}

{trafficSummary ? t('obs.reqQps', { requests: formatMetricCount( trafficSummary.request_count, ), qps: trafficSummary.estimated_qps.toFixed(2), }) : t('obs.noTrafficSummary')}

{t('obs.windowErrors')}

{formatMetricCount(trafficSummary?.error_count)}

{trafficSummary ? t('obs.errorRate', { rate: trafficSummary.error_rate_percent.toFixed(1), }) : t('obs.noErrorSummary')}

) : (

{t('obs.noNetworkSnapshot')}

)}
{t('obs.structureTitle')} {t('obs.structureDesc')}

{t('obs.mainStatus')}

{dominantStatusCode?.label ?? '—'}

{dominantStatusCode ? t('obs.times', { count: dominantStatusCode.value }) : t('obs.noStatusDist')}

{t('obs.topDomain')}

{dominantDomain?.label ?? '—'}

{dominantDomain ? t('obs.times', { count: dominantDomain.value }) : t('obs.noDomainDist')}

{t('obs.resolvedEvents')}

{resolvedHealthEvents.length}

{t('obs.resolvedHint')}

{t('obs.statusDist')}

{t('obs.topDomain')}

setCleanupOpen(true)} cleanupPending={cleanupMutation.isPending} />
{t('obs.cleanupTitle')} {t('obs.cleanupDesc')} {tc('cancel')} cleanupMutation.mutate()} > {cleanupMutation.isPending ? t('obs.cleaning') : t('obs.confirmCleanup')}
); }