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>
85 lines
3.6 KiB
JavaScript
85 lines
3.6 KiB
JavaScript
// ESLint (flat config) — v3-Umbau P1.
|
|
//
|
|
// Warum ueberhaupt: In dieses Repo schreiben nachts auch Agenten. Ohne Waechter laeuft
|
|
// der Stil auseinander, und typische React-Fehler (fehlende Hook-Abhaengigkeiten,
|
|
// bedingt aufgerufene Hooks) fallen erst im Betrieb auf.
|
|
//
|
|
// Bewusst streng bei Korrektheit, bewusst still bei Geschmack: Formatierung macht
|
|
// Prettier, nicht der Linter. Regeln, die nur Meinung sind, stehen hier nicht.
|
|
|
|
import js from "@eslint/js"
|
|
import globals from "globals"
|
|
import reactHooks from "eslint-plugin-react-hooks"
|
|
import reactRefresh from "eslint-plugin-react-refresh"
|
|
import tseslint from "typescript-eslint"
|
|
|
|
export default tseslint.config(
|
|
{ ignores: ["dist", "node_modules", "*.config.js"] },
|
|
|
|
{
|
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
files: ["src/**/*.{ts,tsx}"],
|
|
languageOptions: {
|
|
ecmaVersion: 2022,
|
|
globals: globals.browser,
|
|
},
|
|
plugins: {
|
|
"react-hooks": reactHooks,
|
|
"react-refresh": reactRefresh,
|
|
},
|
|
rules: {
|
|
...reactHooks.configs.recommended.rules,
|
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
|
|
// Die Regeln aus der React-Compiler-Generation (Plugin v7) finden im Bestand
|
|
// 18 echte Stellen: setState direkt im Effekt, Ref-Zugriff waehrend des Renderns,
|
|
// Komponenten im Render erzeugt. Das sind keine Fehlalarme — aber sie quer durch
|
|
// 10 000 Zeilen zu beheben ist die Arbeit von Etappe P5 (Feature-Slices), nicht
|
|
// von P1. Als Fehler waere das Gate ab Tag eins rot, und ein dauerhaft rotes Gate
|
|
// ist kein Gate mehr ("Rot ist ein Ergebnis, kein Aergernis").
|
|
// Darum: sichtbare Warn-Liste statt stumm abgeschaltet. Beim Zerlegen eines
|
|
// Bereichs wird sein Anteil mit erledigt; ist der Zaehler bei 0, kommen sie
|
|
// hier auf "error" und der Rueckfall ist verbaut.
|
|
"react-hooks/set-state-in-effect": "warn",
|
|
"react-hooks/refs": "warn",
|
|
"react-hooks/immutability": "warn",
|
|
"react-hooks/static-components": "warn",
|
|
|
|
// `any` ist in diesem Projekt an einigen Stellen bewusst gesetzt (Recharts-Rueckrufe,
|
|
// Fehlerobjekte in catch). Warnen statt blockieren — sonst wird die Regel abgeschaltet.
|
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
|
|
// Ungenutzte Variablen sind fast immer ein Rest von etwas Halbfertigem.
|
|
// Fuehrendes _ heisst "absichtlich ungenutzt" (Rueckrufe mit fester Signatur).
|
|
"@typescript-eslint/no-unused-vars": [
|
|
"error",
|
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
|
|
],
|
|
|
|
// Der Zaun um B-01: Geheimnisse gehoeren auf die Box, nicht in den Browser-Speicher.
|
|
// Wer localStorage fuer Bedienvorlieben nutzt, ist willkommen — wer ein Passwort oder
|
|
// einen Token hineinschreibt, soll hier haengen bleiben.
|
|
"no-restricted-syntax": [
|
|
"error",
|
|
{
|
|
selector:
|
|
"CallExpression[callee.object.name='localStorage'][arguments.0.value=/(?:password|passwort|token|secret|geheim|api_?key)/i]",
|
|
message:
|
|
"Geheimnisse gehoeren nicht in den localStorage (v3-Umbau P1, Befund B-01). " +
|
|
"Auf der Box ablegen: backend/services/geheimnisse.py.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// Tests duerfen mehr: Attrappen brauchen `any`, und sie schreiben absichtlich
|
|
// Geheimnisse in den localStorage, um zu beweisen, dass api() sie NICHT mitschickt.
|
|
{
|
|
files: ["src/**/*.test.{ts,tsx}", "src/test/**/*.ts"],
|
|
rules: {
|
|
"@typescript-eslint/no-explicit-any": "off",
|
|
"no-restricted-syntax": "off",
|
|
},
|
|
},
|
|
)
|