Workers P2: Bühne (StrictMode-fest) und Board mit Pausenschalter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 04:20:35 +00:00
1 parent 319e731d0e
commit a6f1b0c56d
3 files changed
+274

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import Button from '../ui/Button'
import EmptyState from '../ui/EmptyState'
import { useWorkerTrace, useWorkersFeed } from '../../hooks/useWorkersFeed'
import { indexWorkers, reduceEvents } from '../../lib/workersEvents'
import { pauseLabel } from '../../lib/workersView'
import WorkerCard from './WorkerCard'
import WorkerList from './WorkerList'
import WorkersKpis from './WorkersKpis'
import WorkersSettingsDialog from './WorkersSettingsDialog'
import WorkersStage from './WorkersStage'
const RUNNING_TEXT = 'Läuft'
const PAUSED_TEXT = 'Angehalten: Es wird nichts gesendet und nichts gebaut.'
const PAUSE_FAILED_TEXT = 'Umschalten fehlgeschlagen.'
const NO_REASON_TEXT = 'Grund nicht angegeben'
// Grund der Notbremse für den Satz „Versand gestoppt: <Grund>. …“: getrimmt, ohne abschließende Punkte (sonst entstünde „..“)
function haltReasonOf(reason) {
const text = typeof reason === 'string' ? reason.trim().replace(/[.\s]+$/, '') : ''
return text || NO_REASON_TEXT
}
// Inhalt des Reiters „Hochhaus“: Kopfzeile mit Pausenschalter, Kennzahlen, Hochhaus mit Seitenspalte (Karte und Liste).
// Ohne Auth-Bezug: isAdmin kommt von der Seite. stageRef ist optional (nur der Prüfstand reicht eine durch); ohne eins legt das Board
// selbst eins an, damit pushEvents die Bühne immer erreicht.
export default function WorkersBoard({ api, isAdmin, stageRef }) {
const ownRef = useRef(null)
const ref = stageRef ?? ownRef
const aliveRef = useRef(true)
const [workers, setWorkers] = useState({})
const [selectedId, setSelectedId] = useState(null)
const [settingsOpen, setSettingsOpen] = useState(false)
const [pausing, setPausing] = useState(false)
const [pauseError, setPauseError] = useState('')
useEffect(() => {
aliveRef.current = true
return () => { aliveRef.current = false }
}, [])
// Neue Ereignisse: Tabelle fortschreiben und der Bühne vorspielen (die Bühne kennt den Rückstau selbst)
const onEvents = useCallback((events) => {
setWorkers((w) => reduceEvents(w, events))
if (ref.current) ref.current.pushEvents(events)
}, [ref])
const { state, error, loaded, refresh } = useWorkersFeed(api, { onEvents })
useEffect(() => {
if (state) setWorkers(indexWorkers(state))
}, [state])
const { trace, loading: traceLoading, error: traceError } = useWorkerTrace(api, selectedId)
if (!loaded || !state) {
if (error) {
return (
<EmptyState
title="Der Hustler-Dienst ist noch nicht erreichbar"
hint={<p>{error}</p>}
action={<Button type="button" onClick={() => refresh()}>Neu laden</Button>}
/>
)
}
return <div className="loading">Wird geladen …</div>
}
const paused = Boolean(state.paused)
const worker = selectedId && Object.hasOwn(workers, selectedId) ? workers[selectedId] : undefined
// Pause für alle Mitarbeiter: der Dienst bekommt den Zielwert, danach wird der Zustand sofort neu geholt
const togglePause = async () => {
if (pausing) return
setPausing(true)
setPauseError('')
try {
await api.pause(!paused)
refresh()
} catch (err) {
if (aliveRef.current) setPauseError(err?.message || PAUSE_FAILED_TEXT)
}
if (aliveRef.current) setPausing(false)
}
return (
<>
<div className="wk-toolbar">
<span className="wk-status">{paused ? PAUSED_TEXT : RUNNING_TEXT}</span>
<span className="grow" />
<Button type="button" onClick={togglePause} disabled={pausing}>{pauseLabel(paused)}</Button>
{isAdmin && <Button type="button" onClick={() => setSettingsOpen(true)}>Einstellungen</Button>}
</div>
{pauseError && <div className="msg err" role="alert">{pauseError}</div>}
{state.sendingHalted === true && (
<div className="msg err" role="alert">
Versand gestoppt: {haltReasonOf(state.sendingHaltedReason)}. Ein Admin kann ihn in den Einstellungen freigeben.
</div>
)}
{error && <div className="msg warn" role="status">Verbindung gestört: {error} Es wird weiter versucht.</div>}
<WorkersKpis kpis={state.kpis} />
<div className="wk-main">
<WorkersStage ref={ref} state={state} selectedId={selectedId} onSelect={setSelectedId} />
<div className="wk-side">
<WorkerCard
workerId={selectedId}
worker={worker}
trace={trace}
loading={traceLoading}
error={traceError}
onClose={() => setSelectedId(null)}
/>
<WorkerList workers={workers} selectedId={selectedId} onSelect={setSelectedId} />
</div>
</div>
{isAdmin && settingsOpen && (
<WorkersSettingsDialog api={api} onClose={() => setSettingsOpen(false)} onSaved={() => refresh()} />
)}
</>
)
}