feat(v5): Etappe W-1 — Laufwerk: Wache, Typ, Auswerfen mit Nachsehen
Ampel / ampel (push) Successful in 1m21s
Ampel / ampel (push) Successful in 1m21s
kern/laufwerk in drei Schichten: codes.ts (Steuercodes HERGELEITET wie win_ioctl.py, gegen die Doku-Zahlen getestet), win32.ts (der einzige koffi-Ort im Kern, Muster aus beweise/laufwerk.js), disc.ts (Logik gegen die LaufwerkApi-Schnittstelle — classify mit den cdrom.py-Schwellen, Geraete-Info mit ready/empty/unknown + Klartext-Grund, Auswurf als entriegeln→auswerfen→NACHSEHEN mit injizierbarem Warten). Dazu wache.ts im 3-Sekunden-Takt (§ 5 Plan A): Uebergangslogik pur, gescheiterte Runde behaelt den Stand (R2) und meldet laut (R4). Fenster zeigt Laufwerke live (Modell, Typ, Groesse, Grund) mit Auswerfen-Knopf; Ereigniszeilen fuer Disc rein/raus und Fehler. Bündel-Falle gefunden und behoben: Ein woertliches require überlebt Rollup nicht — win32 wird per dynamischem import() als eigener Chunk gebaut; der Fehler war dank R4 im Fenster sichtbar statt still. Gemessen am echten BU40N (30.08.2026, RIPPY_MESSUNG=1): Laufwerk G: status=ready typ=bluray groesse=33759690752 modell=HL-DT-ST BD-RE BU40N 1.03 serial=0025114C0149 Der Auswurf-Beweis (messung.auswurf) folgt am Ende der Sitzung — der BU40N kann die Schublade nicht selbst einziehen. 52 Tests gruen (Steuercodes, classify, Auswurf-Ablauf, Geraete-Info, Wache, Smoke weiterhin gruen mit Laufwerks-Kachel im Bild). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6ed8035dcc
commit
a45069af5e
@@ -1,9 +1,9 @@
|
||||
// W-0-Statusbild: zeigt, dass die drei Prozesse stehen und miteinander
|
||||
// reden. Die echte Oberfläche (Dashboard, Bibliothek, Einstellungen) ist
|
||||
// Etappe W-5 — hier geht es nur darum, das Gerüst SICHTBAR zu beweisen.
|
||||
// Statusbild W-0/W-1: die drei Prozesse plus die echten Laufwerke aus der
|
||||
// Disc-Wache. Die große Oberfläche (Dashboard, Bibliothek, Einstellungen)
|
||||
// ist Etappe W-5 — hier wird das Gerüst SICHTBAR bewiesen.
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { HauptStatus } from '../gemeinsam/nachrichten'
|
||||
import { aufVerbindung, verbindungStarten, type Verbindung } from './kernverbindung'
|
||||
import type { GeraeteInfo, HauptStatus } from '../gemeinsam/nachrichten'
|
||||
import { aufVerbindung, auswerfen, verbindungStarten, type Verbindung } from './kernverbindung'
|
||||
|
||||
function istHauptStatus(wert: unknown): wert is HauptStatus {
|
||||
return (
|
||||
@@ -13,6 +13,18 @@ function istHauptStatus(wert: unknown): wert is HauptStatus {
|
||||
)
|
||||
}
|
||||
|
||||
function gb(bytes: number): string {
|
||||
return `${(bytes / 1e9).toFixed(2).replace('.', ',')} GB`
|
||||
}
|
||||
|
||||
const TYP_NAMEN: Record<GeraeteInfo['typ'], string> = {
|
||||
cd: 'Audio-CD',
|
||||
dvd: 'DVD',
|
||||
bluray: 'Blu-ray',
|
||||
uhd: '4K-UHD',
|
||||
unknown: 'unbekannt',
|
||||
}
|
||||
|
||||
interface KachelProps {
|
||||
titel: string
|
||||
zustand: 'ok' | 'fehler' | 'wartet'
|
||||
@@ -39,12 +51,52 @@ function Kachel({ titel, zustand, zeilen }: KachelProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function LaufwerkKachel({ geraet }: { geraet: GeraeteInfo }) {
|
||||
const zustand = geraet.status === 'ready' ? 'ok' : geraet.status === 'empty' ? 'wartet' : 'fehler'
|
||||
const punktFarbe =
|
||||
zustand === 'ok' ? 'bg-emerald-400' : zustand === 'fehler' ? 'bg-red-400' : 'bg-slate-500'
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700/60 bg-slate-800/40 p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`inline-block h-2.5 w-2.5 rounded-full ${punktFarbe}`} />
|
||||
<h2 className="text-sm font-semibold tracking-wide text-slate-200">{geraet.name}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => auswerfen(geraet.id)}
|
||||
disabled={geraet.status !== 'ready'}
|
||||
className="rounded-lg border border-slate-600 px-3 py-1 text-xs text-slate-300 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
title="Entriegeln, auswerfen, nachsehen — in dieser Reihenfolge"
|
||||
>
|
||||
Auswerfen
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-0.5 font-mono text-xs text-slate-400">
|
||||
<p>{geraet.modell.length > 0 ? geraet.modell : 'Modell unbekannt'}</p>
|
||||
{geraet.status === 'ready' && (
|
||||
<p className="text-slate-200">
|
||||
{TYP_NAMEN[geraet.typ]}
|
||||
{geraet.groesseBytes > 0 ? ` · ${gb(geraet.groesseBytes)}` : ''}
|
||||
</p>
|
||||
)}
|
||||
{geraet.status === 'empty' && <p>keine Disc eingelegt</p>}
|
||||
{geraet.status === 'unknown' && <p className="text-red-300">{geraet.grund}</p>}
|
||||
{geraet.status !== 'unknown' && geraet.grund.length > 0 && (
|
||||
<p className="text-amber-300">{geraet.grund}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [haupt, setHaupt] = useState<HauptStatus | null>(null)
|
||||
const [verbindung, setVerbindung] = useState<Verbindung>({
|
||||
verbunden: false,
|
||||
kern: null,
|
||||
pongLatenzMs: null,
|
||||
laufwerke: [],
|
||||
meldungen: [],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,52 +115,91 @@ export default function App() {
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Rippy</h1>
|
||||
<span className="font-mono text-xs text-slate-400">
|
||||
v{haupt?.version ?? '…'} · Etappe W-0 — Gerüst
|
||||
v{haupt?.version ?? '…'} · Etappe W-1 — Laufwerk
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
Drei Prozesse, eine Datenbank, eine Leine — das Fundament für Rippy v5.
|
||||
Drei Prozesse, eine Datenbank, eine Leine — und die Disc-Wache läuft.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main className="grid flex-1 content-start gap-4 p-6 sm:grid-cols-2">
|
||||
<Kachel
|
||||
titel="Kern (utilityProcess)"
|
||||
zustand={verbindung.kern !== null ? 'ok' : haupt?.kern.laeuft ? 'wartet' : 'fehler'}
|
||||
zeilen={
|
||||
verbindung.kern !== null
|
||||
? [
|
||||
`PID ${verbindung.kern.pid} · Node ${verbindung.kern.nodeVersion}`,
|
||||
`Neustarts: ${haupt?.kern.neustarts ?? 0}`,
|
||||
]
|
||||
: ['wartet auf Meldung …']
|
||||
}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Datenbank (node:sqlite)"
|
||||
zustand={db === null ? 'wartet' : db.ok ? 'ok' : 'fehler'}
|
||||
zeilen={db === null ? ['wartet auf Kern …'] : db.ok ? [db.pfad] : [db.fehler]}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Prozess-Leine (Job Object)"
|
||||
zustand={haupt === null ? 'wartet' : haupt.leine.ok ? 'ok' : 'fehler'}
|
||||
zeilen={
|
||||
haupt === null
|
||||
? ['wartet auf Haupt …']
|
||||
: haupt.leine.ok
|
||||
? ['gesetzt — Kern und Werkzeuge sterben mit Rippy']
|
||||
: [haupt.leine.fehler ?? 'nicht gesetzt']
|
||||
}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Fenster ↔ Kern (MessagePort)"
|
||||
zustand={verbindung.pongLatenzMs !== null ? 'ok' : 'wartet'}
|
||||
zeilen={
|
||||
verbindung.pongLatenzMs !== null
|
||||
? [`Ping → Pong in ${verbindung.pongLatenzMs} ms`]
|
||||
: ['Ping unterwegs …']
|
||||
}
|
||||
/>
|
||||
<main className="flex-1 space-y-6 p-6">
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
Laufwerke
|
||||
</h2>
|
||||
{verbindung.laufwerke.length === 0 ? (
|
||||
<p className="font-mono text-xs text-slate-500">
|
||||
{verbindung.verbunden ? 'Kein optisches Laufwerk gefunden.' : 'Warte auf den Kern …'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{verbindung.laufwerke.map((geraet) => (
|
||||
<LaufwerkKachel key={geraet.id} geraet={geraet} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
Gerüst
|
||||
</h2>
|
||||
<div className="grid content-start gap-4 sm:grid-cols-2">
|
||||
<Kachel
|
||||
titel="Kern (utilityProcess)"
|
||||
zustand={verbindung.kern !== null ? 'ok' : haupt?.kern.laeuft ? 'wartet' : 'fehler'}
|
||||
zeilen={
|
||||
verbindung.kern !== null
|
||||
? [
|
||||
`PID ${verbindung.kern.pid} · Node ${verbindung.kern.nodeVersion}`,
|
||||
`Neustarts: ${haupt?.kern.neustarts ?? 0}`,
|
||||
]
|
||||
: ['wartet auf Meldung …']
|
||||
}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Datenbank (node:sqlite)"
|
||||
zustand={db === null ? 'wartet' : db.ok ? 'ok' : 'fehler'}
|
||||
zeilen={db === null ? ['wartet auf Kern …'] : db.ok ? [db.pfad] : [db.fehler]}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Prozess-Leine (Job Object)"
|
||||
zustand={haupt === null ? 'wartet' : haupt.leine.ok ? 'ok' : 'fehler'}
|
||||
zeilen={
|
||||
haupt === null
|
||||
? ['wartet auf Haupt …']
|
||||
: haupt.leine.ok
|
||||
? ['gesetzt — Kern und Werkzeuge sterben mit Rippy']
|
||||
: [haupt.leine.fehler ?? 'nicht gesetzt']
|
||||
}
|
||||
/>
|
||||
<Kachel
|
||||
titel="Fenster ↔ Kern (MessagePort)"
|
||||
zustand={verbindung.pongLatenzMs !== null ? 'ok' : 'wartet'}
|
||||
zeilen={
|
||||
verbindung.pongLatenzMs !== null
|
||||
? [`Ping → Pong in ${verbindung.pongLatenzMs} ms`]
|
||||
: ['Ping unterwegs …']
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{verbindung.meldungen.length > 0 && (
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
Ereignisse
|
||||
</h2>
|
||||
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4">
|
||||
{verbindung.meldungen.map((zeile, i) => (
|
||||
<p key={i} className="font-mono text-xs text-slate-400">
|
||||
{zeile}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-slate-700/60 px-6 py-3 text-center text-xs text-slate-500">
|
||||
|
||||
@@ -2,43 +2,84 @@
|
||||
// Die Port-Übergabe vom Haupt passiert genau einmal je Fenster-Ladung;
|
||||
// React-Re-Mounts (StrictMode im Dev, HMR) dürfen sie nicht verlieren.
|
||||
//
|
||||
// R2 gilt auch hier: Der letzte bekannte Kern-Stand bleibt stehen und wird
|
||||
// R2 gilt auch hier: Der letzte bekannte Stand bleibt stehen und wird
|
||||
// jedem neuen Abonnenten sofort wiedergegeben — er wird nie auf »leer«
|
||||
// zurückgesetzt, nur weil gerade keine Nachricht kam.
|
||||
import { istKernNachricht, type KernNachricht } from '../gemeinsam/nachrichten'
|
||||
// zurückgesetzt, nur weil gerade keine Nachricht kam. Insbesondere die
|
||||
// Laufwerksliste: Sie ändert sich NUR durch eine neue Liste vom Kern.
|
||||
import { istKernNachricht, type GeraeteInfo, type KernNachricht } from '../gemeinsam/nachrichten'
|
||||
|
||||
export interface Verbindung {
|
||||
verbunden: boolean
|
||||
kern: Extract<KernNachricht, { art: 'kern-bereit' }> | null
|
||||
pongLatenzMs: number | null
|
||||
laufwerke: GeraeteInfo[]
|
||||
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
|
||||
meldungen: string[]
|
||||
}
|
||||
|
||||
type Horcher = (stand: Verbindung) => void
|
||||
|
||||
const stand: Verbindung = { verbunden: false, kern: null, pongLatenzMs: null }
|
||||
const MAX_MELDUNGEN = 8
|
||||
|
||||
const stand: Verbindung = { verbunden: false, kern: null, pongLatenzMs: null, laufwerke: [], meldungen: [] }
|
||||
const horcher = new Set<Horcher>()
|
||||
let gestartet = false
|
||||
let port: MessagePort | null = null
|
||||
|
||||
function melden(): void {
|
||||
for (const h of horcher) h({ ...stand })
|
||||
for (const h of horcher) h({ ...stand, laufwerke: [...stand.laufwerke], meldungen: [...stand.meldungen] })
|
||||
}
|
||||
|
||||
function portAnnehmen(port: MessagePort): void {
|
||||
stand.verbunden = true
|
||||
port.onmessage = (ereignis: MessageEvent) => {
|
||||
const nachricht: unknown = ereignis.data
|
||||
if (!istKernNachricht(nachricht)) return
|
||||
if (nachricht.art === 'kern-bereit') {
|
||||
function meldung(text: string): void {
|
||||
stand.meldungen = [text, ...stand.meldungen].slice(0, MAX_MELDUNGEN)
|
||||
}
|
||||
|
||||
function kernNachricht(nachricht: KernNachricht): void {
|
||||
switch (nachricht.art) {
|
||||
case 'kern-bereit':
|
||||
stand.kern = nachricht
|
||||
} else if (nachricht.art === 'pong') {
|
||||
break
|
||||
case 'pong':
|
||||
stand.pongLatenzMs = Math.max(0, Date.now() - nachricht.zeit)
|
||||
// Der Beweis für den Smoke-Lauf: Ping ging hin, Pong kam zurück.
|
||||
window.rippy.melden('kern-pong')
|
||||
break
|
||||
case 'kern-fehler':
|
||||
meldung(`⚠ ${nachricht.text}`)
|
||||
break
|
||||
case 'laufwerke':
|
||||
stand.laufwerke = nachricht.liste
|
||||
for (const ereignis of nachricht.ereignisse) {
|
||||
if (ereignis.art === 'disc-eingelegt') {
|
||||
meldung(`Disc erkannt in ${ereignis.geraet.name} — ${ereignis.geraet.typ}`)
|
||||
} else if (ereignis.art === 'disc-entnommen') {
|
||||
meldung(`Disc entnommen aus ${ereignis.geraet.name}`)
|
||||
} else if (ereignis.art === 'laufwerk-weg') {
|
||||
meldung(`Laufwerk ${ereignis.id}: nicht mehr da`)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'auswurf-ergebnis': {
|
||||
const text =
|
||||
nachricht.grund === 'ok'
|
||||
? `Laufwerk ${nachricht.id}: ausgeworfen (nachgesehen)`
|
||||
: `Laufwerk ${nachricht.id}: Auswurf ${nachricht.grund}${nachricht.fehler !== null ? ` — ${nachricht.fehler}` : ''}`
|
||||
meldung(nachricht.grund === 'ok' ? text : `⚠ ${text}`)
|
||||
break
|
||||
}
|
||||
melden()
|
||||
}
|
||||
port.start()
|
||||
port.postMessage({ art: 'ping', zeit: Date.now() })
|
||||
melden()
|
||||
}
|
||||
|
||||
function portAnnehmen(neuerPort: MessagePort): void {
|
||||
port = neuerPort
|
||||
stand.verbunden = true
|
||||
neuerPort.onmessage = (ereignis: MessageEvent) => {
|
||||
const nachricht: unknown = ereignis.data
|
||||
if (istKernNachricht(nachricht)) kernNachricht(nachricht)
|
||||
}
|
||||
neuerPort.start()
|
||||
neuerPort.postMessage({ art: 'ping', zeit: Date.now() })
|
||||
melden()
|
||||
}
|
||||
|
||||
@@ -63,6 +104,15 @@ export function verbindungStarten(): void {
|
||||
|
||||
export function aufVerbindung(h: Horcher): () => void {
|
||||
horcher.add(h)
|
||||
h({ ...stand })
|
||||
h({ ...stand, laufwerke: [...stand.laufwerke], meldungen: [...stand.meldungen] })
|
||||
return () => horcher.delete(h)
|
||||
}
|
||||
|
||||
export function auswerfen(id: string): void {
|
||||
if (port === null) {
|
||||
meldung(`⚠ Auswerfen ${id}: keine Verbindung zum Kern`)
|
||||
melden()
|
||||
return
|
||||
}
|
||||
port.postMessage({ art: 'auswerfen', id })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user