Vierte Etappe. Der Client-Zustand hatte bisher keinen Ort: zwei handgebaute
useSyncExternalStore-Speicher, drei CustomEvent-Kanaele und ein localStorage-Griff
mitten in einem useState — verteilt ueber fuenf Dateien.
## Ein Store (Befund B-08/B-09)
app/store.ts (Zustand, 1,2 kB gzip) haelt genau vier Dinge: Bedienvorlieben
(Schiene, Expertenmodus, Palette), die Lage des Ereignisstroms und die
Meldungs-Warteschlange. Der Kopfkommentar nennt die Regel, nach der etwas dort
landen darf — und ein Test prueft sie: gespeichert werden AUSSCHLIESSLICH
Schienen- und Expertenmodus-Zustand, kein Strom, keine Meldungen, kein Geheimnis.
Der Metrik-Verlauf bleibt bewusst DRAUSSEN (lib/metricsStore.ts): Er nimmt ab P4
jede Sekunde einen Messpunkt entgegen, ein Store-Update pro Sekunde wuerde jede
abonnierende Komponente neu rendern.
Die drei CustomEvent-Kanaele sind ERSATZLOS weg — `grep -r dispatchEvent src`
liefert nichts mehr. Wer die Schublade oeffnen will, setzt `?system=`; wer springen
will, navigiert. Auch der letzte Rest von P2 (`mc-palette-open`) ist aufgeloest.
## Statusleiste (Spezifikation §4.1)
Der Zustand der Box stand bisher verstreut: die Ampel unten in der Sidebar, die
Auslastung nur im Cockpit, das aktive Modell nur im Modell-Manager. Wer in der
Konsole arbeitete, sah gar nichts.
Jetzt eine feste Leiste ueber allen Ansichten: Motor/Hirn-Ampel (klickbar zum
Agenten) · aktive Rolle + Durchsatz mit Sparkline · geteilter Speicher ALS BALKEN
(nicht als Prozent — 128 GB Unified Memory ist die Kernzahl dieser Box) · CPU/GPU
· Temperatur (ab 85 °C bernstein) · Betriebszeit · Token-Summe · Live-Anzeige.
Die Sparklines sind eigenes SVG, ~20 Zeilen. Fuer eine 56-Pixel-Linie waere eine
Diagramm-Bibliothek Verschwendung.
Dafuer neu im Backend: `uptime_s` in /api/system/status (psutil.boot_time).
Gehoert dorthin, weil die Box sich woechentlich selbst neu startet — dann ist
"laeuft seit 20 Minuten" die Antwort auf eine ganze Klasse von Fragen.
Liefert psutil nichts, steht dort None und die Leiste blendet das Feld aus,
statt eine Zahl zu erfinden.
## aria-live (Befund B-11) — und diesmal mit Absender
Im ganzen Frontend gab es KEIN einziges aria-live. app/shell/Meldungen.tsx ist
jetzt die eine Stelle fuer beilaeufige Rueckmeldungen; Fehler bekommen
`assertive` und bleiben stehen, alles andere `polite` und raeumt sich weg.
Wichtig: Die Region ist keine Attrappe. Gespeist wird sie von den Uebergaengen des
Ereignisstroms und vom Konsolen-Neustart (mit dem echten Grund aus ApiError.detail).
Der erste Verbindungsaufbau wird bewusst NICHT gemeldet — sonst begruesst jede
Seite den Nutzer mit "Verbindung wieder da".
## Ehrliche Anzeige statt stillem Altern (Befund B-15/B-18)
lib/events.ts fuehrt die Stromlage jetzt im Store: live · nachlauf · getrennt.
Beim Wechsel wird EINMAL invalidiert — vorher las relax() den Zustand erst beim
naechsten Refetch, nach einem Riss blieb die UI bis zu 150 s im langsamen Modus.
## Bahnbreiten-Deckel (Befund B-10)
Die Arbeitsflaeche endet bei 1600 px. Ohne Deckel zog sich das Cockpit auf einem
3440-px-Ultrawide auf ueber 3 000 px, waehrend die Wurzel-Schriftgroesse mitwuchs
und die Zeilen GROESSER statt lesbarer machte.
## Nebenbei
Der Buendel-Budget-Check aus P0 war fragil: `ls dist/assets/index-*.js | head -1`
kann den falschen treffen, weil Rollup auch kleine geteilte Module "index-*.js"
nennt (gemessen: ein 67-Byte-Chunk neben dem 374-kB-Einstieg). Er waere dann still
immer gruen gewesen. Beide Ampel-Dateien lesen den Einstieg jetzt aus index.html.
## Verifiziert im Browser
Statusleiste: alle Felder mit Live-Daten, "Laeuft seit 1 Std 10 min"
Backend abgewuergt -> Leiste springt auf "Nachlauf" UND die aria-live-Region
meldet "Verbindung zur Zentrale verloren" (polite)
Backend zurueck -> "Live", ohne Begruessungs-Meldung
Cockpit-Kachel -> /cockpit?system=logs, Schublade offen, Reiter "System-Logs"
Arbeitsflaeche -> max-width 1600 px
33/33 Tests gruen (7 neue fuer den Store) · ESLint 0 Fehler · tsc sauber ·
Einstieg 118 153 B gzip / Budget 125 000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
6.5 KiB
YAML
124 lines
6.5 KiB
YAML
# Ampel-CI fuer MC2 — auf dieses Multi-Service-Monorepo zugeschnitten (24.07.2026).
|
||
#
|
||
# Die universelle Vorlage (deploy/ampel-ci.yml) passt fuer EIN-Service-Repos. MC2 hat
|
||
# 5 Python-Dienste (backend/voice_service/mem0_service/mcp/client) mit schweren ML-
|
||
# Abhaengigkeiten (Whisper/TTS/Embeddings) + ein Frontend. "pip install ALLER requirements
|
||
# + pytest repo-weit" in einem stateless Container ist weder machbar (GB-schwere Wheels,
|
||
# CUDA) noch aussagekraeftig — die echten Tests sind der Pruefstand (deploy/pruefstand)
|
||
# und die Integration auf der Box mit LIVE-Diensten/Modellen, nicht isolierte Unit-Tests.
|
||
#
|
||
# Darum prueft die MC2-Ampel, was im Container EHRLICH gruen sein kann und trotzdem echte
|
||
# Fehler faengt: Lint (ruff, Projekt-Politik in ruff.toml) + Import/Syntax (compileall) +
|
||
# Frontend-Build inkl. TypeScript-Typecheck (tsc) + Frontend-Tests (vitest/jsdom) + ESLint
|
||
# + Buendel-Budget. Rot ist ein Ergebnis, kein Aergernis.
|
||
name: Ampel
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
ampel:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Ampel — Lint + Import + Frontend-Build (MC2-Zuschnitt)
|
||
shell: bash
|
||
run: |
|
||
# Runner-bash laeuft mit -e; abschalten und JEDEN Fehler selbst werten (rot=1),
|
||
# am Ende EIN Klartext-Urteil (Lehre Ampel-Lauf #5).
|
||
set -u +e
|
||
rot=0
|
||
|
||
echo "== Python: Lint (ruff, Projekt-Politik aus ruff.toml) =="
|
||
python3 -m venv /tmp/ampel-venv && . /tmp/ampel-venv/bin/activate || { echo "❌ venv kaputt"; exit 1; }
|
||
pip install -q ruff || { echo "❌ ruff-Install kaputt"; exit 1; }
|
||
ruff check . || rot=1
|
||
|
||
echo "== Python: Import/Syntax (compileall) =="
|
||
python3 -m compileall -q backend voice_service mem0_service mcp client deploy hermes scripts || rot=1
|
||
|
||
echo "== Frontend: reproduzierbarer Build (npm ci + tsc + vite) =="
|
||
if [ -f frontend/package.json ]; then
|
||
if [ ! -f frontend/package-lock.json ]; then
|
||
echo "❌ frontend/: kein package-lock.json — Build nicht reproduzierbar."
|
||
rot=1
|
||
else
|
||
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
||
|
||
# Frontend-Tests + Linter (v3-Umbau P1, 28.08.2026).
|
||
# Bis dahin lief das Frontend als einziger Teil des Stacks voellig ungeprueft
|
||
# durch das Gate — 10 500 Zeilen, null Tests. Die Begruendung weiter oben
|
||
# ("echte Tests sind der Pruefstand mit LIVE-Diensten") gilt fuer die
|
||
# ML-schweren Python-Dienste, nicht fuer Frontend-Unit-Tests: die laufen in
|
||
# jsdom, brauchen weder Modell noch GPU und sind in Sekunden durch.
|
||
# Der Linter meldet 0 Fehler / ~90 Warnungen; rot wird nur bei Fehlern.
|
||
if [ $rot -eq 0 ]; then
|
||
echo "-- Vitest"
|
||
( cd frontend && npm test --silent ) || rot=1
|
||
echo "-- ESLint (Warnungen sind erlaubt, Fehler nicht)"
|
||
( cd frontend && npm run lint --silent ) || rot=1
|
||
fi
|
||
|
||
# Bündel-Budget (v3-Umbau P0, 28.08.2026): Der Start-Chunk ist das, was der
|
||
# Nutzer VOR dem ersten Bild lädt. Er lag bei 220 kB gzip, weil das eifrig
|
||
# geladene Cockpit Recharts mitzog; dist lag bei 27 MB wegen eines verwaisten
|
||
# Avatar-Modells. Ohne Deckel wächst beides unbemerkt zurück. Gemessen wird der
|
||
# FRISCHE Build im Runner — kein Vergleich mit dem committeten dist (das wäre
|
||
# über Node-Versionen hinweg flatterhaft und würde dauerhaft rot leuchten).
|
||
if [ $rot -eq 0 ] && [ -d frontend/dist/assets ]; then
|
||
budget_gz=125000 # Stand nach P0: 110 571 B gzip
|
||
budget_dist=3145728 # Stand nach P0: 1 477 852 B
|
||
# Den Einstiegs-Chunk aus index.html lesen, NICHT per Glob raten:
|
||
# Rollup nennt auch kleine geteilte Module "index-*.js" (gemessen: ein
|
||
# 67-Byte-Chunk neben dem 375-kB-Einstieg). `ls | head -1` haette je nach
|
||
# Hash den falschen erwischt — und das Budget waere still immer gruen.
|
||
einstieg=$(grep -o 'assets/index-[A-Za-z0-9_-]*\.js' frontend/dist/index.html | head -1)
|
||
haupt="frontend/dist/$einstieg"
|
||
if [ -n "$haupt" ]; then
|
||
gz=$(gzip -c "$haupt" | wc -c)
|
||
echo "-- Start-Chunk: $gz B gzip (Budget $budget_gz)"
|
||
if [ "$gz" -gt "$budget_gz" ]; then
|
||
echo "❌ Start-Bündel über Budget. Meist eine neue Bibliothek, die über"
|
||
echo " das eifrig geladene Cockpit hereinkommt — hinter lazy() legen."
|
||
rot=1
|
||
fi
|
||
fi
|
||
gesamt=$(du -sb frontend/dist | cut -f1)
|
||
echo "-- dist gesamt: $gesamt B (Budget $budget_dist)"
|
||
if [ "$gesamt" -gt "$budget_dist" ]; then
|
||
echo "❌ dist über Budget — Ballast? (Schrift-Subsets, Medien, WOFF 1)"
|
||
rot=1
|
||
fi
|
||
fi
|
||
|
||
if [ $rot -eq 0 ]; then
|
||
echo "== Frontend: Push nach release-dist =="
|
||
git config --global user.name "Gitea Actions"
|
||
git config --global user.email "actions@gitea.local"
|
||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||
# Erzeuge (oder hole) den orphan branch 'release-dist'
|
||
git checkout --orphan release-dist 2>/dev/null || git checkout release-dist
|
||
git rm -rf . >/dev/null 2>&1 || true
|
||
|
||
# Checkout nur frontend/dist vom aktuellen Stand
|
||
git add -f frontend/dist
|
||
|
||
if ! git diff-index --quiet HEAD; then
|
||
git commit -m "Automatischer Frontend Build ($CURRENT_COMMIT) [skip ci]"
|
||
# Push to release-dist. Token auth from runner is expected to work for Gitea Actions.
|
||
git push origin HEAD:release-dist -f
|
||
else
|
||
echo "Keine Änderungen am Frontend-Build."
|
||
fi
|
||
# Zurück zum originalen Branch für den Rest des Skripts
|
||
git checkout $CURRENT_COMMIT
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ $rot -ne 0 ]; then
|
||
echo "❌ AMPEL ROT — nichts heißt ‚fertig', solange das rot ist."
|
||
else
|
||
echo "✅ AMPEL GRÜN — Lint + Import + Frontend-Build sauber."
|
||
fi
|
||
exit $rot
|