Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie veröffentlicht wurde): Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge, Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts, Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts. Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/, uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte), einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/ (ErstEinrichtung, PosterHintergrund). Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs); LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update; Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown; Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag; Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem Poster-Grid. Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung, fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit 29 Schritten inklusive Erst-Einrichtung (zweiter Start mit RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
184 lines
8.1 KiB
TypeScript
184 lines
8.1 KiB
TypeScript
// Die Übersicht — Laufwerke, Warteschlange, Speicher und Bilanz, Verlauf,
|
|
// Ereignisse. Und die Dialoge: Sie leben im Zustand DIESES Bereichs, nicht
|
|
// in der Kachel (5.4.0: ein leerer Takt der Wache räumte sonst Kachel UND
|
|
// Dialog ab).
|
|
import { useState } from 'react'
|
|
import type { HauptStatus } from '../../gemeinsam/nachrichten'
|
|
import { IconDisc, LAUFENDE_PHASEN } from '../bausteine'
|
|
import { AendernDialog } from '../dialoge/AendernDialog'
|
|
import { TitelWahlDialog } from '../dialoge/TitelWahlDialog'
|
|
import { automatikStopp, titelLesen, type Verbindung } from '../kernverbindung'
|
|
import { KompressionsKarte } from './KompressionsKarte'
|
|
import { LaufwerkKachel } from './LaufwerkKachel'
|
|
import { SpeicherKarte } from './SpeicherKarte'
|
|
import { VerlaufKarte } from './VerlaufKarte'
|
|
|
|
export function Uebersicht({
|
|
verbindung,
|
|
haupt,
|
|
systemFehler,
|
|
aufRohDateien,
|
|
}: {
|
|
verbindung: Verbindung
|
|
haupt: HauptStatus | null
|
|
systemFehler: string[]
|
|
aufRohDateien: () => void
|
|
}) {
|
|
// Sagt ein Update-Knopf NEIN, steht der Grund daneben (R4) — ein Knopf,
|
|
// der nichts tut und nichts sagt, ist schlimmer als kein Knopf.
|
|
const [updateHinweis, setUpdateHinweis] = useState('')
|
|
const [dialog, setDialog] = useState<{ art: 'titel' | 'aendern'; id: string } | null>(null)
|
|
const einRipLaeuft = Object.values(verbindung.rips).some((r) => LAUFENDE_PHASEN.includes(r.phase))
|
|
|
|
return (
|
|
<>
|
|
{systemFehler.length > 0 && (
|
|
<div className="rounded-2xl border border-rot/40 bg-rot/10 p-4">
|
|
{systemFehler.map((zeile, i) => (
|
|
<p key={i} className="break-all font-mono text-xs text-rot">
|
|
✗ {zeile}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* § 6.8: Ein verfügbares Update ist eine SICHTBARE Karte, kein
|
|
Satz in den Einstellungen. Der Commander musste es am
|
|
01.09.2026 suchen — gefunden hat er es nicht. */}
|
|
{haupt !== null && haupt.update.neueVersion.length > 0 && (
|
|
<div className="rounded-2xl border border-gold/40 bg-gold/10 p-4">
|
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
<span className="font-display text-lg text-gold-hell">Update verfügbar — Version {haupt.update.neueVersion}</span>
|
|
<span className="font-mono text-xs text-nebel">du hast {haupt.version}</span>
|
|
</div>
|
|
<p className="mt-1 font-mono text-xs text-dunst">{haupt.update.text}</p>
|
|
{haupt.update.fortschritt >= 0 && !haupt.update.bereit && (
|
|
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-white/10">
|
|
<div className="h-full rounded-full bg-gold transition-all" style={{ width: `${Math.max(0, Math.min(100, haupt.update.fortschritt))}%` }} />
|
|
</div>
|
|
)}
|
|
{haupt.update.neuerungen.length > 0 && (
|
|
<div className="mt-3 rounded-xl border border-white/8 bg-buehne/60 p-3">
|
|
<p className="text-[11px] uppercase tracking-wider text-nebel">Was neu ist</p>
|
|
<p className="mt-1 whitespace-pre-wrap font-mono text-xs leading-5 text-dunst">{haupt.update.neuerungen}</p>
|
|
</div>
|
|
)}
|
|
{haupt.update.bereit && (
|
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
<button
|
|
onClick={() => {
|
|
setUpdateHinweis('')
|
|
void window.rippy.updateInstallieren().then((urteil) => {
|
|
if (!urteil.ok) setUpdateHinweis(urteil.grund)
|
|
})
|
|
}}
|
|
disabled={einRipLaeuft}
|
|
className="rounded-lg border border-gold/40 bg-gold/15 px-3 py-1.5 text-xs font-semibold text-gold-hell hover:bg-gold/25 disabled:opacity-35"
|
|
title={einRipLaeuft ? 'Erst wenn der Rip fertig ist — Rippy startet dafür neu.' : 'Rippy beendet sich, installiert und startet wieder.'}
|
|
>
|
|
Jetzt neu starten und installieren
|
|
</button>
|
|
<span className="font-mono text-[11px] text-nebel">… oder einfach nichts tun: Beim nächsten Beenden passiert es von selbst.</span>
|
|
</div>
|
|
)}
|
|
{updateHinweis.length > 0 && <p className="mt-2 font-mono text-xs text-warn">{updateHinweis}</p>}
|
|
</div>
|
|
)}
|
|
|
|
<section>
|
|
{verbindung.laufwerke.length === 0 ? (
|
|
<div className="flex items-center gap-5 rounded-2xl border border-white/10 bg-karte/50 p-6">
|
|
<IconDisc groesse={44} farbe="oklch(0.4 0.02 75)" />
|
|
<p className="font-mono text-xs text-nebel">{verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'}</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{verbindung.laufwerke.map((geraet) => {
|
|
const ripVorgangId = verbindung.rips[geraet.id]?.vorgangId ?? ''
|
|
const laeuft = verbindung.vorgaenge?.kompression.laeuft ?? null
|
|
const wartend = verbindung.vorgaenge?.kompression.wartend ?? []
|
|
return (
|
|
<LaufwerkKachel
|
|
key={geraet.id}
|
|
geraet={geraet}
|
|
rip={verbindung.rips[geraet.id]}
|
|
info={verbindung.discInfos[geraet.id]}
|
|
ripStartMs={verbindung.ripStart[geraet.id]}
|
|
protokoll={verbindung.ripProtokoll[geraet.id] ?? []}
|
|
verbindung={verbindung}
|
|
bericht={verbindung.gesundheit[geraet.id]}
|
|
vorgang={verbindung.vorgaenge?.vorgaenge.find((v) => v.id === ripVorgangId)}
|
|
kompression={laeuft !== null && laeuft.vorgangId === ripVorgangId ? laeuft : null}
|
|
wartePlatz={wartend.findIndex((w) => w.vorgangId === ripVorgangId) + 1}
|
|
automatik={verbindung.automatik[geraet.id]}
|
|
aufRippen={() => {
|
|
// Der Nutzer entscheidet selbst — ein Countdown endet.
|
|
automatikStopp(geraet.id)
|
|
// Erst die Titel lesen (20-120 s), dann waehlen lassen.
|
|
setDialog({ art: 'titel', id: geraet.id })
|
|
titelLesen(geraet.id)
|
|
}}
|
|
aufAendern={() => {
|
|
automatikStopp(geraet.id)
|
|
setDialog({ art: 'aendern', id: geraet.id })
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{verbindung.vorgaenge !== null && (
|
|
<section>
|
|
<KompressionsKarte stand={verbindung.vorgaenge.kompression} />
|
|
</section>
|
|
)}
|
|
|
|
<section>
|
|
<SpeicherKarte speicher={verbindung.speicher} statistik={verbindung.vorgaenge?.statistik ?? null} />
|
|
</section>
|
|
|
|
{verbindung.vorgaenge !== null && (
|
|
<section>
|
|
<VerlaufKarte vorgaenge={verbindung.vorgaenge.vorgaenge} aufAlle={aufRohDateien} />
|
|
</section>
|
|
)}
|
|
|
|
{dialog !== null && dialog.art === 'titel' && (
|
|
<TitelWahlDialog
|
|
id={dialog.id}
|
|
typ={verbindung.laufwerke.find((l) => l.id === dialog.id)?.typ ?? 'bluray'}
|
|
stand={verbindung.discTitel[dialog.id]}
|
|
info={verbindung.discInfos[dialog.id]}
|
|
einstellungen={verbindung.einstellungen}
|
|
regel={verbindung.vorgaenge?.regel ?? ''}
|
|
speicher={verbindung.speicher}
|
|
schliessen={() => setDialog(null)}
|
|
/>
|
|
)}
|
|
{dialog !== null && dialog.art === 'aendern' && (
|
|
<AendernDialog
|
|
id={dialog.id}
|
|
startText={verbindung.discInfos[dialog.id]?.titel ?? ''}
|
|
vorschlaege={verbindung.vorschlaege[dialog.id] ?? []}
|
|
schliessen={() => setDialog(null)}
|
|
/>
|
|
)}
|
|
|
|
{verbindung.meldungen.length > 0 && (
|
|
<section>
|
|
<div className="mb-2.5 font-mono text-[10px] tracking-[0.22em] text-nebel">EREIGNISSE</div>
|
|
<div className="max-h-56 space-y-1 overflow-y-auto rounded-2xl border border-white/10 bg-karte/50 p-4">
|
|
{verbindung.meldungen.map((zeile, i) => (
|
|
<p key={i} className="break-all font-mono text-[11px] text-dunst">
|
|
{zeile}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</>
|
|
)
|
|
}
|