Commander: „wenn man auf rippen starten klickt passiert irgendwas, was das
Programm nicht mag. Der Hintergrund ist einfach leer und er startet nix. Es
gibt auch keine fehlermeldung."
Nachgestellt: Dienst lokal gestartet, Disc-Karte geoeffnet, „Rippen starten"
geklickt. Browser-Konsole:
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
## Er startet sehr wohl — man sieht es nur nie
Waehrend die Seite leer war, lief der Job (per API gemessen: status
`processing`, progress 12). Das „er startet nix" ist also der zweite Teil
desselben Fehlers: Die Oberflaeche war weg, bevor sie ihn zeigen konnte.
## Die Ursache
`_job_kurz` in `bus/waechter.py` trug nur `status`, `progress`, `title` und
`error` — **keinen `type`**. Das UI fuegt aus `job.created` einen halben Job
in seine Liste ein, `LiveLogSection` liest `job.type.toUpperCase()`, und React
baut bei einem Fehler im Zeichnen den GESAMTEN Baum ab. Es gab in diesem
Projekt keine einzige Fehlergrenze — also blieb ein leerer Bildschirm ohne
jede Meldung.
## Drei Reparaturen, weil es drei Fehler waren
1. **Das Ereignis traegt den Job.** `type`, `device` und `startTime` fahren
mit. Sie aendern sich ueber die Lebenszeit eines Jobs nie, kosten also kein
zusaetzliches Ereignis — und ohne `startTime` stand in der Jobliste
sekundenlang „Invalid Date".
2. **Das UI vertraegt sein Fehlen.** `LiveLogSection` und `TypeBadge` nahmen
einen vollstaendigen Job an. Zeile 108 derselben Datei hatte das
Fragezeichen laengst, Zeile 48 nicht.
3. **Eine Fehlergrenze.** Ein Fehler in einer Karte darf nicht den ganzen
Bildschirm mitnehmen — und schon gar nicht schweigend. Jetzt steht da, was
los ist, die Navigation bleibt bedienbar, und der Hinweis sagt das
Wichtigste: laufende Rips gehen weiter.
## Warum es niemand gefunden hat
`unterschiede()` bekommt die Kurzform schon fertig, und alle Tests reichen
ihre eigenen Woerterbuecher herein. **`_job_kurz` selbst war nie geprueft.**
Jetzt bewacht `UI_PFLICHTFELDER` den Vertrag mechanisch — es gibt kein
gemeinsames Typsystem zwischen Python und dem UI.
Gegengeprueft im Browser: derselbe Klick, keine Konsolenfehler, Job erscheint
als „Alle (1)".
833 Tests gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
8.3 KiB
TypeScript
200 lines
8.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { LayoutDashboard, BookOpen, FileText, Settings as SettingsIcon } from 'lucide-react'
|
|
import Dashboard from './pages/Dashboard'
|
|
import SettingsPage from './pages/Settings'
|
|
import LogsPage from './pages/Logs'
|
|
import AnleitungPage from './pages/Anleitung'
|
|
import FirstRunWizard from './components/FirstRunWizard'
|
|
import { api } from './lib/api'
|
|
import { EventStreamProvider, useStrom } from './lib/useEventStream'
|
|
import { BetriebProvider } from './lib/useBetrieb'
|
|
import { Fehlergrenze } from './components/Fehlergrenze'
|
|
|
|
type Page = 'dashboard' | 'anleitung' | 'logs' | 'settings'
|
|
|
|
/**
|
|
* Zeigt, ob der Live-Strom steht.
|
|
*
|
|
* Hier stand bis V2-3 ein fest verdrahtetes „ONLINE" mit pulsierendem Punkt —
|
|
* es leuchtete auch dann gruen, wenn die API tot war. Genau die Sorte
|
|
* Placebo-Anzeige, die in Etappe 19 schon einmal aufgeraeumt wurde
|
|
* („Fortschritt log, Auswurf tat nichts, Encoder wurden behauptet statt
|
|
* gemessen"). Jetzt sagt sie die Wahrheit.
|
|
*/
|
|
function VerbindungsAnzeige() {
|
|
const { verbunden, zuletzt } = useStrom()
|
|
if (verbunden) {
|
|
return (
|
|
<div className="flex items-center gap-2 text-xs font-mono text-emerald-400 bg-emerald-500/10 px-3 py-1.5 rounded-xl border border-emerald-500/20">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
|
<span>LIVE</span>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div
|
|
className="flex items-center gap-2 text-xs font-mono text-amber-400 bg-amber-500/10 px-3 py-1.5 rounded-xl border border-amber-500/30"
|
|
title={
|
|
zuletzt
|
|
? `Letzte Meldung um ${zuletzt.toLocaleTimeString()}. Die Anzeige zeigt diesen Stand, bis die Verbindung zurueck ist.`
|
|
: 'Noch keine Verbindung zum Server.'
|
|
}
|
|
>
|
|
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
|
<span>VERBINDUNG WEG</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AppInhalt() {
|
|
const [currentPage, setCurrentPage] = useState<Page>('dashboard')
|
|
// First-Run: solange der Einrichtungs-Assistent nicht abgeschlossen ist, zeigen wir
|
|
// ihn statt des Dashboards. null = /setup noch nicht geprüft (kein Aufblitzen).
|
|
const [setupDone, setSetupDone] = useState<boolean | null>(null)
|
|
|
|
/*
|
|
* ⚠️ Ein fehlgeschlagener Abruf ist KEINE Aussage über die Einrichtung.
|
|
*
|
|
* Hier stand `.catch(() => setSetupDone(true))` mit der Begründung
|
|
* „API/Setup nicht erreichbar → nicht blockieren". Die Folge hat der
|
|
* Commander am 28.08.2026 gemeldet: **Nach einer frischen Installation
|
|
* fehlte der Ersteinrichtungs-Assistent.**
|
|
*
|
|
* Und zwar zwangsläufig: Das Fenster geht unmittelbar nach dem Setup auf,
|
|
* der Dienst braucht ein bis zwei Sekunden bis zur ersten Antwort — der
|
|
* erste Abruf scheitert also fast immer. Aus „ich konnte nicht nachsehen"
|
|
* wurde „ist schon erledigt", und der Assistent war für immer weg.
|
|
*
|
|
* Dasselbe Prinzip steht nebenan in `useEventStream.tsx`: Wer nichts Neues
|
|
* weiß, behält, was er wusste — und behauptet nichts.
|
|
*
|
|
* Deshalb: nachfragen, bis der Server ANTWORTET. Erst eine Antwort
|
|
* entscheidet.
|
|
*/
|
|
useEffect(() => {
|
|
let abgemeldet = false
|
|
let versuche = 0
|
|
|
|
const fragen = () => {
|
|
api.get('/setup')
|
|
.then((r) => { if (!abgemeldet) setSetupDone(Boolean(r.data?.done)) })
|
|
.catch(() => {
|
|
if (abgemeldet) return
|
|
versuche += 1
|
|
// Zwei Minuten lang alle zwei Sekunden. Antwortet der Server bis
|
|
// dahin nicht, hat der Nutzer ein anderes Problem als den
|
|
// Assistenten — dann zeigen wir die Oberfläche, damit er die Logs
|
|
// sehen kann.
|
|
if (versuche < 60) setTimeout(fragen, 2000)
|
|
else setSetupDone(true)
|
|
})
|
|
}
|
|
fragen()
|
|
return () => { abgemeldet = true }
|
|
}, [])
|
|
|
|
const navItems: { id: Page; label: string; icon: any }[] = [
|
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
{ id: 'anleitung', label: 'Anleitung', icon: BookOpen },
|
|
{ id: 'logs', label: 'Logs', icon: FileText },
|
|
{ id: 'settings', label: 'Einstellungen', icon: SettingsIcon },
|
|
]
|
|
|
|
// Solange /setup noch nicht geantwortet hat: sagen, dass gewartet wird.
|
|
// Ein leeres Fenster sieht aus wie ein Absturz — und nach einer frischen
|
|
// Installation ist genau das der erste Eindruck.
|
|
if (setupDone === null) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-[#0b0f19]">
|
|
<div className="text-center">
|
|
<div className="text-amber-400 text-3xl mb-3">◉</div>
|
|
<p className="text-slate-300 text-sm">Rippy startet …</p>
|
|
<p className="text-slate-500 text-xs mt-1 font-mono">
|
|
warte auf den Dienst
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
if (!setupDone) return <FirstRunWizard onDone={() => setSetupDone(true)} />
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#080b11] text-slate-100 font-sans selection:bg-amber-500/30 selection:text-amber-300">
|
|
{/* Top Header Navigation Bar */}
|
|
<header className="sticky top-0 z-50 bg-[#0c101d]/95 backdrop-blur-2xl border-b border-amber-500/20 shadow-xl shadow-black/50">
|
|
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
|
|
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setCurrentPage('dashboard')}>
|
|
<span className="text-2xl font-black tracking-wider bg-gradient-to-r from-amber-400 via-amber-300 to-amber-500 bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(245,158,11,0.4)] font-mono">
|
|
RIPPY
|
|
</span>
|
|
<span className="px-2.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest bg-amber-500/15 text-amber-400 border border-amber-500/30">
|
|
CINEMA OS
|
|
</span>
|
|
</div>
|
|
|
|
{/* Central Nav Tabs */}
|
|
<nav className="flex items-center gap-1.5 bg-[#121827] p-1.5 rounded-2xl border border-slate-800 shadow-inner">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon
|
|
const isActive = currentPage === item.id
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setCurrentPage(item.id)}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all duration-200 ${
|
|
isActive
|
|
? 'bg-amber-500 text-slate-950 shadow-md shadow-amber-500/20 font-bold scale-[1.02]'
|
|
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
|
|
}`}
|
|
>
|
|
<Icon size={15} className={isActive ? 'text-slate-950' : 'text-slate-400'} />
|
|
<span>{item.label}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<VerbindungsAnzeige />
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content Viewport */}
|
|
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Je Seite eine eigene Grenze: Faellt eine aus, bleibt die
|
|
Navigation stehen und man kommt woanders hin. Ohne das war am
|
|
29.08.2026 nach einem Klick auf „Rippen starten" der ganze
|
|
Bildschirm leer — samt Kopfzeile. */}
|
|
<Fehlergrenze bereich={
|
|
currentPage === 'dashboard' ? 'Das Dashboard'
|
|
: currentPage === 'anleitung' ? 'Die Anleitung'
|
|
: currentPage === 'logs' ? 'Die Protokolle' : 'Die Einstellungen'}>
|
|
{currentPage === 'dashboard' && <Dashboard />}
|
|
{currentPage === 'anleitung' && <AnleitungPage />}
|
|
{currentPage === 'logs' && <LogsPage />}
|
|
{currentPage === 'settings' && <SettingsPage />}
|
|
</Fehlergrenze>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
// Der Provider haelt GENAU EINE SSE-Verbindung fuer die ganze Anwendung.
|
|
// Ein Hook je Komponente waere eine Verbindung je Komponente — also das
|
|
// alte Problem in neuer Form.
|
|
// BetriebProvider ganz aussen: Was Rippy anzeigen DARF, entscheidet sich
|
|
// vor allem anderen. Ohne ihn zeigte das Windows-Fenster "Pruefen: docker
|
|
// compose ps" — einen Rat, den dort niemand befolgen kann.
|
|
return (
|
|
<Fehlergrenze>
|
|
<BetriebProvider>
|
|
<EventStreamProvider>
|
|
<AppInhalt />
|
|
</EventStreamProvider>
|
|
</BetriebProvider>
|
|
</Fehlergrenze>
|
|
)
|
|
}
|