# 00 — Architekturentscheidungen Dieses Dokument hält die grundlegenden Technik- und Architekturentscheidungen von Nodvard Deck fest, jeweils mit Begründung und Konsequenzen. Code, Kommentare und andere Dokumente verweisen mit der Kennung `D-` darauf; die Nummern bleiben stabil. --- ## Zusammenfassung | Bereich | Entscheidung | Anmerkung | |---|---|---| | Backend | Python 3.12 · FastAPI | genau ein Worker-Prozess (D-01) | | Frontend | React 18 · Vite · TypeScript | Tailwind + Radix (D-03) | | Mobile | Flutter | Widgets nur deklarativ (D-04) | | Datenbank | SQLite als Default | PostgreSQL optional (D-02) | | Terminal | xterm.js + SSH | `asyncssh`, nicht `paramiko` (D-05) | | Auslieferung | ein Multi-Arch-Docker-Image | ein Container, ein Volume (D-10) | --- ## D-01 Backend: Python 3.12 + FastAPI **Entscheidung:** Das Backend ist in Python 3.12 mit FastAPI geschrieben. **Begründung:** async für WebSockets und Terminal. Außerdem senkt Python das Risiko bei der Ablösung des Vorgängersystems, das ebenfalls in Python geschrieben war: bewährte Regex-Muster (z. B. die Sperrliste für Befehle), Prompts und Heuristiken (z. B. gegen Neustart-Schleifen) sind aus dem Vorgängersystem übernommen, statt in eine andere Sprache übersetzt zu werden. Das Ökosystem passt: `asyncssh`, `httpx`, `cryptography`, `pyotp`. **Auflage 1: genau ein Worker-Prozess** (`uvicorn --workers 1`), kein Gunicorn-Multiprocessing. Der Kern hält prozessinternen Zustand, der nicht dupliziert werden darf: Extension-Registry, Event-Bus, WebSocket-Hubs, Scheduler, SSH-Verbindungspool, Anti-Flapping-Historie. Skalierung erfolgt bei Bedarf über `asyncio`, nicht über Prozesse. Wer mehrere Prozesse will, muss Event-Bus und Scheduler vorher externalisieren — das ist ein bewusster, dokumentierter Bruch, kein Versehen. **Auflage 2: kein blockierender Code im Event-Loop.** Alles, was blockiert (`subprocess`, `paramiko`, Tesseract, ClamAV), läuft über `asyncio.to_thread` / einen expliziten Executor. Das ist die häufigste Fehlerquelle in FastAPI-Projekten dieser Größe und der Grund für die `asyncssh`-Entscheidung (D-05). **Verworfene Alternative:** Go. Wäre auf einem Raspberry Pi sparsamer und erlaubte eine Auslieferung als einzelne Binärdatei. Unterliegt aber bei der Übernahme aus dem Vorgängersystem und beim Python-Ökosystem für die Infrastruktur-Anbindung. Die Auslieferungsfrage löst stattdessen ein Multi-Arch-Docker-Image (D-10). --- ## D-02 Datenbank: SQLite als Default, PostgreSQL optional **Entscheidung:** SQLite ist die Standard-Datenbank. PostgreSQL ist vorgesehen und der Code bleibt dafür dialektneutral, ist aber **ungetestet**: Es gab noch keinen Testlauf gegen PostgreSQL, und der Treiber `asyncpg` ist nicht im Image. **Begründung:** 1. **Zielhardware.** Ein typisches Hosting-Ziel ist ein Raspberry Pi mit wenigen GB RAM, auf dem bereits andere Dienste laufen. Ein Postgres-Server kostet dort dauerhaft 100–200 MB RSS plus einen zweiten Dienst, der gepatcht, gesichert und überwacht werden will. Mit SQLite bleibt der Pi ein realistisches Hosting-Ziel. 2. **Betriebsaufwand.** Nodvard Deck soll sich ohne Begleitung betreiben lassen: offline, ohne Lizenzserver, mit Dokumentation statt Support-Kontakt. Jede zusätzliche Pflicht-Komponente ist eine weitere Hürde bei der Installation und eine weitere Fehlerquelle. `docker run` mit einem Volume muss reichen. 3. **Die Last rechtfertigt Postgres nicht.** Eine Handvoll Nutzer, ein paar Dutzend Hosts, Audit- und Run-Logs im Bereich einiger tausend Zeilen pro Woche. SQLite im WAL-Modus liegt Größenordnungen über diesem Bedarf. **Umsetzung:** SQLAlchemy 2.0 (async) + Alembic. `DATABASE_URL` entscheidet; `sqlite+aiosqlite:///./data/lattice.db` ist der Default. `postgresql+asyncpg://…` ist vorgesehen, gilt aber erst nach einem echten Testlauf als unterstützt. **Disziplin-Regeln, die die Wahlfreiheit erhalten — für jeden neuen Code verbindlich:** - Kein `JSONB`, kein `ARRAY`, kein `ILIKE`, kein `ON CONFLICT … DO UPDATE`-Dialektcode, keine Postgres-Funktionen. Nur `sa.JSON`, `sa.String`, portable Ausdrücke. - Listen (z. B. Host-Tags) als Assoziationstabelle, nicht als Array-Spalte. - Zeitstempel immer UTC-aware `DateTime(timezone=True)`, Default in Python gesetzt (nicht `server_default=func.now()`, das driftet zwischen den Dialekten). - SQLite-Pragmas beim Connect: `journal_mode=WAL`, `foreign_keys=ON`, `busy_timeout=5000`, `synchronous=NORMAL`. - Schreibzugriffe laufen über **einen** Session-Pfad; keine parallelen Writer-Tasks, die sich selbst blockieren. Lange Schreibvorgänge (Log-Import) in Batches. **Grenze, die SQLite nicht überschreiten darf:** Zeitreihen-Telemetrie (CPU-/RAM-Kurven pro Host im Sekundentakt) gehört **nicht** in die relationale DB — weder in SQLite noch in Postgres. Live-Werte liegen nur im Speicher. Wer lange Metrik-Historie will, bindet ein vorhandenes Prometheus/Grafana über einen Connector an. **Nachtrag: Metrik-Verlauf.** Einen Verlauf gibt es trotzdem, ohne diese Grenze zu verletzen. Hat die Quelle selbst Historie (Proxmox führt RRD-Daten), liefert der Anbieter sie über `MetricsProvider.history()` durch — Nodvard Deck speichert nichts. Nur für Hosts ohne solche Quelle (z. B. ein per SSH angebundener Raspberry Pi) sammelt `core/metrics_history.py` alle 30 s — in eine **eigene** SQLite-Datei `data/metrics.db` mit eigener Verbindung und genau einem Schreiber, nicht in `lattice.db`, ohne Alembic, jederzeit löschbar (dann fehlt nur der Verlauf). Rohwerte 48 h, 5-Minuten-Verdichtung (Mittel/Min/Max) 35 Tage. Der Sekundentakt bleibt der Live-Ansicht vorbehalten und wird nie gespeichert. --- ## D-03 Frontend: React 18 + Vite + TypeScript **Entscheidung:** React 18 mit Vite und TypeScript, ergänzt um folgende Festlegungen: - **UI-Basis: Tailwind CSS + Radix UI Primitives.** Bewusst *kein* fertiges Komponenten-Framework (MUI, Mantine, Ant). Grund: White-Labeling muss **zur Laufzeit** funktionieren — ein Admin ändert Farben in den Einstellungen, ohne das Frontend neu zu bauen. Das geht sauber nur über CSS-Custom-Properties, die aus `GET /api/v1/branding` gespeist werden. Fertige Frameworks bringen eigene, teils zur Build-Zeit fixierte Theming-Systeme mit und arbeiten dagegen. - **Server-State: TanStack Query.** Client-State: Zustand. Kein Redux. - **Routing: React Router v7** mit Lazy-Routes — Extension-Seiten hängen sich dort ein. - **Terminal: xterm.js** + `@xterm/addon-fit`. - **Editor (Script-Repository): ein einfaches `