Zweite Etappe. Kern: das Frontend traegt kein Geheimnis mehr, und es laeuft
nicht laenger als einziger Teil des Stacks ungeprueft durchs Gate.
## B-01 — Box-Sudo-Passwort: ersatzlos entfernt
Das Passwort lag im localStorage und reiste bei JEDEM mutierenden Request mit —
als Header `X-Sudo-Password` UND im JSON-Rumpf. Da /etc/sudoers den Dienst-Nutzer
mit `NOPASSWD: ALL` fuehrt, waere ein einziger XSS in der SPA Root auf der Box
gewesen.
AUF DER BOX GEMESSEN: `sudo -n true` laeuft durch. Das Passwort wurde also nie
gebraucht — es war reines Risiko ohne Gegenwert. Darum keine Umkonstruktion
(Sitzungs-Cookie o. AE.), sondern Loeschung, quer durch den ganzen Pfad:
frontend/src/lib/api.ts kein localStorage-Zugriff mehr
frontend/.../SettingsTab.tsx Eingabefeld weg, dafuer die Erklaerung warum
backend/routers/maintenance.py SudoReq entfaellt, 8 Endpunkte entschlackt
backend/services/maintenance.py _run() nutzt immer `sudo -n`
backend/services/jobengine.py keine stdin-Pipe mehr (DEVNULL)
`password_required` bleibt als ehrliches Signal: Verlangt sudo je doch ein
Passwort, ist das eine Konfigurations-Frage auf der Box — nichts, was man mit
einem im Browser geparkten Geheimnis uebertuencht.
## HuggingFace-Token: liegt jetzt auf der Box
Derselbe Fehler, kleinerer Radius. Neu: backend/services/geheimnisse.py — Datei
neben den anderen mc2-*.json, Rechte 0600, atomar geschrieben. Die Oberflaeche
erfaehrt nur, OB ein Token gesetzt ist, nie seinen Wert. Ein Schluessel-Allowlist
verhindert, dass ein fehlgeleiteter Request beliebige Felder hineinschreibt.
Prozess-Env (HF_TOKEN) hat Vorrang und wird als solche angezeigt.
Verifiziert gegen das lokale Backend: setzen/lesen/loeschen ok, unerlaubter
Schluessel wird mit Klartext-Grund abgewiesen, der Wert kommt nie zurueck.
## B-14 — Fehlermeldungen sagen jetzt, was los ist
api() warf `new Error("500 Internal Server Error")` und verwarf den Rumpf; der
eigentliche Grund aus FastAPIs `detail` erreichte die Oberflaeche nie. Neu:
ApiError mit status + detail, inklusive Validierungslisten und HTML-Fehlerseiten
(ein kaputter Rumpf darf die Meldung nicht in einen zweiten Fehler verwandeln).
Zwei Aufrufstellen zeigen den Grund jetzt statt "Fehler" (Discover, ModelBrowse).
## B-07 — Tests und Linter, ehrlich eingeordnet
Praezisierung gegenueber dem Audit: Die MC2-Ampel fuehrt bewusst GAR KEINE Tests
aus (dokumentiert: die Python-Dienste haengen an ML-Wheels, die echten Tests sind
Pruefstand + Box). Das ist fuer die Dienste richtig — fuer Frontend-Unit-Tests
nicht: die laufen in jsdom, brauchen weder Modell noch GPU, und sind in 1,3 s durch.
Vitest + Testing Library, 17 Tests in 3 Dateien
ESLint (flat config) + Prettier
Beides jetzt Teil der Ampel
Die Tests sind kein Feigenblatt: acht davon sind der Zaun um B-01 — sie beweisen,
dass api() weder Kopfzeilen noch Rumpf aus dem localStorage anreichert. Dazu eine
ESLint-Regel, die localStorage-Zugriffe auf Schluessel mit password/token/secret
im Namen hart abweist (an einer Probe verifiziert; harmlose Schluessel wie
mc_sidebar_collapsed bleiben erlaubt).
ESLint meldet 0 Fehler / 93 Warnungen. Die 18 Treffer der neuen React-Compiler-
Regeln (setState im Effekt, Ref-Zugriff im Render) sind ECHT, aber quer durch
10 500 Zeilen zu beheben ist P5-Arbeit. Sie stehen als sichtbare Warn-Liste statt
abgeschaltet — ein ab Tag eins rotes Gate ist kein Gate mehr.
## Nebenbefund, im Browser reproduziert: veraltetes Buendel nach Deploy
Ein Deploy ersetzt dist und startet den Dienst neu; offene Tabs behalten aber ihr
altes Start-Buendel, dessen Nachlade-Chunks nun fehlen — der naechste
Ansichtswechsel wirft. Galt schon fuer die 10 lazy Views, traf durch P0 nun auch
die Startseite. lib/veralteteVersion.ts faengt Vites `vite:preloadError` ab und
laedt EINMAL neu (Sperre in sessionStorage gegen Endlosschleife).
## Nachgemessen
Start-Chunk gzip 111 411 B · dist gesamt 1 480 097 B (beide im Ampel-Budget)
tsc --noEmit sauber · 17/17 Tests gruen · ESLint 0 Fehler
Im echten Browser gegen das lokale Backend geprueft: Einstellungen holen den
Token-Zustand von der Box, kein Passwort-Feld mehr, Knoepfe korrekt gesperrt,
beide Cockpit-Diagramme rendern (Achsen + Zeitachse sichtbar).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
6.1 KiB
YAML
119 lines
6.1 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
|
||
haupt=$(ls frontend/dist/assets/index-*.js 2>/dev/null | head -1)
|
||
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
|