import { GaugeContainer, GaugeSvg, GaugeValue, VitalCard, VitalLabel, VitalsSection, } from '../styles' import { update } from '../update' interface AppMetrics { cpu: number mem: number disk: number } interface SystemMetrics { cpu: number ram: { used: number, total: number, percent: number } disk: { used: number, total: number, percent: number } apps: Record } const SEGMENTS = 19 const START_ANGLE = -225 const END_ANGLE = 45 const SWEEP = END_ANGLE - START_ANGLE const CX = 60 const CY = 60 const RADIUS = 44 const SEGMENT_GAP = 3 const SEGMENT_WIDTH = 8 const NEEDLE_LENGTH = 38 let _metrics: SystemMetrics = { cpu: 0, ram: { used: 0, total: 0, percent: 0 }, disk: { used: 0, total: 0, percent: 0 }, apps: {}, } let _source: EventSource | undefined const formatBytes = (bytes: number): string => { if (bytes < 1024) return `${bytes}B` if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K` if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}M` return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G` } function updateTooltips(apps: Record) { for (const [name, m] of Object.entries(apps)) { document.querySelectorAll(`[data-app="${name}"]`).forEach(dot => { const parent = dot.parentElement if (parent?.hasAttribute('data-tooltip')) { parent.setAttribute('data-tooltip', `${name}\n${'─'.repeat(name.length)}\nCPU ${m.cpu}%\nMEM ${formatBytes(m.mem)}\nDISK ${formatBytes(m.disk)}`) } }) } } const toRad = (deg: number) => (deg * Math.PI) / 180 const segmentColor = (i: number, total: number): string => { const t = i / (total - 1) if (t < 0.4) return '#4caf50' if (t < 0.6) return '#8bc34a' if (t < 0.75) return '#ffc107' if (t < 0.9) return '#ff9800' return '#f44336' } function Gauge({ value }: { value: number }) { const segmentSweep = SWEEP / SEGMENTS const activeSegments = Math.round((value / 100) * SEGMENTS) const segments = [] for (let i = 0; i < SEGMENTS; i++) { const startDeg = START_ANGLE + i * segmentSweep + SEGMENT_GAP / 2 const endDeg = START_ANGLE + (i + 1) * segmentSweep - SEGMENT_GAP / 2 const innerR = RADIUS - SEGMENT_WIDTH / 2 const outerR = RADIUS + SEGMENT_WIDTH / 2 const x1 = CX + outerR * Math.cos(toRad(startDeg)) const y1 = CY + outerR * Math.sin(toRad(startDeg)) const x2 = CX + outerR * Math.cos(toRad(endDeg)) const y2 = CY + outerR * Math.sin(toRad(endDeg)) const x3 = CX + innerR * Math.cos(toRad(endDeg)) const y3 = CY + innerR * Math.sin(toRad(endDeg)) const x4 = CX + innerR * Math.cos(toRad(startDeg)) const y4 = CY + innerR * Math.sin(toRad(startDeg)) const color = i < activeSegments ? segmentColor(i, SEGMENTS) : 'var(--colors-border)' segments.push( ) } // Needle const needleAngle = START_ANGLE + (value / 100) * SWEEP const nx = CX + NEEDLE_LENGTH * Math.cos(toRad(needleAngle)) const ny = CY + NEEDLE_LENGTH * Math.sin(toRad(needleAngle)) // Needle base width const perpAngle = needleAngle + 90 const bw = 3 const bx1 = CX + bw * Math.cos(toRad(perpAngle)) const by1 = CY + bw * Math.sin(toRad(perpAngle)) const bx2 = CX - bw * Math.cos(toRad(perpAngle)) const by2 = CY - bw * Math.sin(toRad(perpAngle)) return ( {segments} {value}% ) } function VitalsContent() { return ( <> CPU RAM Disk ) } export function initVitals() { if (_source) return _source = new EventSource('/api/system/metrics/stream') _source.onmessage = e => { try { _metrics = JSON.parse(e.data) update('#vitals', ) updateTooltips(_metrics.apps) } catch {} } } export function Vitals() { return ( ) }