Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com> Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com> Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com> Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
175 lines
5.9 KiB
TypeScript
175 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
const POLL_INTERVAL_MS = 15000;
|
|
|
|
interface Replica {
|
|
url: string;
|
|
healthy: boolean;
|
|
active_sessions?: number;
|
|
pending_sessions?: number;
|
|
max_available_sessions?: number;
|
|
prompt_provider_success_counts?: Record<string, number>;
|
|
}
|
|
|
|
function formatTimestamp(value: string): string {
|
|
if (!value) return '';
|
|
try {
|
|
return new Date(value).toLocaleString();
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function formatProviderSuccessCounts(
|
|
counts: Record<string, number> | undefined,
|
|
): string {
|
|
const normalized = counts || {};
|
|
const cerebrasIfm = normalized.cerebras_ifm ?? 0;
|
|
const cerebras = normalized.cerebras ?? 0;
|
|
const groq = normalized.groq ?? 0;
|
|
return `IFM ${cerebrasIfm} / Cerebras ${cerebras} / Groq ${groq}`;
|
|
}
|
|
|
|
export default function MonitorPage() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [replicas, setReplicas] = useState<Replica[]>([]);
|
|
const [lastUpdated, setLastUpdated] = useState('');
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function loadReplicaSessions() {
|
|
try {
|
|
const response = await fetch('/router/replicas/sessions');
|
|
const payload = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
payload.detail || 'Failed to fetch replica sessions',
|
|
);
|
|
}
|
|
setReplicas(
|
|
Array.isArray(payload.replicas) ? payload.replicas : [],
|
|
);
|
|
setError('');
|
|
setLastUpdated(new Date().toISOString());
|
|
} catch (err: any) {
|
|
setError(err?.message || String(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadReplicaSessions();
|
|
timerRef.current = setInterval(loadReplicaSessions, POLL_INTERVAL_MS);
|
|
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<main className="mx-auto flex min-h-screen w-full max-w-6xl flex-col gap-4 px-4 py-8 text-foreground">
|
|
<div className="space-y-2">
|
|
<h1 className="text-3xl font-semibold text-foreground">
|
|
Replica Session Monitor
|
|
</h1>
|
|
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
|
|
<Badge variant="secondary">poll interval: 15s</Badge>
|
|
{lastUpdated && (
|
|
<Badge variant="outline">
|
|
last updated: {formatTimestamp(lastUpdated)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<Card>
|
|
<CardContent className="p-6 text-sm text-muted-foreground">
|
|
Loading monitor data...
|
|
</CardContent>
|
|
</Card>
|
|
) : error ? (
|
|
<Card className="border-rose-500/30 bg-rose-950/45">
|
|
<CardContent className="p-6 text-sm text-rose-100">
|
|
{error}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="overflow-hidden">
|
|
<CardHeader className="border-b border-border pb-4">
|
|
<CardTitle className="text-xl">Replica capacity</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[760px] border-collapse text-left text-sm">
|
|
<thead className="bg-secondary text-muted-foreground">
|
|
<tr>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
URL
|
|
</th>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
Healthy
|
|
</th>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
Active WS Sessions
|
|
</th>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
Pending Sessions
|
|
</th>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
Max Available Sessions
|
|
</th>
|
|
<th className="border-b border-border px-4 py-3 font-semibold">
|
|
Prompt API Successes
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{replicas.map((replica, index) => (
|
|
<tr
|
|
key={replica.url || index}
|
|
className="border-b border-border last:border-b-0"
|
|
>
|
|
<td className="px-4 py-3 text-foreground">{replica.url}</td>
|
|
<td className="px-4 py-3">
|
|
<Badge
|
|
variant={replica.healthy ? 'success' : 'destructive'}
|
|
>
|
|
{replica.healthy ? 'yes' : 'no'}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-4 py-3 text-foreground">
|
|
{replica.active_sessions ?? '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-foreground">
|
|
{replica.pending_sessions ?? '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-foreground">
|
|
{replica.max_available_sessions ?? '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-foreground">
|
|
{formatProviderSuccessCounts(
|
|
replica.prompt_provider_success_counts,
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|