Zum Hauptinhalt springen

Architektur

Next.js 16 (App Router) als ein Prozess, läuft als root nativ auf Debian 13 (systemd-Dienst bunkerbox, Ablage unter /appdata/bunkerbox, Port 80). Docker wird nur für die Apps der Benutzer verwendet.

MVC-Schichten​

src/
app/ Routing only. Pages und route.ts sind dünn und rufen nur Controller bzw. Views auf.
login/page.tsx -> views/auth/LoginView (öffentlich)
(admin)/layout.tsx Sitzungsprüfung + views/shell/AdminShell (Seitenleiste, Kopfzeile, Fußzeile)
(admin)/page.tsx -> views/dashboard/DashboardView
(admin)/<bereich>/** -> views/<bereich>/**
api/**/route.ts -> controllers/* (über handler() aus controllers/http.ts)
proxy.ts Zugriffsschutz für alles (Sitzung, Origin-Prüfung)
instrumentation.ts baut die Runtime beim Serverstart
models/ M: Domain-Typen, DTOs und Persistenz
types.ts Kern-Typen (Msg, Settings, UpdateStatus, ActivityItem, Auth-DTOs)
<feature>.ts Typen eines Bereichs (storage, users, shares, mounts, apps, dashboard)
validation.ts strenge Namens- und Pfadmuster für alle Bereiche
JsonStore.ts atomares JSON-Schreiben (tmp + fsync + rename)
JsonSettingsRepository data/settings.json (600)
JsonSessionRepository data/sessions.json (600), Schlüssel = sha256 des Tokens
services/ Domain-Logik und Systemzugriff (Teil des Model-Layers)
contracts.ts Kern-Interfaces, ServiceError, Runtime, ServiceContext
core.ts Singletons für Einstellungen und Anmeldung (auch vom Proxy genutzt)
runtime.ts Composition Root, Singleton auf globalThis
auth/ Anmeldung: AuthService, Linux-/Mock-Adapter, Sitzungen, Rate-Limit, Origin-Prüfung
<feature>/ contract.ts, Default<F>Service, Linux<F>System, Mock<F>System, index.ts (Factory)
system/exec.ts Programme starten (nur Argument-Arrays, nie eine Shell)
HttpUpdateChecker Update-Prüfung und -Start
controllers/ C: Validierung, Mapping, HTTP-Fehler
views/ V: React Client Components, sprechen per fetch mit /api/*
i18n/ Übersetzungen (de, en) und t/msg/render

Regeln:

  • app/ enthält keine Logik.
  • Controller kennen nur getRuntime() (bzw. core.ts) und Typen aus models/.
  • Views kennen nur Typen aus models/ und sprechen per fetch mit /api/*. Kein Import aus services/ oder controllers/ in views/.
  • Systemzugriff ausschließlich über das System-Interface des Bereichs (<F>System) und exec().

Bereichs-Module (Feature-Konvention)​

Jeder Bereich ist ein eigenes Modul. Die gemeinsamen Registrierungspunkte (Runtime, Seitenleiste, i18n-Index, Verträge) sind vorverdrahtet; ein Bereich ändert nur seine eigenen Dateien.

TeilPfad
Typen/DTOssrc/models/<feature>.ts (plus eigene Repositories src/models/<Feature>*.ts)
Vertragsrc/services/<feature>/contract.ts (Service-Interface fest, System-Interface frei erweiterbar)
Servicesrc/services/<feature>/Default<Feature>Service.ts
Systemzugriffsrc/services/<feature>/Linux<Feature>System.ts, Mock<Feature>System.ts, reine Parser/Generatoren daneben
Factorysrc/services/<feature>/index.ts (create<Feature>Service(ctx))
Controllersrc/controllers/<Feature>Controller.ts
Routensrc/app/api/<feature>/**
Seitensrc/app/(admin)/<feature>/** (Dashboard: src/app/(admin)/page.tsx)
Viewssrc/views/<feature>/**, Fetch-Client src/views/<feature>/api.ts, eigene Styles als CSS-Module
Textesrc/i18n/messages/{de,en}/<feature>.ts, Schlüssel beginnen mit <feature>.
Teststest/<feature>/*.test.ts
Dokudocs/<seite>.md und site/i18n/en/docusaurus-plugin-content-docs/current/<seite>.md

Bereiche: storage (Speicher, mit den Erweiterungen spindown und burnin unter src/services/storage/spindown|burnin/), users (Benutzer & Gruppen), shares (Freigaben), mounts (Netzlaufwerke), apps (Docker), dashboard, virtualization (Incus), backup, notifications (E-Mail), ups (NUT), security (HTTPS, 2FA), audit (Audit-Log), logs (Systemprotokoll), templates (App-Vorlagen), files (Dateimanager), terminal (Browser-Terminal), network (Netzwerk).

Dashboard-Karten neuer Bereiche liegen in src/views/<feature>/DashboardCard.tsx (holen ihre Daten selbst über /api/<feature>/summary), Dashboard-Ereignisse und Fehler melden Bereiche über notifications.notify().

Jeder Service erweitert FeatureService: start() startet Hintergrundaufgaben (einmalig, idempotent), activity() meldet länger dauernde Vorgänge (ActivityItem). Solange runtime.activity() nicht leer ist, verweigern die Oberfläche und update.sh ein Update. Andere Bereiche erreicht ein Service über ctx.runtime() (nur innerhalb von Methoden aufrufen, nie im Konstruktor), z. B. nutzt das Dashboard storage.overview(), shares.connections() und apps.summary().

Erwartete Fehler sind ServiceError(msg("<feature>.error.<grund>", params), status); handler() macht daraus { error, key, params } in der Sprache der Anfrage. Alles andere wird als 500 protokolliert.

Anmeldung und Zugriffsschutz​

  • Nur Mitglieder der Linux-Gruppe bunkerbox-admin (ADMIN_GROUP) können sich anmelden; normale NAS-Benutzer kommen nie in Oberfläche oder API.
  • Passwortprüfung (LinuxAuthAdapter): Der Dienst liest als root /etc/shadow, lehnt gesperrte (!, *), leere und abgelaufene Konten ab und vergleicht das Passwort per crypt(3) der System-libxcrypt über perl -e (yescrypt unter Debian 13, aber jedes vom System unterstützte Verfahren). Hash und Passwort gehen über stdin, nie als Argument (Argumente sind über /proc/<pid>/cmdline für alle sichtbar). Gründe: perl ist auf jedem Debian vorhanden, keine nativen Node-Module, kein zusätzliches PAM-Hilfsprogramm. Die Gruppenzugehörigkeit (zusätzliche Gruppe oder primäre GID) kommt aus /etc/group und /etc/passwd.
  • Mock (MockAuthAdapter): admin / admin.
  • Sitzungen (DefaultAuthService): 32 zufällige Bytes als Token, Cookie bb_session=<token>.<hmac> (HMAC-SHA256 mit data/session-secret, 600), HttpOnly, SameSite=Strict, Path=/, Secure hinter HTTPS. Serverseitig in data/sessions.json unter dem sha256 des Tokens. Inaktivitäts-Timeout (Standard 30 min) und absolute Dauer (Standard 12 h) aus den Einstellungen. logoutUser(name) beendet alle Sitzungen eines Benutzers (Passwortwechsel, Rolle entzogen, deaktiviert, gelöscht).
  • Zweite Stufe (TOTP): Der Bereich security meldet eine SecondFactorPolicy beim AuthService an. Verlangt sie eine zweite Stufe, entsteht beim Login eine Sitzung mit pendingSecondFactor; bis POST /api/auth/2fa/verify (ruft completeSecondFactor()) erreicht sie nur SECOND_FACTOR_PATHS (/login, /api/auth/me, /api/auth/logout, /api/auth/2fa, /api/auth/2fa/verify, /api/auth/2fa/enroll, /api/auth/2fa/confirm). Proxy, Admin-Layout und Gateway weisen solche Sitzungen sonst ab.
  • Rate-Limit: 5 Fehlversuche pro Client-Adresse bzw. 10 pro Benutzername in 15 min sperren für 15 min (429 mit Retry-After). Die Adresse stammt aus X-Forwarded-For; weil sie hinter einem Reverse-Proxy fälschbar ist, gilt zusätzlich das Limit pro Benutzername. Nach jedem Fehlversuch 400 ms Verzögerung. Falsches Passwort und fehlende Adminrechte ergeben dieselbe Meldung.
  • Proxy (src/proxy.ts, Node.js-Runtime): öffentlich sind nur /login, /api/auth/login, /api/auth/logout, /api/health und statische Dateien. Alles andere braucht eine gültige Sitzung: Seiten leiten auf /login?next=… um, API-Routen antworten 401. Der Proxy entfernt einen vom Client gesendeten Header x-bunkerbox-user und setzt ihn mit dem angemeldeten Benutzer (currentUser(request) in Controllern). Das Admin-Layout prüft die Sitzung zusätzlich.
  • Client-Adresse: Route-Handler sehen den Socket nicht. Der Dienst lädt deshalb server/preload.cjs vor (node --require ./preload.cjs server.js), das jeder Anfrage die Gegenstelle als x-bunkerbox-peer und das Protokoll als x-bunkerbox-proto mitgibt (vom Client gesendete Werte werden überschrieben). clientIp()/isHttps() in src/controllers/http.ts werten X-Forwarded-* nur aus, wenn diese Gegenstelle in BUNKERBOX_TRUSTED_PROXIES steht (src/services/auth/clientAddress.ts).
  • CSRF: Für ändernde API-Anfragen (nicht GET/HEAD/OPTIONS) muss Origin zum Host (bzw. X-Forwarded-Host) passen; ohne Origin nur mit Sec-Fetch-Site: same-origin. Zusätzlich verlangt readJson() Content-Type: application/json (erzwingt einen CORS-Preflight).
  • Sicherheits-Header: X-Frame-Options: DENY, frame-ancestors 'none', nosniff, Referrer-Policy: same-origin.

Server-Schicht: Vorladeskript, WebSocket-Gateway, HTTPS​

server/preload.cjs (ohne Abhängigkeiten, im Release neben server.js; in der Entwicklung über NODE_OPTIONS aus scripts/dev-mock.mjs) umhüllt den HTTP-Server von Next.js:

  • setzt x-bunkerbox-peer und x-bunkerbox-proto auf jeder Anfrage,
  • übergibt WebSocket-Upgrades unter /api/ws/ an das Gateway (globalThis[Symbol.for("bunkerbox.ws.upgrade")], von der Runtime registriert); andere Upgrades (HMR) bleiben bei Next.js,
  • startet optional einen HTTPS-Listener auf einem zweiten Port mit derselben Anwendung. Konfiguration data/server.json (ServerConfigFile: https.enabled, port, certFile, keyFile, redirectHttp), beim Start gelesen und zur Laufzeit über serverControl().applyTls() (src/services/gateway/contract.ts) umschaltbar, ohne Neustart. Ungültige Zertifikate behalten den bisherigen Zustand. Mit redirectHttp leitet Port 80 auf HTTPS um (308), außer für Loopback-Clients (Health-Check von update.sh).

Gateway (src/services/gateway/): DefaultGateway prüft jedes Upgrade wie der Proxy: Origin muss vorhanden sein und zum Host passen, gültige Admin-Sitzung (bb_session) ohne offene zweite Stufe, bekannter Kanal, höchstens 16 Verbindungen pro Benutzer. Offene Verbindungen werden alle 30 s erneut gegen die Sitzung geprüft (Abmeldung oder Ablauf schließt mit 4401). frames.ts (RFC 6455, rein, getestet) und socket.ts (Ping/Pong, Fragmentierung, 1 MiB pro Nachricht, Herzschlag) sind ohne Fremdpakete.

APIBedeutung
runtime.gateway.register({ name, open(socket, request) })Kanal /api/ws/<name> anmelden (im start() des Bereichs)
WsRequestuser, ip, locale, params (Query, ungeprüft), secure, userAgent
WsSocketsend(string | Uint8Array), sendJson, close(code, reason), onMessage, onClose, onDrain, bufferedAmount
Fehler in open()ServiceError -> { type: "error", error, key, params } und Close-Code 4000 + Status
BrowseropenChannel(name, params) aus src/views/ws.ts

Terminal-Protokoll (src/models/terminal.ts, auch für die Incus-Konsole): Ausgabe als Binär-Frames, Eingaben als Text-Frames { type: "input", data } und { type: "resize", cols, rows }, Steuermeldungen ready, exit, error. Pseudo-Terminals ohne native Module: script -qfec (util-linux), Größe über stty -F /dev/pts/N; im Mock ein Echo-Terminal (MockPty, auch unter Windows). Andere Bereiche nutzen runtime.terminal.spawn(spec) und runtime.terminal.bridge(socket, request, pty, target).

Audit-Log und Benachrichtigungen (bereichsübergreifend)​

  • Audit: handler(fn, { audit: "<bereich>.<objekt>.<aktion>" }) in src/controllers/http.ts schreibt nach jeder ändernden Anfrage (POST, PUT, PATCH, DELETE) einen Eintrag (Benutzer, Adresse, Aktion, Ziel, Ergebnis ok/failed/denied, Status, Fehlerschlüssel). Ziel sind die Routen-Parameter; auditedJson(request, result, status) nimmt result.name, auditBodyTarget(request, body, feld) ein Feld des Bodys, auditTarget(request, ziel, details) setzt es direkt. { audit: false } nur für reine Prüfungen und Routen, die selbst protokollieren (Anmeldung). test/core/audit.test.ts verlangt die Angabe bei jeder ändernden Route. Andere Aktionen (Terminal-Sitzungen, zweite Stufe) rufen runtime.audit.record(input) auf. Nie Geheimnisse in target oder details.
  • Benachrichtigungen: runtime.notifications.notify({ type, severity, key, subject, body, details }) ist „fire and forget“ (wirft nie, blockiert nie). Ereignistypen in NOTIFICATION_EVENT_TYPES (src/models/notifications.ts); key dient der Entprellung (z. B. pool-degraded:tank). Betreff und Text sind Msg und werden in der eingestellten Sprache gerendert. SMTP-Zugang, Empfänger und Schalter liegen in data/notifications.json, das Passwort in data/secrets/smtp-password (beide 600), nicht in bunkerbox.env.

API (Kern)​

MethodePfadBeschreibung
POST/api/auth/loginLoginRequest -> MeResponse + Cookie; 401, 429
POST/api/auth/logoutbeendet die Sitzung, löscht das Cookie
GET/api/auth/meMeResponse
GET/api/healthöffentlich: HealthResponse { ok, version, mock, activeOperations }
GET/PUT/api/settingsSettingsResponse; PUT mit Teil-Settings (language, autoUpdate, sessionIdleMinutes 5..1440, sessionAbsoluteHours 1..168)
GET/api/updateUpdateResponse { update, activity, autoUpdate }
POST/api/updateStartUpdateRequest -> 202; 409 bei falscher Version, nicht installierbar, laufendem Update oder Aktivität

Fehler: JSON { error, key, params }. error ist in der Sprache der Anfrage übersetzt (Cookie bb_lang, sonst Accept-Language, sonst Settings.language).

API der Bereiche​

Jeder Bereich pflegt nur seinen eigenen Abschnitt.

Speicher (storage)​

MethodePfadBeschreibung
GET/api/storageStorageOverview (ZFS verfügbar, Platten, Pools, ARC)
GET/api/storage/disksDisk[]
POST/api/storage/disks/refreshSMART sofort neu lesen -> Disk[]
GET/api/storage/disks/[id]DiskDetail mit SMART-Attributen
POST/api/storage/layout/check{ layout, pool? } -> LayoutCheck (mit pool: als Erweiterung dieses Pools)
GET/api/storage/layout/suggest?disks=a,bLayoutSuggestion[]
GET/POST/api/storage/poolsPool[] / CreatePoolRequest -> 201 Pool
GET/DELETE/api/storage/pools/[name]Pool / { confirm }
POST/api/storage/pools/[name]/exportExport
POST/api/storage/pools/[name]/vdevsAddVdevRequest
POST/api/storage/pools/[name]/replaceReplaceDiskRequest
POST/api/storage/pools/[name]/offline, …/onlineDeviceStateRequest
POST/api/storage/pools/[name]/scrub`{ action: "start"
PUT/api/storage/pools/[name]/scrub-schedule`{ schedule: ScrubSchedule
GET/POST/api/storage/importImportablePool[] / ImportPoolRequest -> 201 Pool
GET/POST/api/storage/datasets?pool=Dataset[] / CreateDatasetRequest -> 201
GET/PATCH/DELETE/api/storage/datasets/[...name]Dataset / UpdateDatasetRequest / DeleteDatasetRequest
PUT/api/storage/snapshot-schedules/[...name]`{ schedule: SnapshotSchedule
GET/POST/DELETE/api/storage/snapshots?dataset=Snapshot[] / CreateSnapshotRequest / { name }
POST/api/storage/snapshots/rollbackRollbackSnapshotRequest
GET/PUT/api/storage/arcArcOverview / ArcConfigRequest -> ArcConfig

Aufbau: DefaultStorageService (Regeln, typisierte Bestätigung, SMART-Cache 2 min, Zeitpläne im Minutentakt, ARC beim Start anwenden), LinuxStorageSystem (lsblk, /dev/disk/by-id, smartctl -j -n standby, zpool/zfs mit -j und Text-Fallback -Hp, arcstats, sysfs, modprobe.d), MockStorageSystem (Beispiel-Hardware im Speicher). Reine Module: parsers.ts, layout.ts (Prüfung, Vorschlag, zpool-Argumente), disks.ts (Verwendung, Systemplatte, by-id-Zuordnung), datasets.ts (Eigenschaften), schedule.ts, arc.ts. Zeitpläne und ARC-Override liegen in data/storage-state.json (600, JsonStorageRepository). Vor dem Löschen oder Exportieren fragt der Service shares.sharesBelow().

Benutzer & Gruppen (users)​

MethodeRouteErgebnis
GET / POST/api/usersNasUser[] / neuer NasUser (201)
GET / PATCH / DELETE/api/users/[name]NasUser; DELETE mit { confirm: name }
PUT/api/users/[name]/password{ ok: true }, setzt Linux- und Samba-Passwort
GET / POST/api/groupsNasGroup[] / neue NasGroup (201)
GET / PATCH / DELETE/api/groups/[name]NasGroup; DELETE mit { confirm: name }

DefaultUsersService arbeitet auf einem Abbild der Kontodatenbanken (AccountTables: passwd, group, Sperren aus shadow, Samba-Konten aus pdbedit -L -v, Gruppenbeschreibungen aus data/groups.json). Die reinen Funktionen in services/users/accounts.ts bauen daraus NasUser/NasGroup und berechnen Gruppenlisten, bei denen nicht verwaltete Gruppen (sudo, docker) erhalten bleiben. LinuxUsersSystem ruft useradd -M -N -g users -d /nonexistent -s /usr/sbin/nologin, usermod, userdel, groupadd, gpasswd -M, groupdel, chpasswd und smbpasswd -s mit Argument-Arrays auf, Passwörter nur über stdin. MockUsersSystem hält dieselben Tabellen im Speicher. Änderungen laufen nacheinander; das Löschen prüft über SharesService.list() Zugriffsregeln. Selbst-Löschen, -Deaktivieren und -Herabstufen verhindert der Controller.

Freigaben (shares)​

MethodePfadBeschreibung
GET/api/sharesShare[] (mit status)
POST/api/sharesCreateShareRequest -> 201 Share
GET/PATCH/api/shares/[name]Share; PATCH mit UpdateShareRequest
DELETE/api/shares/[name]{ confirm: <name> }; die Daten bleiben
POST/api/shares/[name]/permissionsACLs rekursiv im Hintergrund schreiben -> 202 (Aktivität shares-acl:<name>)
POST/api/shares/applyKonfiguration neu schreiben, Dienste neu laden -> ShareServicesStatus
GET/api/shares/servicesShareServicesStatus (Samba, NFS)
GET/api/shares/pathsSharePathOption[]: eingehängte Datasets (storage.listDatasets()) und Netzlaufwerke
GET/api/shares/connectionsShareConnections (smbstatus --json, /proc/fs/nfsd/clients)
GET/api/shares/previewShareConfigPreview: erzeugte Include- und Exportdatei
  • Ablage: data/share-definitions.json (600) über JsonShareRepository, dazu je Freigabe die zuletzt geschriebenen ACL-Regeln.
  • Reine Generatoren in services/shares/: smbConfig.ts (Include, include-Zeile, testparm-Auswertung), nfsExports.ts, acl.ts (setfacl-/find-Befehle), parsers.ts, validation.ts. Generatoren prüfen jede Freigabe erneut, bevor sie etwas ausgeben.
  • Ablauf jeder Änderung (serialisiert): Validierung -> Speichern -> testparm auf eine Kopie von smb.conf mit dem Kandidaten -> Include schreiben -> smbcontrol smbd reload-config -> Exporte schreiben -> exportfs -ra -> ACLs des Freigabeverzeichnisses. Bei einem Fehler wird die vorherige Definition wiederhergestellt und erneut angewendet.
  • SharesService hat zusätzlich applyPermissions(name) und preview().

Netzlaufwerke (mounts)​

MethodePfadBeschreibung
GET/api/mountsRemoteMount[] (Zustand, Fehler, usedByShares)
POST/api/mountsCreateRemoteMountRequest -> 201 RemoteMount (wird sofort eingehängt)
GET/PATCH/api/mounts/[name]RemoteMount; PATCH mit UpdateRemoteMountRequest (Passwort weggelassen = behalten, "" = entfernen)
DELETE/api/mounts/[name]{ confirm: <name> }; 409, solange Freigaben das Laufwerk nutzen
POST/api/mounts/[name]/mounteinhängen; 503 mit Journal-Auszug bei Fehler
POST/api/mounts/[name]/unmountaushängen; 409, wenn das Laufwerk belegt ist
  • Ablage: data/remote-mounts.json (600, ohne Passwörter) über JsonRemoteMountRepository; Passwörter nur in data/credentials/<name> (600).
  • Reine Teile in services/mounts/: units.ts (Unit-Namen wie systemd-escape --path, .mount/.automount, Optionen, Zugangsdatei), validation.ts, parsers.ts (/proc/mounts, systemctl show).
  • Zustand aus /proc/mounts und systemctl show, nie durch Zugriff auf den Einhängepunkt.

Apps (apps)​

MethodePfadBeschreibung
GET/api/appsAppStack[] (Verzeichnisse unter /appdata mit Compose-Datei, Container per docker compose ps)
POST/api/appsCreateStackRequest -> 201 AppStackDetail; 409 wenn vorhanden, 422 bei ungültiger Konfiguration
GET/api/apps/dockerDockerStatus
GET/api/apps/summaryAppsSummary (für das Dashboard)
POST/api/apps/validateValidateStackRequest -> StackValidation (docker compose config ohne Speichern)
GET/PUT/api/apps/[name]AppStackDetail; PUT mit UpdateStackRequest prüft vor dem Speichern
DELETE/api/apps/[name]DeleteStackRequest (confirm = Name, optional removeData, removeVolumes)
POST/api/apps/[name]/actions{ action } (up, down, restart, pull) -> 202 AppOperation; 409 solange ein Vorgang läuft
GET/api/apps/operations/[id]AppOperation mit Ausgabe
GET/api/apps/[name]/logs?tail=1..5000&since=<cursor>&service= -> StackLogs
  • Systemzugriff in LinuxAppsSystem: docker compose --project-name <name> --project-directory /appdata/<name> -f <datei>, immer als Argument-Array. Aktionen streamen ihre Ausgabe (--progress plain) in den Vorgang (die letzten 1000 Zeilen, im Speicher, die letzten 50 Vorgänge).
  • Prüfen schreibt den ungespeicherten Inhalt in ein temporäres Verzeichnis (700) und ruft docker compose config --format json auf; analyzeCompose() liefert Warnungen (privilegiert, Host-Namespaces, Docker-Socket, Systempfade).
  • .env wird mit 600, die Compose-Datei mit 644 atomar geschrieben. Das Löschen des Verzeichnisses verweigert symbolische Links und Mountpoints (eigene Datasets).
  • Log-Cursor: neuester Zeitstempel (9 Nachkommastellen); --since ist bei Docker inklusiv, gleiche Zeitstempel werden verworfen.
  • Reine Parser in services/apps/parsers.ts, .env-Helfer (auch für den Editor im Browser) in models/apps.ts.

Dashboard (dashboard)​

GET /api/dashboard -> DashboardSnapshot. DefaultDashboardService liest Host-Werte über DashboardSystem (Rohtexte aus /proc, /sys/class/net, sensors -j, /sys/class/hwmon) und wertet sie mit den reinen Parsern in services/dashboard/parsers.ts aus. MockDashboardSystem erzeugt dieselben Rohtexte, sodass im Mock-Modus dieselben Parser laufen. CPU-Auslastung und Netzwerkdurchsatz sind Differenzen zur vorherigen Messung (DeltaSampler: Hintergrundmessung alle 5 s, Messungen unter 1 s Abstand nutzen das letzte Ergebnis). Speicher, Freigaben und Apps werden nur über ihre Verträge abgefragt (storage.overview, shares.connections, apps.summary), jeweils mit 8 s Zeitlimit; Fehler ergeben leere Teile statt eines fehlgeschlagenen Snapshots. Anzeige-Regeln (Temperaturgrenzen, Speicheraufteilung) liegen in views/dashboard/derive.ts.

Virtualisierung (virtualization)​

MethodePfadBeschreibung
GET/api/virtualization/statusVirtualizationStatus (installiert, erreichbar, Pool, ISO-Dataset, Netze, Bridge, Host)
POST/api/virtualization/setupSetupVirtualizationRequest -> VirtualizationStatus (Pool, ISO-Dataset, NAT-Netz; vorhandene Teile bleiben)
GET/api/virtualization/summaryVirtualizationSummary (Dashboard)
GET/POST/api/virtualization/instancesInstance[] / CreateInstanceRequest -> 202 VirtualizationOperation
GET/PATCH/DELETE/api/virtualization/instances/[name]InstanceDetail / UpdateInstanceRequest / { confirm }
POST/api/virtualization/instances/[name]/actions{ action } (start, stop, restart, force-stop) -> 202
POST/api/virtualization/instances/[name]/sharesInstanceShareMount -> 201 InstanceDetail
DELETE/api/virtualization/instances/[name]/shares/[device]-> InstanceDetail
GET/POST/api/virtualization/instances/[name]/snapshotsInstanceSnapshot[] / { name } -> 201
POST/api/virtualization/instances/[name]/snapshots/[snapshot]/restore{ confirm } -> 202
DELETE/api/virtualization/instances/[name]/snapshots/[snapshot]{ confirm }
GET/api/virtualization/operations/[id]VirtualizationOperation
GET/api/virtualization/images?server=RemoteImage[] (nur IMAGE_SERVERS)
GET/api/virtualization/isosIsoImage[] mit usedBy
DELETE/api/virtualization/isos/[name]{ confirm }; 409 solange eingelegt
POST/api/virtualization/isos/uploads{ name, sizeBytes } -> 201 IsoUpload
PUT/DELETE/api/virtualization/isos/uploads/[id]?offset=Stück (application/octet-stream, höchstens ISO_CHUNK_BYTES) / abbrechen
POST/api/virtualization/isos/uploads/[id]/complete-> IsoImage
WS/api/ws/incus-console?instance=&mode=&cols=&rows=Konsole (mode = shell oder console, Terminal-Protokoll)
  • LinuxVirtualizationSystem spricht über IncusClient (node:http mit socketPath) mit der Incus-REST-API: /1.0/instances?recursion=2, /1.0/instances/<name> (PUT mit dem kompletten beschreibbaren Teil), /state, /snapshots, /1.0/operations/<id>/wait, /1.0/storage-pools, /1.0/networks. Asynchrone Incus-Vorgänge laufen als VirtualizationOperation im Speicher (Fortschritt aus den Metadaten, die letzten 100); activity() meldet laufende Vorgänge und ISO-Uploads. Instanzen im Anlegen erscheinen als Platzhalter, bis Incus sie listet.
  • Reine Module: incus.ts (Rohobjekte <-> Modelle, Anfragekörper, Größen, Fortschritt), simplestreams.ts (Image-Index von images.linuxcontainers.org, 6 h Cache), validation.ts (Grenzen, Netzwerk, Freigabe-Verzeichnisse), console.ts (Konsolenbefehle).
  • Instanzen entstehen ohne Profile mit eigenen Geräten root, eth0, install-iso; Freigabe-Verzeichnisse sind disk-Geräte (Container mit shift=true). Quellen werden per realpath geprüft und müssen unter einer Freigabe, einem eingehängten Dataset oder /appdata liegen (nicht /appdata/bunkerbox, nicht der Instanz-Pool).
  • Konsole: Der Kanal incus-console (im start() registriert) startet incus exec bzw. incus console über runtime.terminal.spawn() und verbindet sie per terminal.bridge() (Audit terminal.open mit Ziel incus-console:<name>). Die Oberfläche nutzt TerminalPane aus views/terminal.
  • ISO-Uploads laufen in Stücken, weil der Proxy Anfragekörper puffert und bei 10 MB abschneidet. Die Teildatei wird mit O_EXCL|O_NOFOLLOW angelegt und per link() (nie überschreibend) unter ihren Namen gebracht.
  • Backup: stopForBackup()/startAfterBackup() und dataDatasets() (Dataset des Speicherpools).
  • Mock: MockVirtualizationSystem hält Incus-Rohobjekte im Speicher, sodass dieselben Abbildungen laufen; Vorgänge enden nach einigen Schritten.

Backup (backup)​

MethodePfadBeschreibung
GET/api/backup/summaryBackupSummary (Dashboard)
GET/POST/api/backup/jobsBackupJob[] / CreateBackupJobRequest -> 201 BackupJob
GET/PATCH/DELETE/api/backup/jobs/[id]BackupJob / UpdateBackupJobRequest (Geheimnis weggelassen = behalten, "" = entfernen) / { confirm: <name> }
POST/api/backup/jobs/[id]/runLauf starten -> 202 BackupRun; 409 solange der Auftrag läuft
POST/api/backup/jobs/[id]/testBackupTargetTest (Verbindung ohne Übertragung)
GET/api/backup/jobs/[id]/runs?limit=BackupRun[] (neueste zuerst, höchstens 50)
POST/api/backup/runs/[id]/cancelLauf abbrechen
GET/api/backup/runs/[id]/logBackupRunLog (Zeilen als Text: Msg für eigene Schritte, Rohtext für Werkzeugausgabe)
GET/POST/api/backup/ssh-keyBackupSshKey / { regenerate? } erzeugt den ed25519-Schlüssel
GET/api/backup/sourcesBackupSourceOptions für den Editor
GET/api/backup/appdata/snapshotsAppdataSnapshot[]
POST/api/backup/appdata/restoreRestoreAppdataRequest (stacks = App-Verzeichnisse kopieren, sonst Rollback) -> 202 BackupRun
  • Ablage in data/backup/ (700) über JsonBackupRepository: jobs.json (Aufträge, Läufe, Zeitplan-Anker), secrets/<id>.json und logs/<lauf>.json (je 600). Temporäre rclone-Konfiguration und rsync-Passwortdatei in data/backup/tmp/, SSH-Schlüssel und known_hosts in data/backup/ssh/.
  • Reine Module in services/backup/: validation.ts (Namen, Hosts, Pfade gegen erlaubte Wurzeln, Endpunkte, Geheimnisse ohne Zeilenumbruch), commands.ts (rsync-/rclone-Argumente, rclone-Konfiguration, Löschen von Versionsordnern per leerem Verzeichnis und --include), parsers.ts (rsync --info=stats2,progress2, --list-only, rclone-Statistik, lsf, ssh-keygen -l), retention.ts (Zeitstempel, Snapshot-Namen, letzte/tägliche/wöchentliche/monatliche Aufbewahrung).
  • DefaultBackupService: Zeitpläne im Minutentakt (isDue/nextSlot aus services/storage/schedule.ts), laufende Aufträge als Aktivität backup:<id>, Fehlschläge als notify("backup-failed") mit Schlüssel backup-failed:<id>. Appdata-Läufe nutzen apps.list/action/operation, virtualization.stopForBackup/startAfterBackup und storage.createSnapshot/listSnapshots/deleteSnapshot/rollbackSnapshot; der Neustart läuft auch nach Fehlern. Ein Rollback des Datasets mit dem eigenen Datenverzeichnis wird verweigert.
  • LinuxBackupSystem: /usr/bin/rsync, /usr/bin/rclone, ssh-keygen über Argument-Arrays; rsync-Exit 24 (verschwundene Dateien) gilt als Warnung. MockBackupSystem simuliert Übertragungen mit echter Ausgabe (Hosts mit „fail“ bzw. „offline“ schlagen fehl).

Benachrichtigungen (notifications)​

MethodePfadBeschreibung
GET/PUT/api/notifications/settingsNotificationSettings / UpdateNotificationSettingsRequest (smtp.password weggelassen = behalten, "" = entfernen)
POST/api/notifications/testTest-Mail an alle Empfänger -> TestNotificationResult; 409 ohne Einrichtung
GET/api/notifications/history?limit=1..200NotificationLogEntry[]
GET/api/notifications/summaryNotificationsSummary (Dashboard)
  • DefaultNotificationsService.notify() entscheidet mit den reinen Funktionen in policy.ts (decide, remember): Entprellung je Schlüssel, Abwahl je Ereignis, Stundenlimit (kritisch immer), fehlgeschlagene Zustellung frühestens nach 15 min erneut. Zu sendende E-Mails laufen nacheinander in einer Warteschlange (höchstens 50).
  • Ablage: data/notifications.json (600, ohne Passwort) und data/secrets/smtp-password (600) über JsonNotificationsRepository; Verlauf (200 Einträge) und Entprellzustand in data/notifications-state.json (600, JsonNotificationStateStore).
  • Versand ohne Fremdpaket: smtp.ts (SMTP-Client auf node:net/node:tls: TLS, STARTTLS, AUTH PLAIN/LOGIN, Anmeldung nur verschlüsselt, Zeitlimit 30 s), mime.ts (Nachricht mit UTF-8, Base64, RFC-2047-Betreff, Dot-Stuffing), compose.ts (Texte in der eingestellten Sprache). MockNotificationsSystem sendet nicht, sondern sammelt die E-Mails.
  • HttpUpdateChecker meldet update-available mit dem Schlüssel update-available:<version>.

USV (ups)​

MethodePfadBeschreibung
GET/api/upsUpsOverview (config, status, services)
GET/api/ups/summaryUpsSummary (Dashboard; status null ohne USV)
GET/api/ups/scanDetectedUps[] (nut-scanner -U -q); 503 ohne NUT
PUT/api/ups/configUpdateUpsConfigRequest -> UpsConfig (netServer.password weggelassen = behalten, "" = entfernen); 409 ohne Gerät, 503 wenn NUT nicht startet
  • DefaultUpsService fragt alle 5 s upsc ups@localhost ab (parsers.ts), merkt sich den Beginn eines Stromausfalls und meldet ups-on-battery, ups-low-battery und ups-shutdown (Schlüssel mit dem Beginn des Ausfalls, also einmal pro Ausfall). Änderungen laufen nacheinander.
  • nutConfig.ts erzeugt nut.conf, ups.conf, upsd.conf, upsd.users, upsmon.conf, upssched.conf und bunkerbox-upssched-cmd und prüft jeden Wert erneut (validation.ts). Das Herunterfahren (LB bzw. upssched-Zeitgeber -> upsmon -c fsd) läuft unabhängig vom Node-Prozess.
  • LinuxUpsSystem: Dateien unter /etc/nut (root:nut, 640), systemctl für nut-driver-enumerator, nut-driver@ups, nut-server, nut-monitor. MockUpsSystem: eine simulierte USB-USV (setOnBattery() für Tests).
  • Ablage: data/ups.json (600) über JsonUpsRepository, Passwörter in data/secrets/ups-monitor-password (zufällig) und ups-net-password (600).

Sicherheit (security)​

  • DefaultSecurityService: HTTPS-Einstellung in data/server.json (ServerConfigFile), angewendet über SecuritySystem.applyTls() (Linux: serverControl().applyTls() des Vorladeskripts; schlägt es fehl, wird die vorherige Datei zurückgeschrieben und erneut angewendet). Zertifikate in data/tls/ (700): selfsigned.* (openssl, deploy/setup.d/10-tls.sh bzw. regenerateSelfSigned()), uploaded.* (600). Änderungen laufen nacheinander.
  • Reine Module: totp.ts (RFC 6238, Base32, Wiederherstellungscodes als sha256), qr.ts (QR-Encoder Byte-Modus, Versionen 1–40, SVG; ohne Fremdpakete, damit der Schlüssel das Gerät nicht verlässt), certificates.ts (Prüfung mit X509Certificate/createPrivateKey, SAN, openssl-Argumente).
  • Zwei-Faktor-Zustand in data/security-state.json (600, JsonSecurityRepository, sofort geschrieben): Schlüssel, letzter Zeitschritt (Replay-Schutz), Hashes der Wiederherstellungscodes, ausstehende Einrichtungen (15 min), Vorgabe für alle Admins, letzte 20 Anmeldeadressen pro Admin.
  • start() meldet die SecondFactorPolicy an (eingerichtet oder vorgeschrieben). Code-Prüfungen sind pro Benutzer begrenzt (5 Fehler -> 15 min, LoginRateLimiter). /api/auth/2fa/** (in SECOND_FACTOR_PATHS) prüfen bzw. richten ein und rufen danach completeSecondFactor().
  • recordLogin() (aus AuthController.login ohne zweite Stufe bzw. nach ihr) löst bei unbekannter Adresse login-new-ip aus.
  • MockSecuritySystem: Dateien und HTTPS-Listener im Speicher (bindet im Mock keinen Port), Beispielzertifikat bunkerbox.local.

Audit-Log (audit)​

  • DefaultAuditService: record() ist synchron und puffert; ein einzelner Schreiber hängt JSON-Zeilen an data/audit/audit.log (600) an und rotiert ab 5 MiB (audit.log.1..5). Abfragen laufen auf einer Kopie im Speicher (beim ersten Aufruf aus allen Dateien geladen, danach durch record() fortgeschrieben, höchstens 200 000 Einträge).
  • Reines Modul auditLog.ts: Normalisierung und Kürzung der Eingaben, (De-)Serialisierung (kaputte Zeilen werden übersprungen), Filter, Cursor (at|id, base64url), facets.
  • LinuxAuditSystem (Dateien, Rotation), MockAuditSystem (im Speicher, mit Beispielverlauf).

Systemprotokoll (logs)​

  • DefaultLogsService -> LogsSystem.read(JournalQuery) -> JSON-Zeilen. Reines Modul journal.ts: normalizeQuery() (Unit-Namen UNIT_NAME_PATTERN, Cursor-Muster, Priorität 0–7, Zeiten als @epoch, Suchtext ohne Steuerzeichen), journalArgs() (--reverse --lines, --after-cursor zum Mitlesen, --reverse --cursor zum Zurückblättern, --grep mit maskierten Sonderzeichen und --case-sensitive=false), parseJournalLine() (MESSAGE als Text oder Byte-Array, ANSI- und Steuerzeichen entfernt, 16 KiB), Unit-Liste aus journalctl --field=_SYSTEMD_UNIT und systemctl list-units.
  • LinuxLogsSystem: Exit-Code 1 ohne Ausgabe = keine Treffer; ohne Mustersuche wird ohne --grep über ein zehnfach größeres Fenster gelesen und selbst gefiltert.
  • MockLogsSystem: deterministisches Journal der letzten drei Tage, das mit der Uhr wächst (Mitlesen funktioniert), im selben JSON-Format.

App-Vorlagen (templates)​

MethodePfadBeschreibung
GET/api/templates?refresh=1TemplateCatalog (1 h zwischengespeichert, refresh lädt neu; Fehler behalten den letzten Stand und setzen error)
GET/api/templates/[id]TemplateFiles (Meta, compose.yaml, .env)
GET/api/templates/[id]/iconSymbol der Vorlage (über Bunkerbox ausgeliefert, Sandbox-CSP)
POST/api/templates/[id]/createCreateFromTemplateRequest -> 201 AppStackDetail
  • Quelle ist <BUNKERBOX_UPDATE_URL>/api/templates (Homepage, site/server/server.mjs liest site/templates/<id>/, im Image /app/templates). LinuxTemplatesSystem lädt mit 10 s Zeitlimit und Größengrenzen (Verzeichnis 1 MiB, Dateien wie bei Apps 256/64 KiB, Symbole 256 KiB); MockTemplatesSystem liest site/templates/ direkt.
  • Reine Teile in services/templates/catalog.ts: parseTemplateIndex/parseTemplateMeta (strenge Prüfung der fremden Daten), interpolate (Compose-Variablen), relativeBindDirs (relative Bind-Mount-Ordner einer Compose-Datei), envOwner (PUID/PGID, nie root).
  • create() ruft apps.create() (Prüfung mit docker compose config, .env 600), legt danach die relativen Ordner unter /appdata/<name>/ komponentenweise ohne Symlink-Verfolgung an (chown nur der neu angelegten auf PUID:PGID) und startet bei Bedarf mit apps.action(name, "up").
  • Die Doku-Liste der Vorlagen erzeugt node scripts/template-docs.mjs; test/templates/site.test.ts prüft Vorlagen und Liste.

Dateien (files)​

MethodePfadBeschreibung
GET/api/files/rootsFileRoot[] (Freigaben, Datasets, Netzlaufwerke, /appdata; 10 s zwischengespeichert)
GET/api/files/list, /api/files/stat?root=&path= -> DirectoryListing (höchstens 5000 Einträge) bzw. FileEntry
GET/api/files/principalsFilePrincipals aus /etc/passwd und /etc/group
GET/api/files/downloadDatei als Stream oder Ordner als ZIP-Stream (application/octet-stream bzw. application/zip, Anhang, Sandbox-CSP)
POST/api/files/upload?root=&path=&name=&overwrite=1, roher Body, Header x-bunkerbox-upload: 1 -> 201 FileEntry
POST/api/files/mkdir, renameCreateFolderRequest, RenameRequest
POST/api/files/move, copy, delete, chown, chmod-> 202 FileOperation (Hintergrund, Aktivität files-<art>:<id>)
GET/api/files/operations, /api/files/operations/[id]Vorgänge (die letzten 50, im Speicher)
  • Pfade sind immer (Wurzel-ID, relativer Pfad). DefaultFilesService.resolve() prüft jede Komponente (splitRelPath, isSafeFileName), geht vom Ablageort aus jede Komponente per lstat durch (kein Symlink auf dem Weg) und vergleicht den realpath des Elternordners. Endkomponenten werden nie verfolgt: Lesen mit O_NOFOLLOW und fstat, Schreiben in neue Teildateien mit O_CREAT | O_EXCL | O_NOFOLLOW, danach link() (kein Überschreiben) bzw. rename() (Ersetzen einer normalen Datei), lchown, chmod über einen geöffneten Handle.
  • Wurzeln: services/files/paths.ts (isAllowedRootPath verbietet / und Systempfade, mergeRoots entfernt doppelte Pfade). /appdata/bunkerbox und das Datenverzeichnis sind geschützt (ausgeblendet, 404).
  • Rekursive Vorgänge (Kopieren, Löschen, chown, chmod) folgen nie Symlinks; Löschen verweigert Bäume mit anderen Dateisystemen (st_dev), chown/chmod überspringen sie. Neue Ordner und Dateien übernehmen Besitzer, Gruppe und Rechte des Elternordners.
  • services/files/zip.ts: ZIP-Writer ohne Abhängigkeiten (gespeichert, Datendeskriptoren, ZIP64 nach Bedarf), Rückdruck über einen ReadableStream mit pull.
  • Upload: src/proxy.ts schließt /api/files/upload aus, weil der Proxy Bodies puffert und bei 10 MB abschneidet. FilesController.upload prüft deshalb selbst Origin, Upload-Header, Sitzung und zweite Stufe und schreibt den Audit-Eintrag files.upload selbst (Route mit { audit: false }).
  • MockFilesSystem erweitert LinuxFilesSystem und bildet die Host-Pfade in data/mock-files/ ab (Beispielinhalt beim ersten Zugriff); Besitzer und Rechte werden simuliert.

Terminal (terminal)​

MethodePfadBeschreibung
WS/api/ws/terminal?cols=&rows=root-Shell (/bin/bash -l), Terminal-Protokoll aus models/terminal.ts; höchstens MAX_ROOT_SHELLS (8)
GET/api/terminal/sessionsTerminalSessionInfo[] aller Terminal- und Konsolensitzungen
DELETE/api/terminal/sessions/[id]Sitzung beenden (Close 1001, das Pty wird beendet); 404 wenn unbekannt
  • DefaultTerminalService meldet den Kanal terminal am Gateway an. bridge(socket, request, pty, target) verbindet Pty und Socket (Ausgabe binär, Eingabe/Größe als JSON, Gegendruck ab 4 MiB mit pause()/resume(), Audit terminal.open/terminal.close) und wird von der Incus-Konsole wiederverwendet; closeSession(id) beendet Sitzungen.
  • ScriptPty (services/terminal/pty.ts): script -qfec mit geprüften, für /bin/sh gequoteten Argumenten, eigene Prozessgruppe (SIGHUP, nach 3 s SIGKILL), Größe per stty -F /dev/pts/N des ersten Kindprozesses. Startfehler (fehlendes script, Arbeitsverzeichnis) melden ein Ende, statt den Dienst zu beenden; das Ende kommt erst nach der letzten Ausgabe (close).
  • Oberfläche: views/terminal/TerminalPane.tsx (xterm.js mit Fit-Addon, dynamisch geladen) ist die gemeinsame Komponente für Shell und Konsolen (<TerminalPane channel params title actions onStatus height />): Status, Neu verbinden, Schriftgröße, Vollbild, Sondertasten für Mobilgeräte, Kopieren mit Strg+Umschalt+C. TerminalView ergänzt Tabs (bis 4) und die Sitzungsliste.

Netzwerk (network)​

MethodePfadBeschreibung
GET/api/networkNetworkOverview (Schnittstellen, Konfiguration, Routen, DNS, Apply-Zustand)
GET/api/network/interfacesNetInterface[]
GET/api/network/stateNetworkApplyState
POST/api/network/previewNetworkConfig -> NetworkPreview (Dateien, entfernte Dateien, erwartete Adressen, Hinweise); 422 bei ungültiger Konfiguration
POST/api/network/applyNetworkConfig -> 202 NetworkApplyState (pending, confirmToken, deadline); 409 solange eine Änderung wartet
POST/api/network/confirm{ token } -> NetworkApplyState; öffentlich (im Proxy freigegeben, ebenso die Seite /network-confirm), 403 falscher Code, 409 noch nicht aktiv bzw. schon zurückgenommen
POST/api/network/rollbacksofort zurücknehmen
  • Reine Module in services/network/: parsers.ts (ip -j -d address, Standardrouten, resolv.conf, /etc/network/interfaces, systemctl show, deriveConfig() vor der Übernahme), validation.ts (Adressen, Gateways, Bridge, MTU, Hinweise, erwartete Adressen), networkd.ts (.network/.netdev, Drop-in, resolv.conf, hosts, ifupdown-Rest; isManagedPath() als Positivliste), plan.ts (gestagte Dateien, Sicherungen und die Units bunkerbox-network-apply.service, bunkerbox-network-rollback.service, bunkerbox-network-rollback.timer mit systemd-Quoting).
  • DefaultNetworkService: serialisierte Abläufe, Zustand in data/network-state.json (600, sofort geschrieben), Aktivierung 1,5 s nach der Antwort per blockierendem systemctl start der Apply-Unit, Token-Vergleich mit timingSafeEqual, Rücknahme erkannt am Marker /var/lib/bunkerbox-network/rolled-back; ist der Timer 30 s überfällig, startet der Dienst die Rücknahme selbst. Während einer offenen Änderung meldet activity() network-apply (sperrt Updates). bridgeAvailable() prüft /sys/class/net/br0/bridge und operstate.
  • LinuxNetworkSystem schreibt nur Positivlisten-Pfade (Units nach /etc/systemd/system, Staging unter /var/lib/bunkerbox-network auf dem Root-Dateisystem, 700/600; die Rollback-Unit hat RequiresMountsFor= darauf, und jede Wiederherstellungszeile ist mit - markiert, damit networkd-Neustart und ifup immer laufen); MockNetworkSystem erzeugt ip -j-JSON aus einer simulierten Konfiguration und nimmt uhrgesteuert zurück (keine echten Timer).

Spindown (spindown, Erweiterung von storage)​

MethodePfadBeschreibung
GET/api/storage/spindownSpindownOverview (policies, power: Zustand, unterstützt, blockedBy, Leerlaufzeit, letzter Spindown)
PUT/api/storage/spindown/[diskId]SetSpindownPolicyRequest { idleMinutes: 5..720 | null, method: "monitor" | "hdparm" } -> SpindownPolicy; Audit storage.spindown.policy
POST/api/storage/spindown/[diskId]/standbysofort hdparm -y -> DiskPowerInfo; 409 während Scrub, Resilver oder Burn-in; Audit storage.spindown.standby

Aufbau (src/services/storage/spindown/): DefaultSpindownService (Monitor im Minutentakt: Zustände, Leerlaufzeiten, hdparm -S bei neuen Kernelnamen erneut setzen, Spindown bei Leerlauf), rules.ts (rein: hdparm -S-Kodierung, Parser für hdparm -C, smartctl -i -n standby und /sys/block/<dev>/stat, IdleTracker, blockedReason, shouldSpinDown), LinuxSpindownSystem (smartctl, hdparm, sysfs; nur Kernelnamen nach BLOCK_NAME_PATTERN), MockSpindownSystem. Platten und Pools kommen von storage.listDisks()/listPools(), laufende Burn-ins von burnin.runningDiskIds(). Richtlinien in data/spindown-state.json (600, JsonSpindownRepository), Schlüssel ist Disk.id.

Burn-in (burnin, Erweiterung von storage)​

MethodePfadBeschreibung
GET/api/storage/burninBurnInRun[] (neueste zuerst, mit Verlauf)
POST/api/storage/burninStartBurnInRequest { diskId, confirmSerial, phases? } -> 201 BurnInRun; 404, 409 (Systemplatte, belegt, läuft schon, badblocks aktiv), 422 (keine Seriennummer); Audit storage.burnin.start (Ziel diskId)
GET/api/storage/burnin/marksBurnInMark[]: Platten mit bestandenem letztem Burn-in und passender Seriennummer
GET/api/storage/burnin/[id]BurnInRun
POST/api/storage/burnin/[id]/cancelAbbruch, wartet auf das Ende -> BurnInRun; Audit storage.burnin.cancel
GET/api/storage/burnin/[id]/logBurnInLog (höchstens 400 Zeilen)

Aufbau (src/services/storage/burnin/): DefaultBurnInService (Eignung, Ablauf SMART-Kurztest -> badblocks -> SMART-Langtest, Abbruch, Verdikt, Verlauf, Ereignis burnin-finished, activity() je laufendem Burn-in), parsers.ts (rein: badblocks-Fortschritt, Blockgröße für > 16 TiB, Selbsttest-Zustand ATA/NVMe aus smartctl -j -a, SMART-Zähler und Änderungen, Gesamtfortschritt, Ende-Schätzung, Verdikt), LinuxBurnInSystem (smartctl -t/-X/-j -a, badblocks -w -s -v -b -c über spawn mit Argumentliste, laufende badblocks aus /proc/*/cmdline), MockBurnInSystem (etwa vier Minuten, sdl fällt durch). Unmittelbar vor badblocks werden Seriennummer, Größe und Verwendung erneut über storage.listDisks() geprüft; Gerätepfade nur nach DEVICE_PATH_PATTERN. Läufe und Protokolle in data/burnin-history.json (600, JsonBurnInRepository, höchstens 50 abgeschlossene), Fortschritt wird höchstens alle 30 s geschrieben; beim Start werden noch laufende Läufe als unterbrochen markiert. storage.createPool, addVdev und replaceDisk lehnen Platten mit laufendem Burn-in ab (storage.error.burnInRunning).

Speicher-Ereignisse (src/services/storage/health.ts, rein, vom Speicherdienst alle 5 min ausgewertet): pool-degraded, pool-faulted, device-errors, scrub-errors, smart-failing, smart-warning, disk-temperature, jeweils mit Entprellschlüssel (z. B. pool-degraded:tank, smart-warning:<id>:<Zähler>).

Oberfläche (Design-System)​

src/app/globals.css definiert Farben (hell/dunkel über prefers-color-scheme) und alle Grundbausteine; Bereiche verwenden diese Klassen und legen nur Besonderes in CSS-Modulen ab.

BausteinKlassenKomponente
Seitepage, page-header, page-subtitle, page-actions, stack, stack-sm, row, spacer, grid grid-2/3/4, span-2PageHeader
Kartecard, card-header, card-title, card-subtitle, card-actions, card-body, card-body-flush, card-footer, card-section-titleCard, Stat
Kennzahlstat, stat-label, stat-value, stat-sub, kv (dl)Stat
Tabelletable-wrap, table, table-compact, num, actions–
Buttonsbtn, btn-primary, btn-danger, btn-ghost, btn-small, btn-block, btn-icon, btn-group–
Formularform, form-grid, form-actions, field, field-label, field-hint, field-error, field-invalid, input, select, textarea, code-editor, input-group, input-addon, checkbox, switchField, Switch
Statusbadge + tone-ok/warning/bad/info/active/muted, dot, chip, note note-info/ok/warning/bad, txt-ok/warning/bad, muted, small, monoBadge, Note, ErrorNote
Fortschrittprogress (+ progress-small, progress-indeterminate, Ton-Klasse), progress-bar, spinnerProgressBar, Spinner
Tabstabs, tab (aria-selected), tab-badgeTabs
Dialogdialog (+ dialog-danger, dialog-small, dialog-large), dialog-header, dialog-body, dialog-actionsDialog, ConfirmDialog (getippte Bestätigung)
Leerempty, empty-title, empty-actionEmptyState

Komponenten liegen in src/views/components/. Daten holt usePoll(fn, intervalMs) (pausiert bei verstecktem Tab), Texte und Formatierung liefert useT() (t, text für Msg, fmt.bytes dezimal, fmt.ibytes binär, fmt.rate, fmt.percent, fmt.duration, fmt.dateTime, fmt.relative). Fetch-Clients bauen auf request, send, query, seg aus src/views/api.ts auf; ein 401 führt zur Anmeldeseite.

Mehrsprachigkeit​

Texte stammen ausschließlich aus src/i18n/messages/<de|en>/*.ts mit gleichen Schlüsseln in beiden Sprachen: common.ts, server.ts (error.*), ui.ts (ui.*) und je Bereich <feature>.ts (nur Schlüssel <feature>.*, Fehler als <feature>.error.*). Der Server erzeugt Msg (msg(key, params)); die Oberfläche rendert in der Sprache des Betrachters. test/core/i18n.test.ts prüft gleiche Schlüssel und Platzhalter, Präfixe der Bereiche und dass jeder im Code verwendete Schlüssel existiert.

Update-Prüfung​

HttpUpdateChecker fragt alle 6 h <BUNKERBOX_UPDATE_URL>/api/latest ab (erste Abfrage 15 s nach dem Start, nach Fehlern nach 30 min) und vergleicht per SemVer. installable ist wahr, wenn nicht Mock, das Arbeitsverzeichnis unter <base>/releases/<version> liegt und systemd-run vorhanden ist. POST /api/update startet systemd-run --unit=bunkerbox-update … deploy/update.sh <version>; verweigert bei Aktivität. Mit Settings.autoUpdate installiert der Checker neue Versionen selbst, sobald activity() leer ist. Siehe Update-Vertrag.

Umgebungsvariablen​

VariableDefaultBedeutung
PORT80 (systemd), 3000 (npm run dev)HTTP-Port
BUNKERBOX_DATA_DIR./dataPersistenz (systemd: /appdata/bunkerbox/data)
BUNKERBOX_MOCK–1 = simulierte Hardware (Entwicklung ohne Linux/root)
BUNKERBOX_UPDATE_URLhttps://bunkerbox.znbw.deBasis-URL der Update-Schnittstelle
BUNKERBOX_UPDATE_CHECKan0 schaltet die Update-Prüfung ab
NODE_ENV–production im Betrieb

Mock-Modus​

Mit BUNKERBOX_MOCK=1 (oder npm run dev:mock, plattformunabhängig) verwendet jede Factory Mock<Feature>System statt Linux<Feature>System und die Anmeldung MockAuthAdapter (admin/admin; der normale Benutzer anna/anna wird wie im echten Betrieb abgewiesen). Der Mock simuliert die Beispiel-Hardware: 1 System-SSD (240 GB), 2 × 16 TB HDD, 5 × 5 TB HDD, 2 × 1 TB SSD, Pools tank und apps, Datasets, Freigaben, Benutzer, Docker-Stacks und Messwerte. Controller, Service-Regeln und Views laufen unverändert; nur der Systemzugriff wird ersetzt. Die Oberfläche zeigt „Mock-Modus“ in der Kopfzeile. Updates sind im Mock nie installierbar.

Sicherheitsregeln​

  • Jeder Systemzugriff läuft über das System-Interface des Bereichs; externe Programme werden immer mit Argument-Arrays gestartet (exec()), nie über eine Shell. Passwörter gehen über stdin (ExecOptions.input).
  • Alles, was in Kommandozeilen, generierte Konfigurationen (smb.conf, exports, systemd-Units, compose) oder Pfade gelangt, wird vorher mit src/models/validation.ts geprüft.
  • Zerstörende Aktionen verlangen die getippte Bestätigung (requireConfirm) und werden im Service geprüft, nicht nur in der Oberfläche. System-Platten und benutzte Platten werden nie angeboten oder angenommen.
  • data/ hat Rechte 700, settings.json, sessions.json, session-secret 600; Zugangsdaten für Netzlaufwerke liegen in data/credentials/<name> (600).
  • JSON-Dateien werden atomar geschrieben (tmp + fsync + rename) und beim Beenden des Dienstes gesichert.

Automatisierte Tests​

npm test (Vitest, test/) läuft ohne Hardware, root oder Netzwerk. test/core/ prüft Übersetzungen, Sitzungs-Token und Ablauf, Rate-Limit, Origin-Prüfung, AuthService, shadow/passwd/group-Parser, Einstellungen, SemVer und die Namensmuster. Jeder Bereich legt seine Tests in test/<feature>/ ab (Parser, Generatoren, Regeln, Service mit Mock-System).