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 ausmodels/. - Views kennen nur Typen aus
models/und sprechen perfetchmit/api/*. Kein Import ausservices/odercontrollers/inviews/. - Systemzugriff ausschließlich über das System-Interface des Bereichs (
<F>System) undexec().
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.
| Teil | Pfad |
|---|---|
| Typen/DTOs | src/models/<feature>.ts (plus eigene Repositories src/models/<Feature>*.ts) |
| Vertrag | src/services/<feature>/contract.ts (Service-Interface fest, System-Interface frei erweiterbar) |
| Service | src/services/<feature>/Default<Feature>Service.ts |
| Systemzugriff | src/services/<feature>/Linux<Feature>System.ts, Mock<Feature>System.ts, reine Parser/Generatoren daneben |
| Factory | src/services/<feature>/index.ts (create<Feature>Service(ctx)) |
| Controller | src/controllers/<Feature>Controller.ts |
| Routen | src/app/api/<feature>/** |
| Seiten | src/app/(admin)/<feature>/** (Dashboard: src/app/(admin)/page.tsx) |
| Views | src/views/<feature>/**, Fetch-Client src/views/<feature>/api.ts, eigene Styles als CSS-Module |
| Texte | src/i18n/messages/{de,en}/<feature>.ts, Schlüssel beginnen mit <feature>. |
| Tests | test/<feature>/*.test.ts |
| Doku | docs/<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 percrypt(3)der System-libxcrypt überperl -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>/cmdlinefü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/groupund/etc/passwd. - Mock (
MockAuthAdapter):admin/admin. - Sitzungen (
DefaultAuthService): 32 zufällige Bytes als Token, Cookiebb_session=<token>.<hmac>(HMAC-SHA256 mitdata/session-secret, 600),HttpOnly,SameSite=Strict,Path=/,Securehinter HTTPS. Serverseitig indata/sessions.jsonunter 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
securitymeldet eineSecondFactorPolicybeimAuthServicean. Verlangt sie eine zweite Stufe, entsteht beim Login eine Sitzung mitpendingSecondFactor; bisPOST /api/auth/2fa/verify(ruftcompleteSecondFactor()) erreicht sie nurSECOND_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 ausX-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/healthund 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 Headerx-bunkerbox-userund 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.cjsvor (node --require ./preload.cjs server.js), das jeder Anfrage die Gegenstelle alsx-bunkerbox-peerund das Protokoll alsx-bunkerbox-protomitgibt (vom Client gesendete Werte werden überschrieben).clientIp()/isHttps()insrc/controllers/http.tswertenX-Forwarded-*nur aus, wenn diese Gegenstelle inBUNKERBOX_TRUSTED_PROXIESsteht (src/services/auth/clientAddress.ts). - CSRF: Für ändernde API-Anfragen (nicht GET/HEAD/OPTIONS) muss
Originzum Host (bzw.X-Forwarded-Host) passen; ohneOriginnur mitSec-Fetch-Site: same-origin. Zusätzlich verlangtreadJson()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-peerundx-bunkerbox-protoauf 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 überserverControl().applyTls()(src/services/gateway/contract.ts) umschaltbar, ohne Neustart. Ungültige Zertifikate behalten den bisherigen Zustand. MitredirectHttpleitet Port 80 auf HTTPS um (308), außer für Loopback-Clients (Health-Check vonupdate.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.
| API | Bedeutung |
|---|---|
runtime.gateway.register({ name, open(socket, request) }) | Kanal /api/ws/<name> anmelden (im start() des Bereichs) |
WsRequest | user, ip, locale, params (Query, ungeprüft), secure, userAgent |
WsSocket | send(string | Uint8Array), sendJson, close(code, reason), onMessage, onClose, onDrain, bufferedAmount |
Fehler in open() | ServiceError -> { type: "error", error, key, params } und Close-Code 4000 + Status |
| Browser | openChannel(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>" })insrc/controllers/http.tsschreibt nach jeder ändernden Anfrage (POST, PUT, PATCH, DELETE) einen Eintrag (Benutzer, Adresse, Aktion, Ziel, Ergebnisok/failed/denied, Status, Fehlerschlüssel). Ziel sind die Routen-Parameter;auditedJson(request, result, status)nimmtresult.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.tsverlangt die Angabe bei jeder ändernden Route. Andere Aktionen (Terminal-Sitzungen, zweite Stufe) rufenruntime.audit.record(input)auf. Nie Geheimnisse intargetoderdetails. - Benachrichtigungen:
runtime.notifications.notify({ type, severity, key, subject, body, details })ist „fire and forget“ (wirft nie, blockiert nie). Ereignistypen inNOTIFICATION_EVENT_TYPES(src/models/notifications.ts);keydient der Entprellung (z. B.pool-degraded:tank). Betreff und Text sindMsgund werden in der eingestellten Sprache gerendert. SMTP-Zugang, Empfänger und Schalter liegen indata/notifications.json, das Passwort indata/secrets/smtp-password(beide 600), nicht inbunkerbox.env.
API (Kern)
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/auth/login | LoginRequest -> MeResponse + Cookie; 401, 429 |
| POST | /api/auth/logout | beendet die Sitzung, löscht das Cookie |
| GET | /api/auth/me | MeResponse |
| GET | /api/health | öffentlich: HealthResponse { ok, version, mock, activeOperations } |
| GET/PUT | /api/settings | SettingsResponse; PUT mit Teil-Settings (language, autoUpdate, sessionIdleMinutes 5..1440, sessionAbsoluteHours 1..168) |
| GET | /api/update | UpdateResponse { update, activity, autoUpdate } |
| POST | /api/update | StartUpdateRequest -> 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)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/storage | StorageOverview (ZFS verfügbar, Platten, Pools, ARC) |
| GET | /api/storage/disks | Disk[] |
| POST | /api/storage/disks/refresh | SMART 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,b | LayoutSuggestion[] |
| GET/POST | /api/storage/pools | Pool[] / CreatePoolRequest -> 201 Pool |
| GET/DELETE | /api/storage/pools/[name] | Pool / { confirm } |
| POST | /api/storage/pools/[name]/export | Export |
| POST | /api/storage/pools/[name]/vdevs | AddVdevRequest |
| POST | /api/storage/pools/[name]/replace | ReplaceDiskRequest |
| POST | /api/storage/pools/[name]/offline, …/online | DeviceStateRequest |
| POST | /api/storage/pools/[name]/scrub | `{ action: "start" |
| PUT | /api/storage/pools/[name]/scrub-schedule | `{ schedule: ScrubSchedule |
| GET/POST | /api/storage/import | ImportablePool[] / 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/rollback | RollbackSnapshotRequest |
| GET/PUT | /api/storage/arc | ArcOverview / 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)
| Methode | Route | Ergebnis |
|---|---|---|
| GET / POST | /api/users | NasUser[] / 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/groups | NasGroup[] / 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)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/shares | Share[] (mit status) |
| POST | /api/shares | CreateShareRequest -> 201 Share |
| GET/PATCH | /api/shares/[name] | Share; PATCH mit UpdateShareRequest |
| DELETE | /api/shares/[name] | { confirm: <name> }; die Daten bleiben |
| POST | /api/shares/[name]/permissions | ACLs rekursiv im Hintergrund schreiben -> 202 (Aktivität shares-acl:<name>) |
| POST | /api/shares/apply | Konfiguration neu schreiben, Dienste neu laden -> ShareServicesStatus |
| GET | /api/shares/services | ShareServicesStatus (Samba, NFS) |
| GET | /api/shares/paths | SharePathOption[]: eingehängte Datasets (storage.listDatasets()) und Netzlaufwerke |
| GET | /api/shares/connections | ShareConnections (smbstatus --json, /proc/fs/nfsd/clients) |
| GET | /api/shares/preview | ShareConfigPreview: erzeugte Include- und Exportdatei |
- Ablage:
data/share-definitions.json(600) überJsonShareRepository, 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 ->
testparmauf eine Kopie vonsmb.confmit 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. SharesServicehat zusätzlichapplyPermissions(name)undpreview().
Netzlaufwerke (mounts)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/mounts | RemoteMount[] (Zustand, Fehler, usedByShares) |
| POST | /api/mounts | CreateRemoteMountRequest -> 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]/mount | einhängen; 503 mit Journal-Auszug bei Fehler |
| POST | /api/mounts/[name]/unmount | aushängen; 409, wenn das Laufwerk belegt ist |
- Ablage:
data/remote-mounts.json(600, ohne Passwörter) überJsonRemoteMountRepository; Passwörter nur indata/credentials/<name>(600). - Reine Teile in
services/mounts/:units.ts(Unit-Namen wiesystemd-escape --path,.mount/.automount, Optionen, Zugangsdatei),validation.ts,parsers.ts(/proc/mounts,systemctl show). - Zustand aus
/proc/mountsundsystemctl show, nie durch Zugriff auf den Einhängepunkt.
Apps (apps)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/apps | AppStack[] (Verzeichnisse unter /appdata mit Compose-Datei, Container per docker compose ps) |
| POST | /api/apps | CreateStackRequest -> 201 AppStackDetail; 409 wenn vorhanden, 422 bei ungültiger Konfiguration |
| GET | /api/apps/docker | DockerStatus |
| GET | /api/apps/summary | AppsSummary (für das Dashboard) |
| POST | /api/apps/validate | ValidateStackRequest -> 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 jsonauf;analyzeCompose()liefert Warnungen (privilegiert, Host-Namespaces, Docker-Socket, Systempfade). .envwird 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);
--sinceist bei Docker inklusiv, gleiche Zeitstempel werden verworfen. - Reine Parser in
services/apps/parsers.ts,.env-Helfer (auch für den Editor im Browser) inmodels/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)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/virtualization/status | VirtualizationStatus (installiert, erreichbar, Pool, ISO-Dataset, Netze, Bridge, Host) |
| POST | /api/virtualization/setup | SetupVirtualizationRequest -> VirtualizationStatus (Pool, ISO-Dataset, NAT-Netz; vorhandene Teile bleiben) |
| GET | /api/virtualization/summary | VirtualizationSummary (Dashboard) |
| GET/POST | /api/virtualization/instances | Instance[] / 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]/shares | InstanceShareMount -> 201 InstanceDetail |
| DELETE | /api/virtualization/instances/[name]/shares/[device] | -> InstanceDetail |
| GET/POST | /api/virtualization/instances/[name]/snapshots | InstanceSnapshot[] / { 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/isos | IsoImage[] 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) |
LinuxVirtualizationSystemspricht überIncusClient(node:httpmitsocketPath) 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 alsVirtualizationOperationim 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 vonimages.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 sinddisk-Geräte (Container mitshift=true). Quellen werden perrealpathgeprüft und müssen unter einer Freigabe, einem eingehängten Dataset oder/appdataliegen (nicht/appdata/bunkerbox, nicht der Instanz-Pool). - Konsole: Der Kanal
incus-console(imstart()registriert) startetincus execbzw.incus consoleüberruntime.terminal.spawn()und verbindet sie perterminal.bridge()(Auditterminal.openmit Zielincus-console:<name>). Die Oberfläche nutztTerminalPaneausviews/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_NOFOLLOWangelegt und perlink()(nie überschreibend) unter ihren Namen gebracht. - Backup:
stopForBackup()/startAfterBackup()unddataDatasets()(Dataset des Speicherpools). - Mock:
MockVirtualizationSystemhält Incus-Rohobjekte im Speicher, sodass dieselben Abbildungen laufen; Vorgänge enden nach einigen Schritten.
Backup (backup)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/backup/summary | BackupSummary (Dashboard) |
| GET/POST | /api/backup/jobs | BackupJob[] / CreateBackupJobRequest -> 201 BackupJob |
| GET/PATCH/DELETE | /api/backup/jobs/[id] | BackupJob / UpdateBackupJobRequest (Geheimnis weggelassen = behalten, "" = entfernen) / { confirm: <name> } |
| POST | /api/backup/jobs/[id]/run | Lauf starten -> 202 BackupRun; 409 solange der Auftrag läuft |
| POST | /api/backup/jobs/[id]/test | BackupTargetTest (Verbindung ohne Übertragung) |
| GET | /api/backup/jobs/[id]/runs?limit= | BackupRun[] (neueste zuerst, höchstens 50) |
| POST | /api/backup/runs/[id]/cancel | Lauf abbrechen |
| GET | /api/backup/runs/[id]/log | BackupRunLog (Zeilen als Text: Msg für eigene Schritte, Rohtext für Werkzeugausgabe) |
| GET/POST | /api/backup/ssh-key | BackupSshKey / { regenerate? } erzeugt den ed25519-Schlüssel |
| GET | /api/backup/sources | BackupSourceOptions für den Editor |
| GET | /api/backup/appdata/snapshots | AppdataSnapshot[] |
| POST | /api/backup/appdata/restore | RestoreAppdataRequest (stacks = App-Verzeichnisse kopieren, sonst Rollback) -> 202 BackupRun |
- Ablage in
data/backup/(700) überJsonBackupRepository:jobs.json(Aufträge, Läufe, Zeitplan-Anker),secrets/<id>.jsonundlogs/<lauf>.json(je 600). Temporäre rclone-Konfiguration und rsync-Passwortdatei indata/backup/tmp/, SSH-Schlüssel undknown_hostsindata/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/nextSlotausservices/storage/schedule.ts), laufende Aufträge als Aktivitätbackup:<id>, Fehlschläge alsnotify("backup-failed")mit Schlüsselbackup-failed:<id>. Appdata-Läufe nutzenapps.list/action/operation,virtualization.stopForBackup/startAfterBackupundstorage.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.MockBackupSystemsimuliert Übertragungen mit echter Ausgabe (Hosts mit „fail“ bzw. „offline“ schlagen fehl).
Benachrichtigungen (notifications)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET/PUT | /api/notifications/settings | NotificationSettings / UpdateNotificationSettingsRequest (smtp.password weggelassen = behalten, "" = entfernen) |
| POST | /api/notifications/test | Test-Mail an alle Empfänger -> TestNotificationResult; 409 ohne Einrichtung |
| GET | /api/notifications/history?limit=1..200 | NotificationLogEntry[] |
| GET | /api/notifications/summary | NotificationsSummary (Dashboard) |
DefaultNotificationsService.notify()entscheidet mit den reinen Funktionen inpolicy.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) unddata/secrets/smtp-password(600) überJsonNotificationsRepository; Verlauf (200 Einträge) und Entprellzustand indata/notifications-state.json(600,JsonNotificationStateStore). - Versand ohne Fremdpaket:
smtp.ts(SMTP-Client aufnode: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).MockNotificationsSystemsendet nicht, sondern sammelt die E-Mails. HttpUpdateCheckermeldetupdate-availablemit dem Schlüsselupdate-available:<version>.
USV (ups)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/ups | UpsOverview (config, status, services) |
| GET | /api/ups/summary | UpsSummary (Dashboard; status null ohne USV) |
| GET | /api/ups/scan | DetectedUps[] (nut-scanner -U -q); 503 ohne NUT |
| PUT | /api/ups/config | UpdateUpsConfigRequest -> UpsConfig (netServer.password weggelassen = behalten, "" = entfernen); 409 ohne Gerät, 503 wenn NUT nicht startet |
DefaultUpsServicefragt alle 5 supsc ups@localhostab (parsers.ts), merkt sich den Beginn eines Stromausfalls und meldetups-on-battery,ups-low-batteryundups-shutdown(Schlüssel mit dem Beginn des Ausfalls, also einmal pro Ausfall). Änderungen laufen nacheinander.nutConfig.tserzeugtnut.conf,ups.conf,upsd.conf,upsd.users,upsmon.conf,upssched.confundbunkerbox-upssched-cmdund prüft jeden Wert erneut (validation.ts). Das Herunterfahren (LBbzw.upssched-Zeitgeber ->upsmon -c fsd) läuft unabhängig vom Node-Prozess.LinuxUpsSystem: Dateien unter/etc/nut(root:nut, 640),systemctlfürnut-driver-enumerator,nut-driver@ups,nut-server,nut-monitor.MockUpsSystem: eine simulierte USB-USV (setOnBattery()für Tests).- Ablage:
data/ups.json(600) überJsonUpsRepository, Passwörter indata/secrets/ups-monitor-password(zufällig) undups-net-password(600).
Sicherheit (security)
DefaultSecurityService: HTTPS-Einstellung indata/server.json(ServerConfigFile), angewendet überSecuritySystem.applyTls()(Linux:serverControl().applyTls()des Vorladeskripts; schlägt es fehl, wird die vorherige Datei zurückgeschrieben und erneut angewendet). Zertifikate indata/tls/(700):selfsigned.*(openssl,deploy/setup.d/10-tls.shbzw.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 mitX509Certificate/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 dieSecondFactorPolicyan (eingerichtet oder vorgeschrieben). Code-Prüfungen sind pro Benutzer begrenzt (5 Fehler -> 15 min,LoginRateLimiter)./api/auth/2fa/**(inSECOND_FACTOR_PATHS) prüfen bzw. richten ein und rufen danachcompleteSecondFactor().recordLogin()(ausAuthController.loginohne zweite Stufe bzw. nach ihr) löst bei unbekannter Adresselogin-new-ipaus.MockSecuritySystem: Dateien und HTTPS-Listener im Speicher (bindet im Mock keinen Port), Beispielzertifikatbunkerbox.local.
Audit-Log (audit)
DefaultAuditService:record()ist synchron und puffert; ein einzelner Schreiber hängt JSON-Zeilen andata/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 durchrecord()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 Moduljournal.ts:normalizeQuery()(Unit-NamenUNIT_NAME_PATTERN, Cursor-Muster, Priorität 0–7, Zeiten als@epoch, Suchtext ohne Steuerzeichen),journalArgs()(--reverse --lines,--after-cursorzum Mitlesen,--reverse --cursorzum Zurückblättern,--grepmit maskierten Sonderzeichen und--case-sensitive=false),parseJournalLine()(MESSAGE als Text oder Byte-Array, ANSI- und Steuerzeichen entfernt, 16 KiB), Unit-Liste ausjournalctl --field=_SYSTEMD_UNITundsystemctl 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)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/templates?refresh=1 | TemplateCatalog (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]/icon | Symbol der Vorlage (über Bunkerbox ausgeliefert, Sandbox-CSP) |
| POST | /api/templates/[id]/create | CreateFromTemplateRequest -> 201 AppStackDetail |
- Quelle ist
<BUNKERBOX_UPDATE_URL>/api/templates(Homepage,site/server/server.mjsliestsite/templates/<id>/, im Image/app/templates).LinuxTemplatesSystemlädt mit 10 s Zeitlimit und Größengrenzen (Verzeichnis 1 MiB, Dateien wie bei Apps 256/64 KiB, Symbole 256 KiB);MockTemplatesSystemliestsite/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()ruftapps.create()(Prüfung mitdocker compose config,.env600), legt danach die relativen Ordner unter/appdata/<name>/komponentenweise ohne Symlink-Verfolgung an (chownnur der neu angelegten auf PUID:PGID) und startet bei Bedarf mitapps.action(name, "up").- Die Doku-Liste der Vorlagen erzeugt
node scripts/template-docs.mjs;test/templates/site.test.tsprüft Vorlagen und Liste.
Dateien (files)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/files/roots | FileRoot[] (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/principals | FilePrincipals aus /etc/passwd und /etc/group |
| GET | /api/files/download | Datei 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, rename | CreateFolderRequest, 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 perlstatdurch (kein Symlink auf dem Weg) und vergleicht denrealpathdes Elternordners. Endkomponenten werden nie verfolgt: Lesen mitO_NOFOLLOWundfstat, Schreiben in neue Teildateien mitO_CREAT | O_EXCL | O_NOFOLLOW, danachlink()(kein Überschreiben) bzw.rename()(Ersetzen einer normalen Datei),lchown,chmodüber einen geöffneten Handle. - Wurzeln:
services/files/paths.ts(isAllowedRootPathverbietet / und Systempfade,mergeRootsentfernt doppelte Pfade)./appdata/bunkerboxund 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 einenReadableStreammitpull.- Upload:
src/proxy.tsschließt/api/files/uploadaus, weil der Proxy Bodies puffert und bei 10 MB abschneidet.FilesController.uploadprüft deshalb selbst Origin, Upload-Header, Sitzung und zweite Stufe und schreibt den Audit-Eintragfiles.uploadselbst (Route mit{ audit: false }). MockFilesSystemerweitertLinuxFilesSystemund bildet die Host-Pfade indata/mock-files/ab (Beispielinhalt beim ersten Zugriff); Besitzer und Rechte werden simuliert.
Terminal (terminal)
| Methode | Pfad | Beschreibung |
|---|---|---|
| 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/sessions | TerminalSessionInfo[] aller Terminal- und Konsolensitzungen |
| DELETE | /api/terminal/sessions/[id] | Sitzung beenden (Close 1001, das Pty wird beendet); 404 wenn unbekannt |
DefaultTerminalServicemeldet den Kanalterminalam Gateway an.bridge(socket, request, pty, target)verbindet Pty und Socket (Ausgabe binär, Eingabe/Größe als JSON, Gegendruck ab 4 MiB mitpause()/resume(), Auditterminal.open/terminal.close) und wird von der Incus-Konsole wiederverwendet;closeSession(id)beendet Sitzungen.ScriptPty(services/terminal/pty.ts):script -qfecmit geprüften, für/bin/shgequoteten Argumenten, eigene Prozessgruppe (SIGHUP, nach 3 sSIGKILL), Größe perstty -F /dev/pts/Ndes ersten Kindprozesses. Startfehler (fehlendesscript, 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.TerminalViewergänzt Tabs (bis 4) und die Sitzungsliste.
Netzwerk (network)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/network | NetworkOverview (Schnittstellen, Konfiguration, Routen, DNS, Apply-Zustand) |
| GET | /api/network/interfaces | NetInterface[] |
| GET | /api/network/state | NetworkApplyState |
| POST | /api/network/preview | NetworkConfig -> NetworkPreview (Dateien, entfernte Dateien, erwartete Adressen, Hinweise); 422 bei ungültiger Konfiguration |
| POST | /api/network/apply | NetworkConfig -> 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/rollback | sofort 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 Unitsbunkerbox-network-apply.service,bunkerbox-network-rollback.service,bunkerbox-network-rollback.timermit systemd-Quoting). DefaultNetworkService: serialisierte Abläufe, Zustand indata/network-state.json(600, sofort geschrieben), Aktivierung 1,5 s nach der Antwort per blockierendemsystemctl startder Apply-Unit, Token-Vergleich mittimingSafeEqual, 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 meldetactivity()network-apply(sperrt Updates).bridgeAvailable()prüft/sys/class/net/br0/bridgeundoperstate.LinuxNetworkSystemschreibt nur Positivlisten-Pfade (Units nach/etc/systemd/system, Staging unter/var/lib/bunkerbox-networkauf dem Root-Dateisystem, 700/600; die Rollback-Unit hatRequiresMountsFor=darauf, und jede Wiederherstellungszeile ist mit-markiert, damit networkd-Neustart undifupimmer laufen);MockNetworkSystemerzeugtip -j-JSON aus einer simulierten Konfiguration und nimmt uhrgesteuert zurück (keine echten Timer).
Spindown (spindown, Erweiterung von storage)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/storage/spindown | SpindownOverview (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]/standby | sofort 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)
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /api/storage/burnin | BurnInRun[] (neueste zuerst, mit Verlauf) |
| POST | /api/storage/burnin | StartBurnInRequest { 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/marks | BurnInMark[]: Platten mit bestandenem letztem Burn-in und passender Seriennummer |
| GET | /api/storage/burnin/[id] | BurnInRun |
| POST | /api/storage/burnin/[id]/cancel | Abbruch, wartet auf das Ende -> BurnInRun; Audit storage.burnin.cancel |
| GET | /api/storage/burnin/[id]/log | BurnInLog (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.
| Baustein | Klassen | Komponente |
|---|---|---|
| Seite | page, page-header, page-subtitle, page-actions, stack, stack-sm, row, spacer, grid grid-2/3/4, span-2 | PageHeader |
| Karte | card, card-header, card-title, card-subtitle, card-actions, card-body, card-body-flush, card-footer, card-section-title | Card, Stat |
| Kennzahl | stat, stat-label, stat-value, stat-sub, kv (dl) | Stat |
| Tabelle | table-wrap, table, table-compact, num, actions | – |
| Buttons | btn, btn-primary, btn-danger, btn-ghost, btn-small, btn-block, btn-icon, btn-group | – |
| Formular | form, form-grid, form-actions, field, field-label, field-hint, field-error, field-invalid, input, select, textarea, code-editor, input-group, input-addon, checkbox, switch | Field, Switch |
| Status | badge + tone-ok/warning/bad/info/active/muted, dot, chip, note note-info/ok/warning/bad, txt-ok/warning/bad, muted, small, mono | Badge, Note, ErrorNote |
| Fortschritt | progress (+ progress-small, progress-indeterminate, Ton-Klasse), progress-bar, spinner | ProgressBar, Spinner |
| Tabs | tabs, tab (aria-selected), tab-badge | Tabs |
| Dialog | dialog (+ dialog-danger, dialog-small, dialog-large), dialog-header, dialog-body, dialog-actions | Dialog, ConfirmDialog (getippte Bestätigung) |
| Leer | empty, empty-title, empty-action | EmptyState |
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
| Variable | Default | Bedeutung |
|---|---|---|
PORT | 80 (systemd), 3000 (npm run dev) | HTTP-Port |
BUNKERBOX_DATA_DIR | ./data | Persistenz (systemd: /appdata/bunkerbox/data) |
BUNKERBOX_MOCK | – | 1 = simulierte Hardware (Entwicklung ohne Linux/root) |
BUNKERBOX_UPDATE_URL | https://bunkerbox.znbw.de | Basis-URL der Update-Schnittstelle |
BUNKERBOX_UPDATE_CHECK | an | 0 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.tsgeprü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-secret600; Zugangsdaten für Netzlaufwerke liegen indata/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).