Ampel / ampel (push) Successful in 1m34s
Zwei Befunde des Commanders (01.09.2026):
1. "Momentan ist alles nach links gerueckt, nichts skaliert mit der
fenstergroesse und es ist alles gequetscht."
Ursache gefunden: <main> hatte WEDER Zentrierung NOCH Maximalbreite,
die Karten darin aber einen harten Deckel von max-w-2xl (672 px). In
einem 1280er Fenster klebte damit alles am linken Rand und rechts
stand nichts. Behoben:
* <main> und Kopfzeile teilen sich mx-auto max-w-[1600px] mit
mitwachsendem Innenabstand — der Inhalt ist mittig und nutzt die
Breite.
* Der 672-px-Deckel der Einstellungs-Spalte ist weg. Drei
Encoder-Felder stehen jetzt nebeneinander statt untereinander.
* Die Sektionswahl der Einstellungen wird im schmalen Fenster zur
Zeile ueber den Karten (lg:flex-row), statt eine 176-px-Spalte vom
ohnehin knappen Platz abzuziehen.
* Info-Kacheln der Laufwerks-Karte: zwei Spalten schmal, vier breit.
* Startgroesse 1000x700 -> 1280x820, Mindestmass 720x480 -> 860x560.
2. "waere sogar richtig cool wenn du den rahmen wegbekommst"
frame:false, und die Kopfzeile IST jetzt die Titelleiste:
Minimieren / Maximieren / Schliessen sitzen oben rechts in
Windows-Massen (46x32), gezogen wird an der Kopfzeile, Doppelklick
maximiert. Groesse aendern bleibt moeglich — Electron setzt fuer
rahmenlose Fenster unter Windows weiter WS_THICKFRAME (thickFrame,
Standard true), die Kanten ziehen also wie gewohnt.
Schliessen nimmt GENAU den Weg des alten Fensterkreuzes
(fenster.close() -> der bestehende close-Horcher versteckt nur):
Rippy lebt im Tray weiter, ein laufender Rip merkt davon nichts
(Paragraph 4.1 Punkt 3).
Die Ziehflaeche ist .ziehbar in stil.css; alles Bedienbare darin
traegt .nicht-ziehbar, sonst verschluckt der Griff den Klick.
fensterMaximiert im HauptStatus entscheidet ueber das Knopf-Symbol
und wird auch bei Doppelklick/Tastenkuerzel nachgefuehrt (maximize-
und unmaximize-Horcher).
Nachgemessen mit dem Smoke-Beweis: beweise/ui-513-uebersicht.png (kein
Rahmen, eigene Knoepfe, Inhalt ueber die volle Breite) und
beweise/ui-513-einstellungen.png (drei Encoder nebeneinander).
230 Tests gruen, Typpruefung sauber. Version 5.1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
// Das Nachrichten-Schema zwischen den drei Prozessen (KONZEPT-WINDOWS.md § 4.1).
|
||
// NUR Typen und Prüf-Funktionen — keine Logik, keine Abhängigkeiten, denn diese
|
||
// Datei läuft im Fenster UND im Kern.
|
||
//
|
||
// Wege:
|
||
// Kern → Haupt über process.parentPort (KernNachricht)
|
||
// Kern ↔ Fenster über einen direkten MessagePort (KernNachricht/FensterNachricht)
|
||
// Haupt → Fenster über IPC-Kanal 'haupt-status' (HauptStatus)
|
||
// Haupt → Kern über utilityProcess.postMessage (HauptNachricht)
|
||
|
||
/** Zustand der Datenbank, wie der Kern ihn beim Start gemessen hat. */
|
||
export type DbStatus = { ok: true; pfad: string } | { ok: false; fehler: string }
|
||
|
||
// ── Laufwerke (Etappe W-1) ──────────────────────────────────────────────
|
||
export type DiscTyp = 'cd' | 'dvd' | 'bluray' | 'uhd' | 'unknown'
|
||
export type LaufwerkStatus = 'ready' | 'empty' | 'unknown'
|
||
|
||
export interface GeraeteInfo {
|
||
/** Kurze Kennung fürs UI und für Befehle — der Laufwerksbuchstabe. */
|
||
id: string
|
||
name: string
|
||
typ: DiscTyp
|
||
pfad: string
|
||
status: LaufwerkStatus
|
||
modell: string
|
||
seriennummer: string
|
||
/** Größe des eingelegten Mediums in Bytes; 0, wenn unbekannt/leer. */
|
||
groesseBytes: number
|
||
/** Leer, solange alles geht — sonst steht hier, WARUM „unknown" dasteht. */
|
||
grund: string
|
||
}
|
||
|
||
export type WacheEreignis =
|
||
| { art: 'disc-eingelegt'; geraet: GeraeteInfo }
|
||
| { art: 'disc-entnommen'; geraet: GeraeteInfo }
|
||
| { art: 'laufwerk-neu'; geraet: GeraeteInfo }
|
||
| { art: 'laufwerk-weg'; id: string }
|
||
|
||
export type AuswurfGrund = 'ok' | 'kein-zugriff' | 'abgelehnt' | 'bleibt-drin'
|
||
|
||
// ── Rippen (Etappe W-2) ─────────────────────────────────────────────────
|
||
export type RipPhase =
|
||
| 'liest-info'
|
||
| 'rippt'
|
||
| 'komprimiert'
|
||
| 'legt-ab'
|
||
| 'fertig'
|
||
| 'fehler'
|
||
| 'abgebrochen'
|
||
|
||
export interface RipStatus {
|
||
/** Laufwerks-Kennung, auf der der Rip läuft. */
|
||
id: string
|
||
phase: RipPhase
|
||
/** 0–100; -1 heißt „noch keine Angabe" (ein echtes 0 % bleibt sichtbar). */
|
||
prozent: number
|
||
text: string
|
||
dateien: string[]
|
||
lesefehler: boolean
|
||
}
|
||
|
||
/** Was Rippy über die eingelegte Disc herausgefunden hat (§ 5 Schritt 3). */
|
||
export interface DiscInfoStand {
|
||
id: string
|
||
titel: string
|
||
jahr: number | null
|
||
/** 0–1; die Schwelle für „sicher genug" ist einstellbar. */
|
||
confidence: number
|
||
quelle: string
|
||
posterPfad: string
|
||
/** TMDb-Hintergrundbild (Querformat) fürs Kachel-Panorama; '' wenn keins. */
|
||
backdropPfad: string
|
||
/** Kurzbeschreibung (TMDb-Overview) — '' wenn keine. */
|
||
beschreibung: string
|
||
/** Datum des früheren Rips dieser Disc — die Bibliothek erkennt sie am
|
||
* Fingerabdruck wieder (§ 6.6); null wenn neu. */
|
||
schonGerippt: string | null
|
||
/** Nummer der Disc in einer Mehr-Disc-Ausgabe — null, wenn keine dasteht. */
|
||
discNr: number | null
|
||
/** Staffel-Nummer aus Titel oder Label — null, wenn keine dasteht. */
|
||
staffel: number | null
|
||
/** Folgen laut dem Inhaltsverzeichnis der Disc selbst — 0, wenn es keines
|
||
* gibt. Ab zwei gilt „Serie" als bewiesen (FOLGEN_FUER_SERIE). */
|
||
folgen: number
|
||
}
|
||
|
||
/** Ein Eintrag der Bibliothek (§ 6.6). */
|
||
export interface BibliothekEintrag {
|
||
fingerprint: string
|
||
titel: string
|
||
jahr: number | null
|
||
typ: string
|
||
groesseBytes: number
|
||
ablageort: string
|
||
datum: string
|
||
dauerS: number
|
||
/** TMDb-Posterpfad fürs Bibliotheks-Regal — '' wenn keiner bekannt. */
|
||
posterPfad: string
|
||
}
|
||
|
||
/** Ein wählbarer Metadaten-Vorschlag (§ 5 Schritt 4: „nein → fragen"). */
|
||
export interface MetaVorschlag {
|
||
typ: 'movie' | 'tv'
|
||
tmdbId: number
|
||
titel: string
|
||
jahr: number | null
|
||
posterPfad: string
|
||
}
|
||
|
||
/** Auskunft über die Werkzeuge (Katalog + gemessene HandBrake-Listen). */
|
||
export interface WerkzeugAuskunft {
|
||
makemkvcon: string
|
||
handbrake: string
|
||
flac: string
|
||
presets: string[]
|
||
backends: string[]
|
||
}
|
||
|
||
/** Stand einer laufenden Werkzeug-Beschaffung (§ 9: MakeMKV-Update). */
|
||
export interface BeschaffungStand {
|
||
/** Welches Werkzeug — bisher nur 'makemkv'. */
|
||
werkzeug: string
|
||
laeuft: boolean
|
||
text: string
|
||
/** 0–1 während des Downloads; null wenn gerade nicht bezifferbar. */
|
||
anteil: number | null
|
||
fehler: boolean
|
||
}
|
||
|
||
/** Nachrichten, die der Kern verschickt. */
|
||
export type KernNachricht =
|
||
| { art: 'kern-bereit'; pid: number; nodeVersion: string; datenbank: DbStatus }
|
||
| { art: 'pong'; zeit: number }
|
||
| { art: 'kern-fehler'; text: string }
|
||
| { art: 'laufwerke'; liste: GeraeteInfo[]; ereignisse: WacheEreignis[] }
|
||
| { art: 'auswurf-ergebnis'; id: string; grund: AuswurfGrund; fehler: string | null }
|
||
| { art: 'rip-status'; status: RipStatus }
|
||
| { art: 'disc-info'; info: DiscInfoStand }
|
||
| { art: 'metadaten-vorschlaege'; id: string; vorschlaege: MetaVorschlag[] }
|
||
| { art: 'einstellungen'; werte: Record<string, string> }
|
||
| { art: 'werkzeuge'; auskunft: WerkzeugAuskunft }
|
||
| { art: 'bibliothek'; eintraege: BibliothekEintrag[] }
|
||
| { art: 'beschaffung-status'; stand: BeschaffungStand }
|
||
/** Nur fürs Haupt: Der Kern hat einen geprüften Installer abgelegt —
|
||
* starten kann ihn nur das Haupt (shell/UAC, § 9). */
|
||
| { art: 'installer-starten'; pfad: string }
|
||
| {
|
||
art: 'schluessel-status'
|
||
stand: {
|
||
quelle: string
|
||
angenommen: boolean | null
|
||
/** Rips laufen trotzdem (Beta-Modus) — Warnung, kein Ausfall. */
|
||
weiterBetrieb: boolean
|
||
gueltigBis: string
|
||
geholtAm: string
|
||
text: string
|
||
/** Die installierte MakeMKV-Fassung (aus MSG 1005) — '' wenn unbekannt. */
|
||
version: string
|
||
}
|
||
}
|
||
|
||
/** Nachrichten, die das Fenster über den MessagePort an den Kern schickt. */
|
||
export type FensterNachricht =
|
||
| { art: 'ping'; zeit: number }
|
||
| { art: 'auswerfen'; id: string }
|
||
| { art: 'rip-start'; id: string }
|
||
| { art: 'rip-abbruch'; id: string }
|
||
| { art: 'einstellung-setzen'; schluessel: string; wert: string }
|
||
| { art: 'metadaten-suchen'; id: string; text: string }
|
||
| { art: 'zuordnung-setzen'; id: string; vorschlag: MetaVorschlag }
|
||
| { art: 'einstellungen-laden' }
|
||
| { art: 'werkzeuge-laden' }
|
||
| { art: 'bibliothek-laden' }
|
||
/** § 9: MakeMKV über die Quellenkette holen und installieren lassen. */
|
||
| { art: 'makemkv-update' }
|
||
/** Schlüsselkette sofort neu laufen lassen (z. B. nach einer Installation). */
|
||
| { art: 'schluessel-pruefen' }
|
||
|
||
/** Nachrichten, die das Haupt an den Kern schickt. */
|
||
export type HauptNachricht =
|
||
| { art: 'fenster-port' }
|
||
/** Antwort auf 'installer-starten': '' bzw. null heißt gestartet. */
|
||
| { art: 'installer-ergebnis'; fehler: string | null }
|
||
|
||
/** Zustand des Haupt-Prozesses fürs Fenster (Kanal 'haupt-status'). */
|
||
export interface HauptStatus {
|
||
version: string
|
||
/** Prozess-Leine (Job Object, § 3.3): gesetzt oder mit Fehlertext. */
|
||
leine: { ok: boolean; fehler?: string }
|
||
kern: { laeuft: boolean; pid?: number; neustarts: number }
|
||
autostart: boolean
|
||
/** Selbst-Update (§ 6.8). */
|
||
update: UpdateStand
|
||
/** Für die eigene Titelleiste (§ 4.2, seit 5.1.3): Ist das Fenster
|
||
* gerade maximiert? Entscheidet nur, welches Symbol der Knopf zeigt. */
|
||
fensterMaximiert: boolean
|
||
}
|
||
|
||
/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt
|
||
* und Fenster teilen. Seit 01.09.2026 trägt sie auch die gefundene
|
||
* Version, ihre Änderungsnotizen und den Ladefortschritt: Der Kanal
|
||
* arbeitete vorher korrekt, aber unsichtbar (Commander-Fund). */
|
||
export interface UpdateStand {
|
||
/** Wann die letzte Prüfung GELANG — '' wenn noch nie. */
|
||
letzteErfolgreichePruefung: string
|
||
text: string
|
||
/** Die gefundene neue Version — '' wenn keine bereitsteht. */
|
||
neueVersion: string
|
||
/** Was in ihr neu ist (aus latest.yml) — '' wenn nichts hinterlegt ist. */
|
||
neuerungen: string
|
||
/** Geladen und wartet auf den Neustart. */
|
||
bereit: boolean
|
||
/** Eine Prüfung oder ein Download läuft gerade. */
|
||
laeuft: boolean
|
||
/** Ladefortschritt in Prozent; -1 heißt: kein Download unterwegs. */
|
||
fortschritt: number
|
||
}
|
||
|
||
function istObjekt(wert: unknown): wert is Record<string, unknown> {
|
||
return typeof wert === 'object' && wert !== null
|
||
}
|
||
|
||
export function istKernNachricht(wert: unknown): wert is KernNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
switch (wert.art) {
|
||
case 'kern-bereit':
|
||
return (
|
||
typeof wert.pid === 'number' &&
|
||
typeof wert.nodeVersion === 'string' &&
|
||
istDbStatus(wert.datenbank)
|
||
)
|
||
case 'pong':
|
||
return typeof wert.zeit === 'number'
|
||
case 'kern-fehler':
|
||
return typeof wert.text === 'string'
|
||
case 'laufwerke':
|
||
return Array.isArray(wert.liste) && Array.isArray(wert.ereignisse)
|
||
case 'auswurf-ergebnis':
|
||
return (
|
||
typeof wert.id === 'string' &&
|
||
typeof wert.grund === 'string' &&
|
||
(wert.fehler === null || typeof wert.fehler === 'string')
|
||
)
|
||
case 'rip-status':
|
||
return istRipStatus(wert.status)
|
||
case 'disc-info':
|
||
return (
|
||
istObjekt(wert.info) &&
|
||
typeof (wert.info as Record<string, unknown>).id === 'string' &&
|
||
typeof (wert.info as Record<string, unknown>).titel === 'string' &&
|
||
typeof (wert.info as Record<string, unknown>).confidence === 'number'
|
||
)
|
||
case 'einstellungen':
|
||
return istObjekt(wert.werte)
|
||
case 'werkzeuge':
|
||
return istObjekt(wert.auskunft) && Array.isArray((wert.auskunft as Record<string, unknown>).presets)
|
||
case 'bibliothek':
|
||
return Array.isArray(wert.eintraege)
|
||
case 'schluessel-status':
|
||
return istObjekt(wert.stand) && typeof (wert.stand as Record<string, unknown>).text === 'string'
|
||
case 'beschaffung-status':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
typeof (wert.stand as Record<string, unknown>).werkzeug === 'string' &&
|
||
typeof (wert.stand as Record<string, unknown>).text === 'string' &&
|
||
typeof (wert.stand as Record<string, unknown>).laeuft === 'boolean'
|
||
)
|
||
case 'installer-starten':
|
||
return typeof wert.pfad === 'string' && wert.pfad.length > 0
|
||
case 'metadaten-vorschlaege':
|
||
return typeof wert.id === 'string' && Array.isArray(wert.vorschlaege)
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
export function istMetaVorschlag(wert: unknown): wert is MetaVorschlag {
|
||
if (!istObjekt(wert)) return false
|
||
return (
|
||
(wert.typ === 'movie' || wert.typ === 'tv') &&
|
||
typeof wert.tmdbId === 'number' &&
|
||
typeof wert.titel === 'string' &&
|
||
(wert.jahr === null || typeof wert.jahr === 'number') &&
|
||
typeof wert.posterPfad === 'string'
|
||
)
|
||
}
|
||
|
||
export function istRipStatus(wert: unknown): wert is RipStatus {
|
||
if (!istObjekt(wert)) return false
|
||
return (
|
||
typeof wert.id === 'string' &&
|
||
typeof wert.phase === 'string' &&
|
||
typeof wert.prozent === 'number' &&
|
||
typeof wert.text === 'string' &&
|
||
Array.isArray(wert.dateien) &&
|
||
typeof wert.lesefehler === 'boolean'
|
||
)
|
||
}
|
||
|
||
export function istDbStatus(wert: unknown): wert is DbStatus {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.ok === true) return typeof wert.pfad === 'string'
|
||
if (wert.ok === false) return typeof wert.fehler === 'string'
|
||
return false
|
||
}
|
||
|
||
export function istFensterNachricht(wert: unknown): wert is FensterNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.art === 'ping') return typeof wert.zeit === 'number'
|
||
if (wert.art === 'auswerfen' || wert.art === 'rip-start' || wert.art === 'rip-abbruch') {
|
||
return typeof wert.id === 'string' && wert.id.length > 0
|
||
}
|
||
if (wert.art === 'einstellung-setzen') {
|
||
return typeof wert.schluessel === 'string' && wert.schluessel.length > 0 && typeof wert.wert === 'string'
|
||
}
|
||
if (wert.art === 'metadaten-suchen') {
|
||
return typeof wert.id === 'string' && wert.id.length > 0 && typeof wert.text === 'string'
|
||
}
|
||
if (wert.art === 'zuordnung-setzen') {
|
||
return typeof wert.id === 'string' && wert.id.length > 0 && istMetaVorschlag(wert.vorschlag)
|
||
}
|
||
return (
|
||
wert.art === 'einstellungen-laden' ||
|
||
wert.art === 'werkzeuge-laden' ||
|
||
wert.art === 'bibliothek-laden' ||
|
||
wert.art === 'makemkv-update' ||
|
||
wert.art === 'schluessel-pruefen'
|
||
)
|
||
}
|
||
|
||
export function istHauptNachricht(wert: unknown): wert is HauptNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.art === 'fenster-port') return true
|
||
if (wert.art === 'installer-ergebnis') return wert.fehler === null || typeof wert.fehler === 'string'
|
||
return false
|
||
}
|