Fix: Live-Graphen ueberleben Tab-Wechsel (modul-globaler Store + App-Feeder)

Bisher lag der Verlauf in component-lokalem State (useSystemHistory / TokenPerformanceCard).
Beim Tab-Wechsel wurde die Zentrale unmountet -> Historie weg -> Graphen starteten leer
und mussten sich neu aufbauen.

Neu: lib/metricsStore.ts haelt den Verlauf modul-global (useSyncExternalStore) und wird
von useMetricsFeeder() gefuettert, das in App (immer gemountet) haengt. So sammelt der
Verlauf kontinuierlich weiter - unabhaengig vom aktiven Tab - und die Graphen zeigen beim
Zurueckwechseln sofort die volle Historie. Zentrale + Diagnose teilen denselben Store.

Verifiziert: Store waechst auch auf Modell-Manager weiter; Rueckkehr zeigt lueckenlosen
Verlauf statt Reset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 17:25:35 +02:00
parent 694aff9801
commit 707b812f8b
8 changed files with 166 additions and 143 deletions
@@ -1,36 +1,16 @@
import { useEffect, useRef, useState } from "react"
import { Activity } from "lucide-react"
import { useTokenStats } from "@/lib/queries"
import { useTokHistory } from "@/lib/metricsStore"
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
const MAX_POINTS = 40
type RateSample = { t: number; prompt: number; completion: number }
const SERIES: ChartSeries[] = [
{ key: "prompt", label: "Prompt", color: "#f59e0b" },
{ key: "completion", label: "Antwort", color: "#2dd4bf" },
]
export function TokenPerformanceCard() {
const { data: ts, dataUpdatedAt } = useTokenStats(3_000)
const [hist, setHist] = useState<RateSample[]>([])
const prev = useRef<{ p: number; c: number; t: number } | null>(null)
// Durchsatz (tok/s) aus den kumulativen Zählern ableiten: Delta / Zeitspanne pro Poll.
useEffect(() => {
if (!ts) return
const now = Date.now()
const p = ts.prompt_tokens, c = ts.completion_tokens
if (prev.current) {
const dt = Math.max((now - prev.current.t) / 1000, 0.001)
const prompt = Math.max(0, (p - prev.current.p) / dt)
const completion = Math.max(0, (c - prev.current.c) / dt)
setHist((h) => [...h, { t: now, prompt, completion }].slice(-MAX_POINTS))
}
prev.current = { p, c, t: now }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataUpdatedAt])
const { data: ts } = useTokenStats(3_000)
const hist = useTokHistory() // Durchsatz-Verlauf aus dem modul-globalen Store
const last = hist[hist.length - 1]
const curRate = last ? Math.round(last.prompt + last.completion) : 0