diff --git a/src/apps/metrics-systems/components/MetricsDashboard.tsx b/src/apps/metrics-systems/components/MetricsDashboard.tsx index bb1984d..d0782a9 100644 --- a/src/apps/metrics-systems/components/MetricsDashboard.tsx +++ b/src/apps/metrics-systems/components/MetricsDashboard.tsx @@ -64,10 +64,27 @@ interface ContainerMetrics { containers: ContainerStats[] } +interface MicrogptScalar { + timestamp: string + requests: { + rate_per_sec: number + generate_total: number + chat_total: number + success_count_5m: number + failure_count_5m: number + } + inference: { + tokens_generated_total: number + tokens_per_second_avg: number + avg_duration_ms: number + conversation_total: number + } +} + interface MetricsDashboardProps { onConnectionStateChange: (status: 'connecting' | 'connected' | 'disconnected' | 'failed') => void - activeTab?: 'system' | 'containers' | 'portrait' - onTabChange?: (tab: 'system' | 'containers' | 'portrait') => void + activeTab?: 'system' | 'containers' | 'portrait' | 'microgpt' + onTabChange?: (tab: 'system' | 'containers' | 'portrait' | 'microgpt') => void } const formatBytes = (bytes: number) => { @@ -137,6 +154,8 @@ const MetricsDashboard = ({ onConnectionStateChange, activeTab = 'system', onTab const [containerMetrics, setContainerMetrics] = useState(null) const [containerTimeseries, setContainerTimeseries] = useState(null) const [portraitTimeseries, setPortraitTimeseries] = useState(null) + const [microgptScalar, setMicrogptScalar] = useState(null) + const [microgptTimeseries, setMicrogptTimeseries] = useState(null) const [timeRange, setTimeRange] = useState<'30m' | '1d' | '7d'>('1d') const [lastUpdate, setLastUpdate] = useState(null) @@ -218,6 +237,32 @@ const MetricsDashboard = ({ onConnectionStateChange, activeTab = 'system', onTab // Silently handle error - UI will show "no data" state } + // Fetch microgpt scalar + try { + const microgptScalarResponse = await fetch(`${apiUrl}/scalar/microgpt`) + if (microgptScalarResponse.ok) { + const text = await microgptScalarResponse.text() + if (text.trim()) { + setMicrogptScalar(JSON.parse(text)) + } + } + } catch { + // Silently handle error - UI will show "no data" state + } + + // Fetch microgpt timeseries + try { + const microgptTimeseriesResponse = await fetch(`${apiUrl}/timeseries/microgpt/${timeRange}`) + if (microgptTimeseriesResponse.ok) { + const text = await microgptTimeseriesResponse.text() + if (text.trim()) { + setMicrogptTimeseries(JSON.parse(text)) + } + } + } catch { + // Silently handle error - UI will show "no data" state + } + setLastUpdate(new Date()) onConnectionStateChange('connected') } catch { @@ -539,6 +584,12 @@ const MetricsDashboard = ({ onConnectionStateChange, activeTab = 'system', onTab > Portrait Metrics + {/* Tab Content */} @@ -1083,6 +1134,210 @@ const MetricsDashboard = ({ onConnectionStateChange, activeTab = 'system', onTab )} + {activeTab === 'microgpt' && ( + <> + {/* Overview cards */} + {microgptScalar && ( +
+
+
Req/s
+
{(microgptScalar.requests.rate_per_sec || 0).toFixed(2)}
+
+
+
Tokens/s
+
{(microgptScalar.inference.tokens_per_second_avg || 0).toFixed(1)}
+
+
+
Chats
+
{(microgptScalar.inference.conversation_total || 0).toFixed(0)}
+
+
+
Avg ms
+
{(microgptScalar.inference.avg_duration_ms || 0).toFixed(0)}
+
+
+ )} + + {/* Inference Activity */} +
+

Inference Activity

+
+ {/* Tokens per Second */} +
+

Tokens / Second

+ {(() => { + const series = microgptTimeseries?.series?.find(s => s.metric_name === 'tokens_per_second') + if (!series?.values?.length) return + const data = series.values.map(v => ({ time: formatTimestamp(v.timestamp), value: Math.max(0, v.value || 0) })) + return ( + + + + + + + [`${Number(value).toFixed(1)} tok/s`, 'Tokens/s']} + /> + + + + + ) + })()} +
+ + {/* Request Rate */} +
+

Request Rate

+ {(() => { + const series = microgptTimeseries?.series?.find(s => s.metric_name === 'request_rate') + if (!series?.values?.length) return + const data = series.values.map(v => ({ time: formatTimestamp(v.timestamp), value: Math.max(0, v.value || 0) })) + return ( + + + + + + + [`${Number(value).toFixed(3)} req/s`, 'Request Rate']} + /> + + + + + ) + })()} +
+ + {/* Average Request Duration */} +
+

Avg Request Duration

+ {(() => { + const series = microgptTimeseries?.series?.find(s => s.metric_name === 'request_duration_ms') + if (!series?.values?.length) return + const data = series.values.map(v => ({ time: formatTimestamp(v.timestamp), value: Math.max(0, v.value || 0) })) + return ( + + + + + + `${v.toFixed(0)}ms`} /> + [`${Number(value).toFixed(0)} ms`, 'Avg Duration']} + /> + + + + + ) + })()} +
+ + {/* Conversation Rate */} +
+

Chat Conversation Rate

+ {(() => { + const series = microgptTimeseries?.series?.find(s => s.metric_name === 'conversation_rate') + if (!series?.values?.length) return + const data = series.values.map(v => ({ time: formatTimestamp(v.timestamp), value: Math.max(0, v.value || 0) })) + return ( + + + + + + + [`${Number(value).toFixed(3)} conv/s`, 'Conversation Rate']} + /> + + + + + ) + })()} +
+
+
+ + {/* Health */} +
+

Service Health

+
+ {/* Success Count */} +
+

Success Count (5m windows)

+ + + s.metric_name === 'success_count'), 0)}> + + + + [`${Number(value).toFixed(0)}`, 'Successes']} + /> + + + + +
+ + {/* Failure Count */} +
+

Failure Count (5m windows)

+ + + s.metric_name === 'failure_count'), 0)}> + + + + [`${Number(value).toFixed(0)}`, 'Failures']} + /> + + + + +
+ + {/* Endpoint breakdown (current totals) */} + {microgptScalar && ( +
+

Endpoint Totals

+ + + + + + + [`${Number(value).toFixed(0)}`, 'Requests']} + /> + + + + +
+ )} +
+
+ + )} + {activeTab === 'portrait' && ( <> {/* Request Performance Section */} diff --git a/src/apps/metrics-systems/pages/MetricsPage.tsx b/src/apps/metrics-systems/pages/MetricsPage.tsx index 6fb4ee3..d4223a6 100644 --- a/src/apps/metrics-systems/pages/MetricsPage.tsx +++ b/src/apps/metrics-systems/pages/MetricsPage.tsx @@ -3,17 +3,20 @@ import { useParams, useNavigate } from 'react-router-dom' import MetricsNavigation from '../components/MetricsNavigation' import MetricsDashboard from '../components/MetricsDashboard' +const VALID_TABS = ['system', 'containers', 'portrait', 'microgpt'] as const +type Tab = typeof VALID_TABS[number] + const MetricsPage = () => { const { tab } = useParams<{ tab: string }>() const navigate = useNavigate() const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'failed'>('disconnected') - const activeTab = (tab === 'system' || tab === 'containers' || tab === 'portrait') - ? tab + const activeTab: Tab = (VALID_TABS as readonly string[]).includes(tab ?? '') + ? tab as Tab : 'system' useEffect(() => { - if (tab !== 'system' && tab !== 'containers' && tab !== 'portrait') { + if (!(VALID_TABS as readonly string[]).includes(tab ?? '')) { navigate('/metrics/system', { replace: true }) } }, [tab, navigate]) @@ -22,22 +25,22 @@ const MetricsPage = () => { setConnectionStatus(status) }, []) - const handleTabChange = useCallback((newTab: 'system' | 'containers' | 'portrait') => { + const handleTabChange = useCallback((newTab: Tab) => { navigate(`/metrics/${newTab}`) }, [navigate]) return (
- -
) } -export default MetricsPage \ No newline at end of file +export default MetricsPage