feat(ui): V2-3 (Teil 2) — das UI haengt am Ereignis-Strom, Taktgeber raus
Ampel / ampel (push) Successful in 47s
Ampel / ampel (push) Successful in 47s
WAS: EventStreamProvider haelt EINE SSE-Verbindung fuer die ganze
Anwendung. Dashboard, Log-Kasten, Log-Seite, Laufwerksliste und
Worker-Liste beziehen ihren Zustand daraus. Sieben von neun setInterval
sind weg.
GEMESSEN AN DEN TAKTGEBERN, je offenem Tab:
Dashboard 5 Endpunkte / 4 s 75/min -> 0
Log-Kasten 2 Endpunkte / 5 s 24/min -> 0
Laufwerke 1 Endpunkt / 5 s 12/min -> 0
Log-Seite 1 Endpunkt /10 s 6/min -> 0 (+1 Abruf beim Oeffnen)
Worker-Liste 1 Endpunkt /15 s 4/min -> 0 (+1 Abruf beim Oeffnen)
------
121/min -> ~2 einmalige Abrufe
Zwei Taktgeber bleiben bewusst: FirstRunWizard (laeuft nur VOR der
Einrichtung) und RipTargetModal (nur solange der Dialog offen ist).
DIE REGEL IST UMGEZOGEN, NICHT VERSCHWUNDEN: Ein Abriss ist keine
Aussage ueber die Welt. Der Provider BEHAELT bei einem Fehler den letzten
Stand und setzt nur `verbunden` auf false; es wird nie eine Liste geleert.
Jede Komponente uebernimmt einen Wert nur, wenn er wirklich da ist —
`devices === null` heisst "konnte nicht nachsehen", nicht "keine
Laufwerke". Das war der Fehler hinter "wird oft neu geladen".
EIN PLACEBO WENIGER: Oben rechts stand ein fest verdrahtetes "ONLINE" mit
pulsierendem Punkt — es leuchtete gruen, auch wenn die API tot war. Jetzt
zeigt es LIVE oder VERBINDUNG WEG, und im Tooltip steht, wann die letzte
Meldung kam.
DER SERVER SCHIEBT JETZT AUCH DEN SERVER-ZUSTAND: Neuer Ereignistyp
system.status (Hardware, Worker, Ablageziele) im 15-Sekunden-Takt des
Waechters — EINMAL im Server statt 15/min je Tab. Nur mitgeschickte
Schluessel werden uebernommen; ein fehlender heisst "behalte deinen Stand".
DAZU EIN FORMATFEHLER GEFUNDEN UND BEHOBEN: /logs bildet ts -> timestamp
ab, mein Snapshot lieferte die rohe DB-Zeile. Das UI haette "Invalid Date"
gezeigt — und zwar NUR im Live-Betrieb, nicht beim manuellen Neuladen.
Jetzt gibt es _log_zeile() einmal, benutzt von beiden.
UND EINEN ZWEITEN: system_lesen lief per asyncio.get_event_loop() in einem
Worker-Thread — dort ist das NICHT die laufende Schleife. Die Coroutine
waere nie gelaufen. Die Schleife wird jetzt im Startup festgehalten.
GEMESSEN: ruff sauber, 378 Tests + 3 uebersprungen, `npm run build`
durch (1650 Module). Der Live-Beweis steht noch aus — er kommt mit dem
Deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f097a0b59d
commit
95705c8d88
@@ -10,6 +10,7 @@ import DeviceDiscovery from '../components/DeviceDiscovery'
|
||||
import JobDetailModal from '../components/JobDetailModal'
|
||||
import LiveLogSection from '../components/LiveLogSection'
|
||||
import RetryDialog from '../components/RetryDialog'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
|
||||
interface JobMeta {
|
||||
year?: number
|
||||
@@ -126,6 +127,7 @@ export default function Dashboard() {
|
||||
const [workersLive, setWorkersLive] = useState<WorkerLive[]>([])
|
||||
const [laufwerke, setLaufwerke] = useState<LaufwerkLive[]>([])
|
||||
const [ablagen, setAblagen] = useState<AblageZiel[]>([])
|
||||
const strom = useStrom()
|
||||
const { toast } = useToast()
|
||||
|
||||
/*
|
||||
@@ -232,64 +234,43 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
/*
|
||||
* ZWEI TAKTE statt einem.
|
||||
* KEIN TAKTGEBER MEHR (Etappe V2-3).
|
||||
*
|
||||
* ⚠️ JEDER Abruf hier gibt bei Fehlschlag `null` — und `null` bedeutet „nichts
|
||||
* Neues erfahren", nicht „es gibt nichts". Vor dem 26.07.2026 stand überall
|
||||
* `catch(() => [])`: ein einziger verpasster Abruf leerte Job-Liste,
|
||||
* Laufwerke und Ablagen, vier Sekunden später war alles wieder da. Genau das
|
||||
* hat der Commander als „wird oft neu geladen" gemeldet. Siehe `fetchJobs`.
|
||||
* Hier standen zwei: alle 4 s Jobs und Laufwerke, alle 12 s Hardware,
|
||||
* Worker und Ablagen — zusammen 75 Anfragen pro Minute JE OFFENEM TAB. Bei
|
||||
* der damaligen Grenze von 100/min wies die API laufend mit HTTP 429 ab
|
||||
* (gemessen: 97 Abweisungen im Log), und weil ein Fehlschlag im UI als
|
||||
* „es gibt nichts" gelesen wurde, leerte sich die Job-Liste im Sekundentakt.
|
||||
* Genau das hat der Commander als „wird oft neu geladen" gemeldet.
|
||||
*
|
||||
* Und verpasst wurde reichlich: Fünf Endpunkte alle vier Sekunden sind 75
|
||||
* Anfragen pro Minute, dazu Log-Kasten und Laufwerks-Suche — die API wies bei
|
||||
* ihrer damaligen Grenze von 100/min laufend mit HTTP 429 ab (gemessen: 97
|
||||
* Abweisungen im Log). Die Grenze ist jetzt der echten Last angemessen
|
||||
* (ratelimit.py), aber das rechtfertigt keine Verschwendung: Jobs und
|
||||
* Laufwerke ändern sich sekündlich, die drei anderen praktisch nie.
|
||||
* 75/min sind damit 15 + 15/min geworden.
|
||||
* Jetzt kommt alles aus der EINEN Verbindung, die App.tsx haelt.
|
||||
*
|
||||
* ⚠️ DIE REGEL BLEIBT — sie ist nur umgezogen: Ein fehlender Wert im Strom
|
||||
* heisst „nichts Neues erfahren", NICHT „es gibt nichts". Deshalb wird unten
|
||||
* jeder Wert nur uebernommen, wenn er wirklich da ist; sonst bleibt der
|
||||
* letzte bekannte Stand stehen. Das UI zeigt dann oben rechts
|
||||
* „VERBINDUNG WEG" statt leerer Listen.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const nichts = () => null
|
||||
setJobs(strom.jobs as Job[])
|
||||
setLoading(false)
|
||||
}, [strom.jobs])
|
||||
|
||||
// Schnell (4 s): was sich während eines Rips wirklich bewegt.
|
||||
const schnellLaden = async () => {
|
||||
try {
|
||||
const [jobsData, devData] = await Promise.all([
|
||||
fetchJobs(),
|
||||
// Die Laufwerke: Ohne sie behauptete die Server-Status-Karte
|
||||
// „keine Disc in Arbeit", während oben die erkannte Disc stand.
|
||||
api.get('/devices').then(r => Array.isArray(r.data) ? r.data : null).catch(nichts),
|
||||
])
|
||||
if (jobsData) setJobs(jobsData)
|
||||
if (devData) setLaufwerke(devData)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (strom.devices !== null) setLaufwerke(strom.devices as LaufwerkLive[])
|
||||
}, [strom.devices])
|
||||
|
||||
// Langsam (12 s): Hardware, Worker-Liste, Ablagen. Ein Worker, der online
|
||||
// geht, oder eine Freigabe, die wegbricht, darf drei Takte später auffallen.
|
||||
const langsamLaden = async () => {
|
||||
const [sysData, capsData, ablData] = await Promise.all([
|
||||
api.get('/system/info').then(r => r.data).catch(nichts),
|
||||
// Für die ECHTE Online-Zahl: /capabilities kennt den Celery-Ping,
|
||||
// /system/info nur die Registrierung.
|
||||
api.get('/capabilities').then(r => r.data?.workers ?? null).catch(nichts),
|
||||
// Die Ablageziele inkl. eingehängter Freigaben — ohne sie zeigte der
|
||||
// Server-Status nur die Container-Platte (Commander-Befund).
|
||||
api.get('/storage-targets').then(r => Array.isArray(r.data) ? r.data : null).catch(nichts),
|
||||
])
|
||||
if (sysData) setSystemInfo(sysData)
|
||||
if (capsData) setWorkersLive(capsData)
|
||||
if (ablData) setAblagen(ablData)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (strom.workers.length || strom.verbunden) setWorkersLive(strom.workers as WorkerLive[])
|
||||
}, [strom.workers, strom.verbunden])
|
||||
|
||||
schnellLaden()
|
||||
langsamLaden()
|
||||
const schnell = setInterval(schnellLaden, 4000)
|
||||
const langsam = setInterval(langsamLaden, 12000)
|
||||
return () => { clearInterval(schnell); clearInterval(langsam) }
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (strom.systemInfo) setSystemInfo(strom.systemInfo as SystemInfo)
|
||||
}, [strom.systemInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (strom.ablagen !== null) setAblagen(strom.ablagen as AblageZiel[])
|
||||
}, [strom.ablagen])
|
||||
|
||||
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding')
|
||||
const queueJobs = jobs.filter(j => j.status === 'pending')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useToast } from '../context/ToastContext'
|
||||
import { PageHeader } from '../components/ui/PageHeader'
|
||||
import { Card, CardContent } from '../components/ui/Card'
|
||||
import { Button } from '../components/ui/Button'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
|
||||
interface LogEntry {
|
||||
id: string
|
||||
@@ -21,6 +22,7 @@ export default function LogsPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const { toast } = useToast()
|
||||
const strom = useStrom()
|
||||
|
||||
const refreshLogs = async (manuell = false) => {
|
||||
if (manuell) setRefreshing(true)
|
||||
@@ -37,11 +39,20 @@ export default function LogsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Einmal die volle Historie holen (200 Zeilen — der Strom liefert im
|
||||
// Snapshot nur die letzten 50), danach kommt alles Neue von selbst.
|
||||
// Der Aktualisieren-Knopf bleibt: Er holt die Historie erneut.
|
||||
useEffect(() => { refreshLogs() }, [])
|
||||
|
||||
useEffect(() => {
|
||||
refreshLogs()
|
||||
const interval = setInterval(refreshLogs, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
if (!strom.logs.length) return
|
||||
setLogs((alt) => {
|
||||
const bekannt = new Set(alt.map((l) => String(l.id)))
|
||||
const neue = strom.logs.filter((l) => !bekannt.has(String(l.id)))
|
||||
return neue.length ? ([...neue, ...alt] as LogEntry[]) : alt
|
||||
})
|
||||
setLoading(false)
|
||||
}, [strom.logs])
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
|
||||
Reference in New Issue
Block a user