fix(ui): Datum 1.1.1970, Disc nicht erkannt, kein Abbrechen — eine Ursache je Fall
Ampel / ampel (push) Successful in 1m16s

Vier Befunde aus einem Bildschirmfoto vom 29.08.2026.

## 1. „das Datum ist falsch" — und der Typ auch

    TYP „DISC"   STARTZEIT „1.1.1970, 01:00:00"   STATUS „running"

⚠️ Das war MEIN Fehler von heute Vormittag. Ich hatte `type`, `device` und
`startTime` in das Job-Ereignis aufgenommen — mit Feldnamen, **die es in der
Datenbankzeile nicht gibt**. Dort heissen sie `disc_type` und `created_at`;
die Uebersetzung macht `_job_row_to_model`, und sie bildet auch `running` auf
`processing` ab.

Die Folge war schlimmer als das Problem davor: Statt eines FEHLENDEN Feldes
kam ein LEERES. `new Date(null)` ist der 1.1.1970, und „DISC" war der
Rueckfall, den ich zwei Stunden vorher fuer genau diesen Fall eingebaut hatte.
Aus „offensichtlich kaputt" war „sieht plausibel aus" geworden.

Und mein Test hat es nicht gefunden, weil ich seine Beispielzeile selbst
erfunden habe — mit meinen falschen Feldnamen. Ein Test, der dieselbe Annahme
macht wie der Code, prueft nichts. Der neue geht von der ECHTEN Zeile aus.

Der Waechter bekommt jetzt dieselbe Uebersetzung eingespritzt, die auch
`/jobs` benutzt.

## 2. „Es ist eine Disk im laufwerk, aber er erkennt sie dort nicht"

Die Laufwerksliste wurde an DREI Stellen gebaut: `/devices` haengte die
erkannte Disc an, der Ereignis-Waechter und der SSE-Schnappschuss nicht. Das
Dashboard liest den Schnappschuss — und schloss aus der fehlenden Disc auf ein
leeres Laufwerk, waehrend ihr Titel eine Zeile weiter oben stand.

Jetzt gibt es `laufwerke_mit_disc()`, und ein Test zaehlt die Aufrufe von
`device_info` — bei zwei ist er rot.

## 3. „man kann einen rip garnicht abbrechen"

Dieselbe Ursache wie 1: Es gab genau EINEN Abbrechen-Knopf, in der Kachel
„laufender Job". Die erscheint nur bei Status `processing`; der Strom lieferte
`running`. Keine Kachel, kein Knopf, kein Weg zurueck — und aus demselben
Grund stand „Aktiv (0)", waehrend der Rip lief.

Zusaetzlich gibt es den Knopf jetzt in der Job-Zeile selbst, wo man ihn sucht.

## 4. „bei ‚Platz für rippy' sollte eher das arbeitsverzeichnis und der
##     Ablagepfad sein"

Dort stand eine nackte Zahl fuer den ERSTEN Ort. Welcher Ordner das war, sah
man nicht — und die zweite Zeile fiel ganz weg, wenn beide auf derselben
Platte lagen. Das stimmt fuer die ZAHL und war der falsche Schluss fuer den
ORT. Jetzt stehen beide da, mit Pfad, plus der Hinweis, dass der Platz
geteilt wird.

Im Browser gegengeprueft: „Disc erkannt — wartet auf ‚Rippen starten'",
BLU-RAY mit richtigem Datum, beide Pfade, keine Konsolenfehler.

852 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 15:00:11 +02:00
co-authored by Claude Opus 5
parent b2acddbdfa
commit 7b0c41ddfe
8 changed files with 361 additions and 37 deletions
+71 -3
View File
@@ -44,7 +44,8 @@ interface Job {
interface SystemInfo {
api_version: string
plaetze: { name: string; frei_gb: number; gesamt_gb: number }[]
plaetze: { name: string; pfad?: string; gleiches_laufwerk?: boolean;
frei_gb: number; gesamt_gb: number }[]
workers: { name: string; encoders: string[] }[]
}
@@ -584,6 +585,38 @@ export default function Dashboard() {
Details
</Button>
{/*
Abbrechen DIREKT in der Zeile (Commander
29.08.2026: „man kann einen rip garnicht
abbrechen").
Es gab genau einen Abbrechen-Knopf, und der
steckte in der Kachel „laufender Job". Die
erschien nur bei Status `processing` — der
Ereignisstrom lieferte aber den rohen
DB-Status `running`. Also keine Kachel, also
kein Knopf, also kein Weg zurueck.
Die Ursache ist behoben; ein zweiter Weg zum
Abbrechen ist trotzdem richtig, denn genau
hier sucht man ihn.
*/}
{['processing', 'transcoding', 'pending'].includes(job.status) && (
<Button
variant="danger"
size="sm"
onClick={() => api.post(`/jobs/${job.id}/cancel`)
.then(() => toast('info', 'Abbruch angefordert — der Rip haelt beim naechsten Zwischenschritt an.'))
.catch((e: any) => toast('error',
e?.response?.data?.detail || 'Abbrechen ging nicht'))}
>
Abbrechen
</Button>
)}
{job.status === 'canceling' && (
<span className="text-xs font-mono text-slate-400">bricht ab </span>
)}
{job.status === 'completed' && (
<Button variant="amber" size="sm" onClick={() => setDetailJobId(job.id)}>
<Download size={13} /> Download
@@ -749,10 +782,45 @@ export default function Dashboard() {
wollte, ob die NAS noch Platz für eine Disc hat, sah die
falsche Zahl (Commander-Befund 26.07.2026). */}
<div>
<div className="flex items-center justify-between text-xs font-mono mb-1">
{/*
Jeder Ort mit seinem PFAD (Commander 29.08.2026): „bei
Platz für rippy' sollte eher das arbeitsverzeichnis und
der Ablagepfad sein." Vorher stand hier eine nackte Zahl
für den ERSTEN Ort — welcher Ordner das war, sah man nicht.
*/}
{!betrieb.kann.container_pfade && (systemInfo?.plaetze || []).map(ort => (
<div key={ort.name + ort.pfad} className="mb-1.5">
<div className="flex items-center justify-between text-xs font-mono">
<span className="text-slate-400 flex items-center gap-1.5">
<HardDrive size={14} className="text-amber-400" />
{ort.name}:
</span>
<span className={ort.frei_gb > 0 && ort.frei_gb < 60
? 'text-amber-400 font-bold' : 'text-slate-200'}>
{ort.frei_gb > 0 ? `${ort.frei_gb} von ${ort.gesamt_gb} GB frei` : 'unbekannt'}
</span>
</div>
{ort.pfad && (
<p className="text-[11px] font-mono text-slate-500 truncate" title={ort.pfad}>
{ort.pfad}
</p>
)}
</div>
))}
{!betrieb.kann.container_pfade
&& (systemInfo?.plaetze || []).length > 1
&& systemInfo?.plaetze?.every(o => o.gleiches_laufwerk) && (
/* Ehrlich sagen, dass die Zahl zweimal dieselbe ist —
statt die zweite Zeile wegzulassen. */
<p className="text-[11px] font-mono text-slate-500 mb-1">
Beide auf demselben Laufwerk der Platz wird geteilt.
</p>
)}
<div className={`flex items-center justify-between text-xs font-mono mb-1 ${
betrieb.kann.container_pfade ? '' : 'hidden'}`}>
<span className="text-slate-400 flex items-center gap-1.5">
<HardDrive size={14} className="text-amber-400" />
{betrieb.kann.container_pfade ? 'Container-Platte:' : 'Platz für Rippy:'}
Container-Platte:
</span>
<span className={freiGb > 0 && freiGb < 60 ? 'text-amber-400 font-bold' : 'text-slate-200'}>
{freiGb > 0 ? `${freiGb} von ${gesamtGb} GB frei` : 'unbekannt'}