Commander: „wenn man auf rippen starten klickt passiert irgendwas, was das
Programm nicht mag. Der Hintergrund ist einfach leer und er startet nix. Es
gibt auch keine fehlermeldung."
Nachgestellt: Dienst lokal gestartet, Disc-Karte geoeffnet, „Rippen starten"
geklickt. Browser-Konsole:
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
## Er startet sehr wohl — man sieht es nur nie
Waehrend die Seite leer war, lief der Job (per API gemessen: status
`processing`, progress 12). Das „er startet nix" ist also der zweite Teil
desselben Fehlers: Die Oberflaeche war weg, bevor sie ihn zeigen konnte.
## Die Ursache
`_job_kurz` in `bus/waechter.py` trug nur `status`, `progress`, `title` und
`error` — **keinen `type`**. Das UI fuegt aus `job.created` einen halben Job
in seine Liste ein, `LiveLogSection` liest `job.type.toUpperCase()`, und React
baut bei einem Fehler im Zeichnen den GESAMTEN Baum ab. Es gab in diesem
Projekt keine einzige Fehlergrenze — also blieb ein leerer Bildschirm ohne
jede Meldung.
## Drei Reparaturen, weil es drei Fehler waren
1. **Das Ereignis traegt den Job.** `type`, `device` und `startTime` fahren
mit. Sie aendern sich ueber die Lebenszeit eines Jobs nie, kosten also kein
zusaetzliches Ereignis — und ohne `startTime` stand in der Jobliste
sekundenlang „Invalid Date".
2. **Das UI vertraegt sein Fehlen.** `LiveLogSection` und `TypeBadge` nahmen
einen vollstaendigen Job an. Zeile 108 derselben Datei hatte das
Fragezeichen laengst, Zeile 48 nicht.
3. **Eine Fehlergrenze.** Ein Fehler in einer Karte darf nicht den ganzen
Bildschirm mitnehmen — und schon gar nicht schweigend. Jetzt steht da, was
los ist, die Navigation bleibt bedienbar, und der Hinweis sagt das
Wichtigste: laufende Rips gehen weiter.
## Warum es niemand gefunden hat
`unterschiede()` bekommt die Kurzform schon fertig, und alle Tests reichen
ihre eigenen Woerterbuecher herein. **`_job_kurz` selbst war nie geprueft.**
Jetzt bewacht `UI_PFLICHTFELDER` den Vertrag mechanisch — es gibt kein
gemeinsames Typsystem zwischen Python und dem UI.
Gegengeprueft im Browser: derselbe Klick, keine Konsolenfehler, Job erscheint
als „Alle (1)".
833 Tests gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import { Terminal, Play, CheckCircle, AlertCircle } from 'lucide-react'
|
|
import { Card, CardHeader, CardTitle } from './ui/Card'
|
|
import { useStrom } from '../lib/useEventStream'
|
|
|
|
interface LiveLogEntry {
|
|
id: string
|
|
timestamp: string
|
|
level: 'info' | 'warning' | 'error' | 'success'
|
|
message: string
|
|
source: string
|
|
}
|
|
|
|
export default function LiveLogSection() {
|
|
// 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) {
|
|
case 'error': return 'text-rose-500 dark:text-rose-400'
|
|
case 'warning': return 'text-amber-500 dark:text-amber-400'
|
|
case 'success': return 'text-emerald-500 dark:text-emerald-400'
|
|
default: return 'text-sky-500 dark:text-sky-400'
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = (level: string) => {
|
|
switch (level) {
|
|
case 'error': return <AlertCircle size={15} />
|
|
case 'warning': return <AlertCircle size={15} />
|
|
case 'success': return <CheckCircle size={15} />
|
|
default: return <Terminal size={15} />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card className="overflow-hidden">
|
|
<CardHeader>
|
|
<div>
|
|
<CardTitle>Live-Log</CardTitle>
|
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
{currentJob
|
|
// `type?.` ist hier kein Beiwerk: Ein frisch angelegter Job kann
|
|
// noch ohne Typ ankommen, und ohne das Fragezeichen riss diese
|
|
// eine Zeile am 29.08.2026 die GANZE Oberflaeche mit.
|
|
? `Aktiver Job: ${currentJob.type?.toUpperCase() || 'Disc'} (${currentJob.progress ?? 0}%)`
|
|
: 'Kein aktiver Job'}
|
|
</p>
|
|
</div>
|
|
{currentJob && (
|
|
<div className="flex items-center gap-2">
|
|
<div className={`px-3 py-1 rounded-full text-xs font-medium flex items-center gap-2 border ${
|
|
currentJob.progress === 100
|
|
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30'
|
|
: 'bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/30'
|
|
}`}>
|
|
{currentJob.progress === 100 ? <CheckCircle size={12} /> : <Play size={12} />}
|
|
{currentJob.progress}%
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardHeader>
|
|
|
|
<div className="max-h-[380px] overflow-y-auto p-4 space-y-2 font-mono text-xs bg-slate-900/90 text-slate-200 border-b border-slate-800">
|
|
{logs.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
<Terminal size={32} className="mx-auto mb-2 opacity-50" />
|
|
<p>Warte auf Log-Einträge...</p>
|
|
</div>
|
|
) : (
|
|
logs.map((log) => (
|
|
<div key={log.id} className="flex items-start gap-2 text-slate-300">
|
|
<span className="text-slate-500 whitespace-nowrap">
|
|
{new Date(log.timestamp).toLocaleTimeString('de-DE', { hour12: false })}
|
|
</span>
|
|
<span className={`font-bold ${getStatusColor(log.level)}`}>
|
|
[{log.source}]
|
|
</span>
|
|
<span className="flex-1 text-slate-200">{log.message}</span>
|
|
<span className={getStatusColor(log.level)}>
|
|
{getStatusIcon(log.level)}
|
|
</span>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
{currentJob && (
|
|
<div className="px-6 py-3 grid grid-cols-3 gap-4 bg-slate-50/50 dark:bg-slate-900/50">
|
|
<div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">Fortschritt</p>
|
|
<p className="text-lg font-bold text-slate-900 dark:text-slate-100">
|
|
{currentJob.progress}%
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">Gerät</p>
|
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
{currentJob.device || 'Unbekannt'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">Typ</p>
|
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
{currentJob.type?.toUpperCase() || 'Unbekannt'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
)
|
|
}
|