Ctrl K
ComunidadGuías…

docs/01-ARCHITECTURE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 01: Architektur

> Hinweis: Das Produkt heißt **Nodvard Deck**, im Code `nodvard_deck`. Früher hieß es „Lattice“;
> welche alten Namen weiter gelten und welche bewusst bleiben, steht in [§ Alte und neue Namen](#alte-und-neue-namen).

## 1. Die drei Schichten

```
┌──────────────────────────────────────────────────────────────┐
│  CLIENTS                                                     │
│  React-Web (statisch vom Backend) │ Flutter-Android (APK)    │
└────────────────────┬─────────────────────────────────────────┘
                     │  eine REST/WS-API  (/api/v1, /ws)
┌────────────────────▼─────────────────────────────────────────┐
│  CORE — generisch, umbenennbar, kennt keine Extension        │
│                                                              │
│  Auth/RBAC · Vault · Audit · Events · Scheduler · Gate       │
│  Execution (SSH) · Hosts · Settings/Branding · Ext-Host      │
└────────────────────┬─────────────────────────────────────────┘
                     │  nodvard_sdk  (Python-Vertrag)
                     │  window.__nodvardDeck + UI-Kit (React-Vertrag)
┌────────────────────▼─────────────────────────────────────────┐
│  EXTENSIONS — alles Infrastruktur-Spezifische                │
│  proxmox · shield · terminal · nextcloud · scripts ·         │
│  docker · backups · gameserver · …                           │
└──────────────────────────────────────────────────────────────┘
```

**Die eine Regel, aus der alles andere folgt:** Abhängigkeiten zeigen nur nach oben.
Eine Extension importiert `nodvard_sdk`. Der Kern importiert **nie** eine Extension und
kennt keinen Extension-Namen. Der Kern kennt nur *Fähigkeiten* (Protokolle), nie
*Anbieter*.

Das ist maschinell prüfbar und wird geprüft: `scripts/check_core_purity.py` läuft in CI
und schlägt fehl, wenn im Code von `backend/src/nodvard_deck/` ein Wort aus einer Sperrliste
(`proxmox`, `nexus`, `ollama`, `ntfy`, `nextcloud`, `truenas`, `syncthing`,
`teleport`, `docker`, dazu `shield` als eigenes Wort) oder das Paket-Präfix der Extensions
(`nodvard_deck_ext_`) auftaucht. Bei `shield` zählen `asyncio.shield(…)` und Symbolnamen mit
Bindestrich wie `shield-check` nicht; jede dieser Ausnahmen ist in `backend/tests/test_core_purity.py`
an ihrer Fundstelle belegt.
Kommentare und Docstrings sind ausgenommen: verboten ist die *Abhängigkeit*, nicht die
Erwähnung: ein Protokoll darf und soll in seiner Dokumentation Beispiele nennen.

Die Prüfung läuft automatisch, weil eine Regel allein nicht reicht: Ohne sie erodiert
die Grenze innerhalb weniger Wochen, weil "nur diese eine Sonderbehandlung" immer
plausibel klingt.

**Dasselbe Prinzip in der Oberfläche:** Der Code einer Erweiterung unter `extensions/<id>/frontend/src` bindet nur
sich selbst und das gemeinsame UI-Kit (`extensions/_shared/frontend/src`) ein, nie Quelltext des Kerns und nie eine
andere Erweiterung, ohne Ausnahmen (`backend/tests/test_extension_frontend_imports.py` prüft es). Was Kern und
Erweiterungen teilen, etwa die Server-Auswahl und den Zeitplan-Wähler, liegt im Kit; der Kern bindet es über
`frontend/kit.resolve.ts` an ([02 §5](02-EXTENSION-API.md#5-seiten-volle-ui-ansichten)).

---

## 2. Prozessmodell

**Ein Prozess.** Extensions sind Python-Pakete, die in den Kern-Prozess geladen werden:
keine Sidecars, keine Subprozesse, kein gRPC.

Begründung: Zielhardware ist ein Raspberry Pi oder ein kleiner x86-Rechner; ein Prozess pro Extension
wäre bei acht Extensions der teuerste Teil des Systems. Und: es gibt genau einen
Administrator, der alle Extensions selbst schreibt oder bewusst installiert.

**Was dazu offen gesagt werden muss:** eine Extension läuft mit den
vollen Rechten des Kerns. Das Berechtigungsmodell (Manifest-Permissions, siehe
[02](02-EXTENSION-API.md)) ist eine *Struktur- und Nachvollziehbarkeitsmaßnahme*, keine
Sandbox. Es verhindert Versehen und macht Zugriffe im Audit sichtbar, es hält keinen
bösartigen Code auf. Das ist dieselbe Vertrauensposition wie bei WordPress-Plugins,
Home-Assistant-Custom-Components oder Proxmox-Hooks. Sie muss benannt, nicht versteckt
werden.

Damit ein späterer Wechsel möglich bleibt, ist der `ExtensionContext` bewusst so
geschnitten, dass jeder Aufruf serialisierbar ist (Daten rein, Daten raus, keine
geteilten Objekte über die Grenze). Wer später Isolation braucht, tauscht die
Context-Implementierung gegen einen RPC-Proxy aus, ohne dass eine Extension sich ändert.

### Laufzeit im Prozess

| Teil | Technik |
|---|---|
| HTTP/WS | FastAPI auf uvicorn, **ein** Worker |
| Hintergrundschleifen (Watcher) | `asyncio.Task`, vom Extension-Host verwaltet und beim Stop sauber abgebrochen |
| Geplante Jobs | ein APScheduler-Objekt, `MemoryJobStore`; dauerhaft ist nur die Tabelle `jobs` |
| Blockierendes | `asyncio.to_thread` / dedizierter Executor, nie im Loop |
| SSH | ein `asyncssh`-Verbindungspool, geteilt von Terminal, Scripts und KI |

---

## 3. Der Lebenszyklus eines Starts

```
1. Config laden (.env → Settings)
2. Vault entsperren (Master-Key-Datei oder Passphrase)
3. DB verbinden, Alembic-Migrationen prüfen (Kern + je Extension ein Branch)
4. Kern-Dienste konstruieren (Events, Audit, Scheduler, Gate, Execution, Notify)
5. Extensions entdecken → Manifeste lesen (ohne Code-Import!)
6. Je aktivierte Extension: importieren, Version prüfen, Permissions prüfen,
   setup(ctx) aufrufen  → sie registriert Routen/Widgets/Connectors/Jobs/Capabilities
7. Router montieren, Widget-Katalog einfrieren
8. Scheduler starten
9. Je Extension: on_start(ctx) → Hintergrundtasks anlaufen lassen
10. HTTP-Server bereit
```

**Umgebungsvariablen (Schritt 1).** Jede Einstellung aus `config.py` heißt als Variable
`NODVARD_DECK_<FELD>` (z. B. `NODVARD_DECK_ENV`, `NODVARD_DECK_DATABASE_URL`). Die früheren
Namen `LATTICE_<FELD>` gelten nur noch übergangsweise als Rückfall; eine der nächsten Versionen, spätestens 1.0, liest
sie nicht mehr. Reihenfolge, vorne gewinnt: Argumente im
Code, Umgebung (neu), Umgebung (alt), `.env` (neu), `.env` (alt), Secret-Dateien. Die Prozess-
Umgebung schlägt also immer die `.env`, und der neue Name schlägt den alten. Beim Start
steht einmalig eine Warnung im Log, welche alten Namen noch benutzt werden (nur die Namen,
nie die Werte).

Schritt 5 vor Schritt 6 ist wichtig: das Manifest muss lesbar sein, **ohne** den
Extension-Code auszuführen. Nur so kann eine deaktivierte oder inkompatible Extension in
der Registry-UI erscheinen, ohne zu laufen, und nur so kann eine kaputte Extension den
Start nicht verhindern.

**Fehlerverhalten:** wirft eine Extension in `setup`/`on_start`, wird sie als
`state=error` mit der Exception in der Registry markiert und **übersprungen**. Der Kern
startet trotzdem. Eine defekte Erweiterung darf nie das Dashboard lahmlegen: ein
bekannter Fehlermodus von Dashboards, die Module per Iframe einbinden (ein Iframe-Fehler =
ein toter Tab).

---

## 4. Das Aktions-Gate

Jede zustandsverändernde Aktion einer Extension läuft durch **eine** Stelle im Kern.
Das ist kein Feature von Nodvard Shield, sondern ein Kern-Dienst, den auch das
Script-Repository, das Gameserver-Modul und der Dateimanager benutzen. Es ist bewusst
als generischer Baustein angelegt.

```
Extension                Core Gate                        Ergebnis
   │                         │
   ├─ propose(ActionRequest)─▶│
   │                         ├─ 1. Extension-Permission?      → deny
   │                         ├─ 2. RBAC des Akteurs?          → deny
   │                         ├─ 3. Sperrliste (Muster)?       → deny
   │                         ├─ 4. Anti-Flapping / Rate?      → deny
   │                         ├─ 5. Autonomie-Modus?
   │                         │     propose  → require_confirmation   (DEFAULT)
   │                         │     full     → allow, falls risk ≤ Schwelle
   │                         ├─ 6. IMMER: Audit-Zeile schreiben
   │                         │            (inkl. reason / BEGRUENDUNG)
   │                         ▼
   │                   actions-Tabelle: status = proposed | approved | denied
   │                         │
   │                         ├─ proposed → WS-Event → Karte in der UI → Klick
   │                         └─ approved → ActionExecutor läuft → Ergebnis + Audit
```

Der entscheidende Punkt: **Vorschlagen und Ausführen sind getrennte Schritte mit einem
persistenten Datensatz dazwischen.** Wo ein einziger Aufruf eine Abhilfe zugleich
auswertet *und* ausführt, lässt sich "erst bestätigen" nachträglich nicht sauber
einbauen. Durch die Trennung ist der Bestätigungsmodus der *natürliche* Zustand und volle
Autonomie die Ausnahme, die einen Schalter braucht.

Nebeneffekt: der Nutzer bekommt keine Meldung über eine Aktion, die gar nicht
stattgefunden hat. Der Text in Benachrichtigung und UI wird aus dem `actions`-Datensatz
gebaut, nicht aus der Absicht eines Modells.

**Ausführung im Hintergrund.** Ist eine Aktion genehmigt (Klick, Modus `full` oder Dauerfreigabe),
schreibt das Gate `executing` fest und startet den `ActionExecutor` als eigenen
`asyncio.Task` mit eigener Datenbank-Session (`core/gate.py`, `start_execution`). Die
HTTP-Anfrage wartet höchstens 20 s darauf und antwortet sonst mit `202 executing`; das
Ergebnis samt Audit-Zeile schreibt der Task selbst. Er endet immer mit `succeeded` oder
`failed`, auch wenn der Executor wirft, nach 60 min noch hängt (`EXECUTE_TIMEOUT_S`,
Notbremse über den Grenzen der Extensions) oder das Dashboard beendet wird. Was ein harter
Absturz auf `executing` stehen lässt, setzt der Kern beim nächsten Start auf `failed`.
`ctx.actions.propose()` wartet ohne Angabe bis zum Ende (Hintergrund-Jobs); Extension-Routen
geben `wait_s=REQUEST_WAIT_S` mit und melden danach ggf. `executing`.

**Dauerfreigabe.** Ein Vorschlag kann sich auf eine Freigabe berufen, die ein Mensch vorab erteilt hat
(`ActionRequest.standing_approval`, [02 §3](02-EXTENSION-API.md#3-capabilities--der-kern-der-entkopplung); bisher nur
geplante Skripte). Nach Sperrliste und Anti-Flapping prüft das Gate bei jedem Vorschlag selbst, ob diese Person noch
aktiv ist und Dauerfreigaben sowie Aktionen dieser Risikostufe freigeben darf; Vorschläge einer KI laufen nie darüber.
Gilt sie, ersetzt sie den Klick, unabhängig von `autonomy.mode`, und die Person steht als Freigebende an der Aktion;
sonst wird ein normaler Vorschlag daraus. Was passiert ist, hängt das Gate selbst an `reason` an, das Datum der
Freigabe in der eingestellten Zeitzone des Dashboards.

**Eine Ausnahme: Einrichten mit Passwort.** `POST /hosts/{id}/credentials/{cid}/setup-with-password` führt einen
Befehl als root auf dem Server aus, geht aber bewusst nicht durchs Gate: Ein Vorschlag legt seine Eingaben in
`actions.payload` ab, und das einmalige Passwort darf nirgends gespeichert werden. Die Route verlangt deshalb
`hosts.write` und `hosts.execute`, der Mensch löst sie mit dem Absenden selbst aus, und jeder Versuch steht im Protokoll
(`host.setup_with_password`, ohne Passwort).

**Einstellungen (nur global, keine Überschreibung pro Extension):**

```
autonomy.mode          = "propose" | "full"                          default: propose
autonomy.max_risk      = "low" | "medium" | "high" | "critical"      default: low
```

---

## 5. Hosts gehören dem Kern, nicht der Extension

Das ist die zweite Stelle, an der die Extension-Grenze leicht falsch gezogen wird.

Der Kern besitzt die Tabelle `hosts`. Extensions **entdecken** Hosts
(`HostProvider`-Capability) und schreiben sie dort hinein, mit Rückverweis
(`provider_ext_id`, `provider_ref`). Ein Host kann auch manuell angelegt werden.

Damit adressieren Terminal, Script-Ausführung, Dateimanager, Audit-Log und
Berechtigungen alle **dieselbe** Host-Identität — unabhängig davon, ob sie aus Proxmox,
aus einer Docker-API, aus einer Inventar-Datei oder aus einem Formular kam. Ohne diese
Regel hätte jede Extension ihre eigene Host-Liste: eine im Monitoring-Skript, eine für
die Web-Links, eine im SSH-Zugangsdienst, eine im Kachel-Dashboard — vier Wahrheiten,
die auseinanderdriften.

---

## 6. Branding als Konfigurationswert

```
GET /api/v1/branding      (ohne Auth, die Login-Seite braucht es)
→ { "product_name": "...", "short_name": "...", "logo_url": "...",
    "favicon_url": "...", "colors": { "accent": "#…", … },
    "login_subtitle": "...", "support_url": "..." }
```

Das Frontend schreibt die Farben beim Start in CSS-Custom-Properties auf `:root`.
Kein Rebuild, kein Neustart, keine Umgebungsvariable. Logo und Favicon liegen als Upload im
Daten-Volume (`branding/`); andere Adressen nimmt `PUT /branding` dafür nur als eigenen Pfad oder
`data:image/…` an. Skripte lädt die App-Seite nur vom eigenen Server: `script-src 'self'` plus die Prüfsumme der
eingebetteten Import-Map aus `index.html` gilt fest (`Content-Security-Policy`), es läuft
also kein `javascript:`-Link, kein `onclick=` aus eingeschleustem HTML und kein `eval`. Die Prüfsumme entsteht beim
Ausliefern und wird neu berechnet, sobald sich `index.html` ändert; ein neues eingebettetes Skript braucht also keine
Pflege. Alles andere kommt ebenfalls nur vom eigenen Server (`default-src 'self'`); `connect-src` erlaubt die eigene API
und WebSockets nur an der eigenen Adresse;
Bilder dürfen zusätzlich `data:` und `blob:` sein (`img-src 'self' data: blob:`), Rahmen nur `blob:` (die
PDF-Vorschau in Dokumente). Die CSP wird ohne Wildcards und ohne `unsafe-inline` erzwungen: Dynamische Stile und
xterm-Stylesheets erhalten eine frische Nonce je nicht cachebarer HTML-Antwort, Stilattribute sind gesperrt, Schriften
kommen nur vom eigenen Server; `NODVARD_DECK_CSP_LEGACY=1` aktiviert im Notfall die bisherige Stilfreigabe mit Logwarnung.
JSON-Antworten ohne eigene Regeln bekommen
`default-src 'none'`. Was das für Erweiterungs-Bundles heißt, steht in
[02-EXTENSION-API.md](02-EXTENSION-API.md) unter „Frontend-Bundle laden“. Links aus Daten von
Erweiterungen (App-Kacheln, Befehlspalette, Widgets, Erweiterungsseiten) nimmt die Oberfläche nur mit `http:` oder
`https:`, Ziele von Meldungen nur als Pfad im Dashboard (`/…`, nicht `//…`).
Ein SVG wird beim Hochladen geprüft (keine Skripte, keine Verweise aus der Datei hinaus), jedes
hochgeladene Bild wird mit einer Sandbox-CSP ausgeliefert;
der Support-Link nimmt nur `http:`, `https:` und `mailto:` an ([04-API](04-API.md)).

Im Code gilt: **kein Literal für Produktname oder Farbe** außerhalb von
`branding.py` (Backend-Defaults) und der CSS-Variablen-Definition (Frontend). Auch
`<title>`, E-Mail-Absender, Push-Titel und APK-Label ziehen aus derselben Quelle.
Der CI-Purity-Check deckt das mit ab. Bewusste Ausnahme: Die Anmeldeseite zeigt immer
„© 2026 Nico Benks · Nodvard Deck“ (`copyrightLine()` in `frontend/src/lib/branding.ts`, ab 2027 mit
Jahresspanne). Eigenes Branding ändert, wie die Oberfläche heißt, nicht, wem die Software gehört.

---

## 7. Sicherheitsposition, zusammengefasst

| Bereich | Position |
|---|---|
| Extensions | Vertrauenswürdiger Code. Permissions = Struktur + Audit, keine Sandbox. |
| KI-Aktionen | Aus KI-Text legt Nodvard Shield höchstens eine feste Aktion an: den Neustart eines Containers, dessen Absturz der Vorfall meldet. Befehl und Begründung baut der Code selbst, der Server kommt aus dem Vorfall; alles andere bleibt Text, der Chat legt nie eine Aktion an. Container-Logs und Docker-Fehlermeldungen gehen als fremde Daten (keine Anweisungen) an die KI. Dazu: Sperrliste für katastrophale Muster, Begründungspflicht, Anti-Flapping, Default = Bestätigung, nie über eine Dauerfreigabe. |
| Secrets | Nie im Klartext in DB, Antwort oder Log. Extensions bekommen Handles; ersetzen und löschen nur ihre eigenen, Zwei-Faktor-Geheimnisse von Konten gar nicht. Ein Zugang für eine andere Extension geht nur über eine Capability (`ConnectionReceiver`), nie über den Ereignis-Bus. |
| SSH | Eine Identität aus dem Vault, Known-Hosts-Pinning statt `StrictHostKeyChecking=no`. |
| API | Alles authentifiziert außer den Endpunkten in [04 §2](04-API.md#2-ohne-authentifizierung) (u. a. `/health`, `/branding`, `/capabilities`, `/auth/login`). Routen von Erweiterungen verlangen ohne Angabe eine Anmeldung; ohne Anmeldung nur mit `public=True` und der Manifest-Berechtigung `api.public` (steht beim Einschalten im Protokoll). Mehr als 1 MiB Body bekommt kein Endpunkt zu lesen, auch ohne Anmeldung nicht (darüber `413`; Uploads haben eigene, höhere Grenzen). |
| Audit | Append-only, jede Aktion, auch die abgelehnten und die nur vorgeschlagenen. |
| Netz | Kein Port nach außen nötig (geht auch hinter DS-Lite/CGNAT); Zugriff über Reverse-Proxy im LAN oder ein VPN wie Tailscale/WireGuard. |

**Bewusst nicht gelöst:** Multi-Tenancy, horizontale Skalierung,
Extension-Sandboxing, SSO/OIDC. Alle vier sind später additiv möglich und keiner davon
hat in einem Ein-Administrator-Homelab einen Nutzen, der die Komplexität rechtfertigt.

## Alte und neue Namen

Das Projekt hieß früher „Lattice“. Die technischen Namen sind auf **Nodvard Deck** umgestellt.
Damit bestehende Installationen, alte Browser-Tabs und Erweiterungen von Dritten weiterlaufen,
gelten viele alte Namen **parallel** weiter; einige bleiben **bewusst** ganz stehen. Der Wächter
`scripts/check_legacy_names.py` (läuft als Test) sorgt dafür, dass kein alter Name neu eingeführt
wird. Er hat zwei Positivlisten, beide mit Begründung je Eintrag: `KEPT_NAMES` für „Lattice“ und
`KEPT_SHIELD` für die alte Kennung von Nodvard Shield (dort nennt jeder Eintrag zusätzlich die Dateien, in denen er
gilt; ein Test prüft, dass jede Angabe noch gebraucht wird). Diese beiden Listen sind maßgeblich. Für die alte Kennung
des Moduls Docker braucht es keine Liste: Die Prüfung auf alte Namen verbietet nur das alte Paket und die alten Adressen.

**Umbenannt: der alte Name funktioniert weiter (Übergang):**

| Neu | Alt | Wie der alte weiterlebt |
|---|---|---|
| Erweiterungs-Pakete `nodvard_deck_ext_<id>` | `lattice_ext_<id>` | Kein Alias für die eingebauten (der `entrypoint` kommt frisch aus `extension.toml`); fremde Pakete mit altem Namen laden weiter |
| Umgebungsvariablen `NODVARD_DECK_*` | `LATTICE_*` | Rückfall, wenn die neue nicht gesetzt ist; nur noch übergangsweise (spätestens bis 1.0), mit Hinweis für den Inhaber in der Übersicht |

**Entfernt mit 0.12 (vor dem Versprechen für 1.0):**

| Neu | Alt | Was jetzt passiert |
|---|---|---|
| Python-Paket `nodvard_deck` | `lattice` | `import lattice` und `python -m lattice.…` gehen nicht mehr. Ein Image von vor der Umbenennung kennt dagegen nur den alten Namen ([deploy/README](../deploy/README.md), Rückweg) |
| SDK `nodvard_sdk`, `NodvardExtension`, `NodvardError` | `lattice_sdk`, `LatticeExtension`, `LatticeError` | Eine Erweiterung, die einen alten Paket- oder Klassennamen importiert, lädt nicht. Ist sie eingeschaltet, nennt die Erweiterungsliste den Grund und den neuen Namen, sonst kommt derselbe Grund beim Einschalten (`ext/old_names.py`) |
| Entry-Point-Gruppe `nodvard_deck.extensions` | `lattice.extensions` | Ein Paket, das sich nur unter der alten Gruppe anmeldet, wird nicht geladen. Den Grund nennt das Protokoll und, wenn die Erweiterung eingeschaltet war, ihre Zeile in der Liste |
| Refresh-Cookie `nodvard_deck_refresh` | `lattice_refresh` | Wird weder gelesen noch geschrieben; wer nur das alte hatte, meldet sich einmal neu an ([04-API](04-API.md)) |
| Frontend: `window.__nodvardDeck`, Ereignisse `nodvard-deck:*`, CSS `.nodvard-deck-focus`, Import-Map `/nodvard-deck-shim/` | `window.__lattice`, `lattice:*`, `.lattice-focus`, `/lattice-shim/` | Bundles mit einem älteren UI-Kit, das nur die alten Namen kennt, finden die Kern-Shell nicht ([02-EXTENSION-API](02-EXTENSION-API.md)); ein alter Tab braucht einmal „Seite neu laden“ |
| Browser-Speicher `nodvard-deck.*` | `lattice.*` | Wird nicht mehr übernommen; die Übernahme lief seit der Umbenennung bei jedem Start |
| Image, Compose-Dienst, Container `nodvard-deck`, `deploy-nodvard-deck-1` | `lattice`, `deploy-lattice-1` | Bei einem Stand von vor 0.6 brechen `deploy_pi.sh`, `pi_switch.sh`, `backup.sh` und `restore.sh` ab und ändern nichts; den Umstieg liefert nur noch 0.11 aus ([deploy/README](../deploy/README.md#stand-von-vor-06)) |

**Bleiben bewusst:**

| Name | Warum |
|---|---|
| Datenbankdatei `lattice.db` (auch `db/lattice.db` in Sicherungen) | Umbenennen bräuchte eine Migration; nach einem Rollback öffnete das alte Image eine leere Datenbank |
| Docker-Volume `deploy_lattice_data` | Sonst startet das Dashboard leer |
| Zielordner `~/lattice-deploy-test` von `scripts/deploy_pi.sh` (Standardwert von `DEPLOY_ROOT`) | So ausgelieferte Installationen hängen daran; über `DEPLOY_ROOT` frei wählbar |
| Linux-Benutzer `lattice` (UID 1000) im Container | Dateirechte im Datenordner |
| Vorgabe `lattice` für `username` bei `POST /hosts/{id}/credentials/generate-key` (die Oberfläche schlägt beim Erzeugen `nodvard` vor und schickt den Namen immer mit); auf bestehenden Servern der Benutzer `lattice`, ältere Schlüssel mit Kommentar `lattice@<Name>` und alte sudo-Dateien `/etc/sudoers.d/lattice-<benutzer>` | Ältere Aufrufer der API schicken oft keinen Namen, ein anderer Standard wäre für sie eine stille Verhaltensänderung; bestehende Server sind so eingerichtet. Neue Schlüssel bekommen `nodvard@<Name>`, neue sudo-Regeln `/etc/sudoers.d/nodvard-<benutzer>`; eine alte Regel entfernt der Einrichtungsbefehl (mit sudo) nur, wenn sie genau die früher von Nodvard Deck angelegte ist |
| Namen auf verwalteten Servern (systemd-Einheiten `lattice-upgrade-*`, Rollback-Tags, Zustandsordner) | Fortsetzen und Zurückrollen von Updates hängen daran |
| API-Wert `source: "lattice"` | Dokumentierter Wert, Clients werten ihn aus |

### Nodvard Shield: `nexus-soc` → `shield` (seit 0.7)

Die Erweiterung **Nodvard Shield** (bis 0.6 „Nexus SOC“) hatte die technische Kennung `nexus-soc`. Seit 0.7
heißt sie `shield`, im Manifest steht `legacy_ids = ["nexus-soc"]` (Mechanik:
[02 „Erweiterung umbenennen“](02-EXTENSION-API.md#erweiterung-umbenennen-legacy_ids)). Es gibt **keine Migration**:
Gespeichertes wird weder kopiert noch umbenannt, ein Rückweg aufs alte Image findet alles unverändert vor.

**Umbenannt: der alte Name funktioniert weiter (Übergang, in allen 1.x):**

| Neu | Alt | Wie der alte weiterlebt |
|---|---|---|
| Kennung `shield` (`extension.toml`) | `nexus-soc` | `legacy_ids`. Bei bestehenden Installationen bleibt die Registry-Zeile `nexus-soc` die **Speicher-Kennung**: Einstellungen, Zustand, Rechte und Zeitpläne hängen weiter an ihr. Neue Installationen legen `shield` an |
| Ordner `extensions/shield/`, Paket `nodvard_deck_ext_shield` | `extensions/nexus-soc/`, `nodvard_deck_ext_nexus_soc` | Kein Alias (Erweiterungen importieren einander nicht). Liegt der alte Ordner noch da, wird Shield nicht geladen ([deploy/README](../deploy/README.md)) |
| Adressen `/api/v1/ext/shield/…` | `/api/v1/ext/nexus-soc/…` | Dieselben Routen hängen zusätzlich unter der alten Adresse, gleiche Anmeldeprüfung, im OpenAPI-Schema `deprecated` ([04 §7](04-API.md#7-kompatibilität)) |
| Kern-Adressen `/api/v1/extensions/shield/…` (Einstellungen, Geheimnisse, Test, Bundle) | `/api/v1/extensions/nexus-soc/…` | Die alte Kennung wird aufgelöst, Antworten nennen in `id` die neue |
| Seiten `/ext/shield/soc`, `/settings/extensions/shield` | `/ext/nexus-soc/soc`, `/settings/extensions/nexus-soc` | Die Oberfläche leitet weiter (Abfrage und Anker bleiben). Gespeicherte Links in Meldungen, Push und Lesezeichen gehen so weiter |
| Dashboard-Kacheln unter `shield` | Einträge unter `nexus-soc` | Behalten Platz, Größe und Sichtbarkeit; der alte Eintrag bleibt für einen Rückweg im Layout |
| Neue Protokoll-Einträge `shield.*` (z. B. `shield.incident`), Akteur `shield` | `nexus_soc.*`, `nexus-soc` | Alte Einträge bleiben, die Oberfläche zeigt bei beiden den Namen. Der Filter `GET /audit?action=shield.incident` findet die älteren `nexus_soc.incident` nicht |
| Logger `nodvard_deck.ext.shield`, Live-Kanäle `ext.shield.*`, Links in neuen Meldungen (`/ext/shield/soc`) | `…nexus-soc…` | Live-Nachrichten gehen zusätzlich an die alten Kanäle, damit offene Tabs mit altem Stand sie bekommen |

**Bleiben bewusst** (gespeichert oder auf den Servern; ein Rückweg hängt daran, wie bei `lattice.db`):

| Name | Warum |
|---|---|
| Registry-Zeile `nexus-soc` bestehender Installationen | Darin liegen alle Einstellungen; das alte Image findet sie nach einem Rückweg unverändert |
| Tabellen `ext_nexus_soc_*` und ihre Indizes `ix_ext_nexus_soc_*` | Umbenennen bräuchte eine Migration; der Rückweg ginge dann nur noch über die Notseite |
| Alembic-Revisionen der Erweiterung, ihre Dateinamen und der Zweig `nexus-soc` (Kopf `b8c9d0e1f2a3`) | Ausgelieferte Revisionen ändern sich nie; nur der Ordner ist mit umgezogen |
| Aktionsarten `nexus_soc.{restore,delete,install,upgrade,reboot,ban}` | Offene Vorschläge und Freigaben tragen sie, und das Gate findet den Ausführer nur über die Art. Die Oberfläche zeigt statt der Art ihren Namen |
| Geheimnis `nexus-soc-ollama-key` und das Recht `secrets.read:nexus-soc-*` | Der Schlüssel für Nodvard KI wird nur über sein Label gefunden |
| Rechte `soc.read` und `soc.manage` | Können in eigenen Rollen stehen; die Rechteprüfung kennt keinen Alias |
| Ordner auf den Servern: `/var/lib/nexus-quarantine` (Quarantäne), `/var/lib/nexus-updates` (laufende Updates) | Die Pfade stehen in gespeicherten Funden, und laufende Updates werden über den Ordner fortgesetzt |
| Alte Einträge in Protokoll, Meldungen, Läufen und Aktionen | Verlauf, wird nicht umgeschrieben |

### Docker: `service-matrix` → `docker` (seit 0.9)

Das Modul **Docker** (bis 0.8 „Service-Matrix“) hatte die technische Kennung `service-matrix`. Seit 0.9 heißt es
`docker`, im Manifest steht `legacy_ids = ["service-matrix"]`, genau wie bei Nodvard Shield. Es gibt **keine Migration**
(das Modul hat keine Tabellen); auf bestehenden Installationen findet ein Rückweg aufs alte Image alles unverändert vor
([deploy/README](../deploy/README.md)).

**Umbenannt: der alte Name funktioniert weiter (Übergang, in allen 1.x):**

| Neu | Alt | Wie der alte weiterlebt |
|---|---|---|
| Kennung `docker` (`extension.toml`) | `service-matrix` | `legacy_ids`. Bei bestehenden Installationen bleibt `service-matrix` die **Speicher-Kennung**: Einstellungen samt eigener Kachel-Adressen, der Zeitplan der Image-Prüfung und der Datenordner `data/ext/service-matrix/` (laufende Image-Updates) hängen weiter an ihr. Neue Installationen legen `docker` an |
| Ordner `extensions/docker/`, Paket `nodvard_deck_ext_docker` | `extensions/service-matrix/`, `nodvard_deck_ext_service_matrix` | Kein Alias (Erweiterungen importieren einander nicht) |
| Adressen `/api/v1/ext/docker/…` | `/api/v1/ext/service-matrix/…` | Dieselben Routen hängen zusätzlich unter der alten Adresse, gleiche Anmeldeprüfung, im OpenAPI-Schema `deprecated` ([04 §7](04-API.md#7-kompatibilität)) |
| Kern-Adressen `/api/v1/extensions/docker/…` | `/api/v1/extensions/service-matrix/…` | Die alte Kennung wird aufgelöst, Antworten nennen in `id` die neue |
| Seiten `/ext/docker/matrix`, `/settings/extensions/docker` | `/ext/service-matrix/matrix`, `/settings/extensions/service-matrix` | Die Oberfläche leitet weiter (Abfrage und Anker bleiben). Gespeicherte Links in Meldungen und Lesezeichen gehen so weiter |
| Dashboard-Kachel „Container“ unter `docker` | Eintrag unter `service-matrix` | Behält Platz, Größe und Sichtbarkeit (`legacy_ext_ids`) |
| Komponente `ContainersPage` im Bundle | `ServiceMatrixPage` | Zweiter Export, für Tabs, die während des Updates offen sind |
| `requires = ["docker"]` in anderen Modulen | `requires = ["service-matrix"]` | Der Kern löst die alte Kennung auf |
| Logger `nodvard_deck.ext.docker`, Links in neuen Meldungen (`/ext/docker/matrix`) | `…service-matrix…` | Alte Meldungen behalten ihren Link, er funktioniert weiter |

**Bleiben bewusst:**

| Name | Warum |
|---|---|
| Speicher-Kennung `service-matrix` bestehender Installationen samt Datenordner | Darin liegen Einstellungen und laufende Updates; das alte Image findet sie nach einem Rückweg unverändert |
| Protokoll-Namen `service_matrix.tile_url` und `service_matrix.image_update` | Gespeicherte Einträge und Filter hängen daran; die Oberfläche zeigt bei beiden einen Namen |
| Aktionsarten `container.*` und `docker.*` | Offene Vorschläge und Freigaben tragen sie; sie hießen nie nach dem Modul |
| Alte Einträge in Protokoll, Meldungen und Aktionen | Verlauf, wird nicht umgeschrieben |