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
@@ -7,6 +7,7 @@ import { Button } from './ui/Button'
|
||||
import { TypeBadge } from './ui/Badge'
|
||||
import RipTargetModal, { RipOptionen } from './RipTargetModal'
|
||||
import MetadataKorrektur from './MetadataKorrektur'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
|
||||
interface DiscInfo {
|
||||
title: string
|
||||
@@ -63,6 +64,7 @@ export default function DeviceDiscovery() {
|
||||
const [modalDevice, setModalDevice] = useState<Device | null>(null)
|
||||
const [korrekturDevice, setKorrekturDevice] = useState<Device | null>(null)
|
||||
const { toast } = useToast()
|
||||
const strom = useStrom()
|
||||
|
||||
const refreshDevices = async (manuell = false) => {
|
||||
if (manuell) setRefreshing(true)
|
||||
@@ -124,11 +126,20 @@ export default function DeviceDiscovery() {
|
||||
}
|
||||
}
|
||||
|
||||
// Vorher: alle 5 s ein eigener Abruf von /devices (12 Anfragen pro Minute).
|
||||
// Jetzt liefert der Server die Aenderung, sobald sie passiert.
|
||||
//
|
||||
// WICHTIG — nur uebernehmen, wenn der Server wirklich etwas WEISS:
|
||||
// `strom.devices === null` heisst „konnte nicht nachsehen" (haengendes
|
||||
// Laufwerk, Zeitgrenze) und NICHT „es gibt keine Laufwerke". In dem Fall
|
||||
// bleibt der letzte bekannte Stand stehen. Genau diese Vermischung hat in
|
||||
// v1 die Listen im Sekundentakt geleert.
|
||||
useEffect(() => {
|
||||
refreshDevices()
|
||||
const interval = setInterval(refreshDevices, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
if (strom.devices !== null) {
|
||||
setDevices(strom.devices as Device[])
|
||||
setLoading(false)
|
||||
}
|
||||
}, [strom.devices])
|
||||
|
||||
const QUELLEN_LABEL: Record<string, string> = {
|
||||
tmdb: 'TMDB', omdb: 'OMDb', jikan: 'MyAnimeList', manuell: 'manuell gewählt',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Terminal, Play, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { Card, CardHeader, CardTitle } from './ui/Card'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
|
||||
interface LiveLogEntry {
|
||||
id: string
|
||||
@@ -12,40 +11,14 @@ interface LiveLogEntry {
|
||||
}
|
||||
|
||||
export default function LiveLogSection() {
|
||||
const [logs, setLogs] = useState<LiveLogEntry[]>([])
|
||||
const [currentJob, setCurrentJob] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
const response = await api.get('/jobs')
|
||||
const jobs = response.data
|
||||
const current = jobs.find((j: any) => j.status === 'processing' || j.status === 'transcoding')
|
||||
setCurrentJob(current || jobs.find((j: any) => j.status === 'pending'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Jobs:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const response = await api.get('/logs', { params: { limit: 50 } })
|
||||
setLogs(response.data)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Logs:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchJobs()
|
||||
fetchLogs()
|
||||
const jobInterval = setInterval(fetchJobs, 5000)
|
||||
const logInterval = setInterval(fetchLogs, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(jobInterval)
|
||||
clearInterval(logInterval)
|
||||
}
|
||||
}, [])
|
||||
// Vorher: zwei eigene Taktgeber (alle 5 s /jobs und /logs) = 24 Anfragen pro
|
||||
// Minute allein aus diesem Kasten. Jetzt kommt beides aus der einen offenen
|
||||
// Verbindung, die der Provider haelt.
|
||||
const { jobs, logs } = useStrom()
|
||||
const currentJob =
|
||||
jobs.find((j: any) => j.status === 'processing' || j.status === 'transcoding') ||
|
||||
jobs.find((j: any) => j.status === 'pending') ||
|
||||
null
|
||||
|
||||
const getStatusColor = (level: string) => {
|
||||
switch (level) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useToast } from '../context/ToastContext'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
|
||||
import { Button } from './ui/Button'
|
||||
import { Input } from './ui/Input'
|
||||
import { useStrom } from '../lib/useEventStream'
|
||||
|
||||
interface WorkerInfo {
|
||||
name: string
|
||||
@@ -54,6 +55,7 @@ export default function WorkerVerwaltung() {
|
||||
// eintragen lassen als eine falsche Adresse vorgaukeln.
|
||||
const [rippyHost, setRippyHost] = useState('')
|
||||
const { toast } = useToast()
|
||||
const strom = useStrom()
|
||||
|
||||
// Für die Befehle: leeres Feld → sichtbarer Platzhalter (Befehl ist dann
|
||||
// erkennbar unvollständig, statt still auf die falsche Adresse zu zeigen).
|
||||
@@ -76,11 +78,16 @@ export default function WorkerVerwaltung() {
|
||||
.catch(() => { if (manuell) toast('error', 'Worker-Liste konnte nicht geladen werden') })
|
||||
}
|
||||
|
||||
// Die Worker-Liste kommt aus dem Live-Strom. /capabilities wird nur noch
|
||||
// EINMAL geholt — dort steht ausser der Liste die Server-Version, und die
|
||||
// aendert sich nur beim Deploy. Vorher lief der Abruf alle 15 s.
|
||||
useEffect(() => { laden() }, [])
|
||||
|
||||
useEffect(() => {
|
||||
laden()
|
||||
const interval = setInterval(laden, 15000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
if (strom.workers.length || strom.verbunden) {
|
||||
setWorkers(strom.workers as WorkerInfo[])
|
||||
}
|
||||
}, [strom.workers, strom.verbunden])
|
||||
|
||||
const installBefehl = variante === 'linux'
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user