feat(v5): Etappe W-0 — das Geruest steht
Electron 44 + TypeScript, drei Prozesse (haupt / kern als utilityProcess / fenster mit React), Nachrichten-Schema mit Pruef-Funktionen, node:sqlite als einzige Datenbank-Stelle (kern/speicher/db.ts, § 6.7), Prozess-Leine im Haupt (Job Object aus beweise/leine.js — Begruendung fuer den Ort im Dateikopf), MessagePort direkt Fenster<->Kern, Einzelinstanz-Sperre, Kern-Neustart-Wache, strikte CSP im gebauten Fenster. Wächter-Tests nach § 4.3: koffi nur an zwei benannten Orten (R1), kein leerer catch und kein catch-mit-Leerwert (R2/R4), kein HTTP-Server (§ 4.1), node:sqlite nur in db.ts (§ 6.7). Dazu Datenbank- und Schema-Tests: 18/18 gruen, Typpruefung in drei Kontexten. Der W-0-Beweis (npm run smoke, echtes Programm): SMOKE: OK — fenster=geladen kern=pid:13676,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.0.0-w0 Nachgemessen ausserdem: taskkill /F auf den Haupt-Prozess (ohne /T, der Absturz-Fall aus rc10) — der Kern stirbt mit. Bau-Fallen dokumentiert (BAUEN.md): npm 11 blockt Electrons Install-Skript (§ 3.5), plugin-react 6 verlangt Vite 8 (deshalb 5.2), TypeScript bewusst auf 5.9.3 gepinnt statt tsgo 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
564bd17f84
commit
c539e65cd2
@@ -0,0 +1,119 @@
|
||||
// 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.
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { HauptStatus } from '../gemeinsam/nachrichten'
|
||||
import { aufVerbindung, verbindungStarten, type Verbindung } from './kernverbindung'
|
||||
|
||||
function istHauptStatus(wert: unknown): wert is HauptStatus {
|
||||
return (
|
||||
typeof wert === 'object' &&
|
||||
wert !== null &&
|
||||
typeof (wert as Record<string, unknown>).version === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
interface KachelProps {
|
||||
titel: string
|
||||
zustand: 'ok' | 'fehler' | 'wartet'
|
||||
zeilen: string[]
|
||||
}
|
||||
|
||||
function Kachel({ titel, zustand, zeilen }: KachelProps) {
|
||||
const punktFarbe =
|
||||
zustand === 'ok' ? 'bg-emerald-400' : zustand === 'fehler' ? 'bg-red-400' : 'bg-amber-400'
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700/60 bg-slate-800/40 p-4">
|
||||
<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">{titel}</h2>
|
||||
</div>
|
||||
<div className="mt-2 space-y-0.5">
|
||||
{zeilen.map((zeile, i) => (
|
||||
<p key={i} className="break-all font-mono text-xs text-slate-400">
|
||||
{zeile}
|
||||
</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,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.rippy.aufStatus((status) => {
|
||||
if (istHauptStatus(status)) setHaupt(status)
|
||||
})
|
||||
verbindungStarten()
|
||||
return aufVerbindung(setVerbindung)
|
||||
}, [])
|
||||
|
||||
const db = verbindung.kern?.datenbank ?? null
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-slate-900 text-slate-100">
|
||||
<header className="border-b border-slate-700/60 px-6 py-4">
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
Drei Prozesse, eine Datenbank, eine Leine — das Fundament für Rippy v5.
|
||||
</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>
|
||||
|
||||
<footer className="border-t border-slate-700/60 px-6 py-3 text-center text-xs text-slate-500">
|
||||
Rippy v5 · LucyAI · Claude · KrBrZ
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Was die Brücke (src/bruecke) der Seite unter window.rippy bereitstellt.
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
rippy: {
|
||||
bereit(): void
|
||||
aufStatus(horcher: (status: unknown) => void): void
|
||||
melden(art: string): void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './stil.css'
|
||||
|
||||
createRoot(document.getElementById('wurzel')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rippy</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wurzel"></div>
|
||||
<script type="module" src="./haupt.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
// Hält den EINEN MessagePort zum Kern — bewusst außerhalb von React:
|
||||
// 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
|
||||
// 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'
|
||||
|
||||
export interface Verbindung {
|
||||
verbunden: boolean
|
||||
kern: Extract<KernNachricht, { art: 'kern-bereit' }> | null
|
||||
pongLatenzMs: number | null
|
||||
}
|
||||
|
||||
type Horcher = (stand: Verbindung) => void
|
||||
|
||||
const stand: Verbindung = { verbunden: false, kern: null, pongLatenzMs: null }
|
||||
const horcher = new Set<Horcher>()
|
||||
let gestartet = false
|
||||
|
||||
function melden(): void {
|
||||
for (const h of horcher) h({ ...stand })
|
||||
}
|
||||
|
||||
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') {
|
||||
stand.kern = nachricht
|
||||
} else if (nachricht.art === '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')
|
||||
}
|
||||
melden()
|
||||
}
|
||||
port.start()
|
||||
port.postMessage({ art: 'ping', zeit: Date.now() })
|
||||
melden()
|
||||
}
|
||||
|
||||
/** Idempotent — der erste Aufruf registriert den Horcher und meldet der
|
||||
* Brücke, dass gepufferte Ports jetzt zugestellt werden können. */
|
||||
export function verbindungStarten(): void {
|
||||
if (gestartet) return
|
||||
gestartet = true
|
||||
window.addEventListener('message', (ereignis: MessageEvent) => {
|
||||
const daten: unknown = ereignis.data
|
||||
if (
|
||||
typeof daten === 'object' &&
|
||||
daten !== null &&
|
||||
(daten as Record<string, unknown>).art === 'kern-port' &&
|
||||
ereignis.ports.length > 0
|
||||
) {
|
||||
portAnnehmen(ereignis.ports[0])
|
||||
}
|
||||
})
|
||||
window.rippy.bereit()
|
||||
}
|
||||
|
||||
export function aufVerbindung(h: Horcher): () => void {
|
||||
horcher.add(h)
|
||||
h({ ...stand })
|
||||
return () => horcher.delete(h)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Grundstimmung wie das heutige Rippy: dunkel, ruhig. Mehr Gestaltung
|
||||
kommt mit der echten Oberfläche in Etappe W-5. */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
Reference in New Issue
Block a user