feat(v5): Etappe W-5 — Oberflaeche: Tray, Toasts, Taskleiste, Einstellungen, Bibliothek
Ampel / ampel (push) Successful in 1m23s

haupt/: tray.ts (Symbol mit Zustand, Kontextmenue, Fenster zu = weiter-
laufen, Beenden nur noch gewollt), Windows-Toasts bei Disc erkannt /
fertig / Fehler (Klick holt das Fenster), Taskleisten-Fortschritt
(setProgressBar, Fehlermodus), echter Windows-Ordnerdialog per IPC.
Eigenes Icon (bau/icon.png + tray.png, Disc-Optik).

Kern: Einstellungen lesen/schreiben NUR ueber die eine Quelle (§ 6.7,
Whitelist der Schluessel), Werkzeug-Auskunft (Katalog + gemessene
HandBrake-Presets/Backends), Bibliothek in db.ts (Schema v2 —
fingerprint, Titel, Groesse, Ort, Dauer), Duplikat-Erkennung beim
Einlegen (§ 6.6: 'schon einmal gerippt am …' als disc-info + Toast),
Pipeline traegt fertige Vorgaenge ein.

Fenster: drei Bereiche (Uebersicht / Einstellungen / Bibliothek).
Einstellungen: Ablage mit Durchsuchen-Dialog, Kompression je Disc-Typ
(Preset-Liste VOM eigenen HandBrake gemessen, 'NICHT komprimieren'
waehlbar), Sprachlisten, TMDb/OMDb-Keys, Medienserver, Werkzeug-Stand
mit Klartext (gefunden/fehlt + Folgen). Bibliothek als Tabelle.

Der Smoke-Screenshot zeigt nebenbei den Ernstfall in schoen: Das
Laufwerk ist nach dem rc11-Vorfall inzwischen auch am Storage-Stack in
'unknown' gekippt — und die Kachel zeigt den ZUGRIFFS_GRUENDE-Klartext
samt Abhilfe statt eines stummen Zustands.

165 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 18:47:39 +02:00
co-authored by Claude Fable 5
parent a9fa8fa2d8
commit c78ff22ab7
13 changed files with 782 additions and 128 deletions
+32 -3
View File
@@ -7,10 +7,10 @@
// Roh-Ordner; die echten Titel ersetzen das dann. Roh-Dateien werden in
// dieser Etappe GRUNDSÄTZLICH BEHALTEN — Löschen bekommt erst mit der
// Einstellung im UI (W-5) eine Stimme.
import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { copyFileSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
// Plattform-Pfade wie in struktur.ts: Laufzeit-Wurzeln, CI testet auf Linux.
import { basename, join } from 'node:path'
import type { GeraeteInfo, RipStatus } from '../../gemeinsam/nachrichten'
import type { BibliothekEintrag, GeraeteInfo, RipStatus } from '../../gemeinsam/nachrichten'
import { baueNfo } from '../ablage/nfo'
import { bibliothekRefresh } from '../ablage/medienserver'
import { posterSpeichern } from '../ablage/poster'
@@ -38,6 +38,10 @@ export interface PipelineUmgebung {
melden: (status: RipStatus) => void
/** Die letzte Metadaten-Zuordnung dieses Laufwerks (W-4) — oder null. */
zuordnung?: (id: string) => Zuordnung | null
/** Disc-Fingerabdruck fürs Wiedererkennen (§ 6.6) — oder null. */
fingerprint?: (id: string) => string | null
/** Trägt einen fertigen Vorgang in die Bibliothek ein (§ 6.6). */
bibliothekEintragen?: (eintrag: BibliothekEintrag) => void
/** Zeitstempel für Ordnernamen — injizierbar für Tests. */
jetzt?: () => Date
}
@@ -99,7 +103,8 @@ export class Pipeline {
}
const abbruch = new AbortController()
const zeit = (this.umgebung.jetzt?.() ?? new Date()).toISOString().replaceAll(/[:.]/g, '-').slice(0, 19)
const startZeit = this.umgebung.jetzt?.() ?? new Date()
const zeit = startZeit.toISOString().replaceAll(/[:.]/g, '-').slice(0, 19)
const vorgangsName = `${geraet.typ}-${zeit}`
const rohOrdner = join(this.umgebung.ablage(), 'roh', vorgangsName)
@@ -267,6 +272,30 @@ export class Pipeline {
const serverKey = String(einstellungen['mediaServerApiKey'] ?? '')
const refresh = await bibliothekRefresh(server, serverUrl, serverKey)
// § 6.6: Merken — beim nächsten Einlegen wird die Disc wiedererkannt.
const fingerprint = this.umgebung.fingerprint?.(id) ?? null
if (fingerprint !== null && this.umgebung.bibliothekEintragen !== undefined) {
let groesse = 0
for (const datei of fertigeDateien) {
try {
groesse += statSync(datei).size
} catch {
// Größe ist Anzeige-Komfort — eine unlesbare Datei ändert nichts
// am Eintrag selbst.
}
}
this.umgebung.bibliothekEintragen({
fingerprint,
titel: zuordnung !== null && zuordnung.titel.length > 0 ? zuordnung.titel : vorgangsName,
jahr: zuordnung?.jahr ?? null,
typ: geraet.typ,
groesseBytes: groesse,
ablageort: fertigOrdner,
datum: startZeit.toISOString(),
dauerS: Math.round(((this.umgebung.jetzt?.() ?? new Date()).getTime() - startZeit.getTime()) / 1000),
})
}
this.laufend.delete(id)
this.melden({
id,