Files
sendewerk/docs/ARCHITECTURE.md
T
2026-10-21 21:05:57 +02:00

137 lines
8.0 KiB
Markdown

# Architektur
## Systemgrenze
Sendewerk ist ein einzelner SvelteKit-Prozess. Browserseiten, serverseitige
Actions, spätere REST-Endpunkte und Hintergrundaufgaben verwenden dieselbe
Geschäftslogik. SQLite und ein lokales Upload-Verzeichnis sind die einzigen
persistenten Laufzeitabhängigkeiten. Externe Mail-, RSS- oder KI-Dienste sind
nicht Voraussetzung für Kernfunktionen.
```text
Browser -> SvelteKit-Routen -> Anwendungsdienste -> SQLite
| -> Upload-Verzeichnis
+-> später: Mail/RSS über Adapter
```
Servercode lebt unter `src/lib/server` und darf nicht in Browser-Bundles
gelangen. Routen liefern nur die für ihre Ansicht erlaubten Daten. Fachlogik
wird nicht an Svelte-Komponenten gebunden, damit Browseraktionen und die
spätere API dieselben Regeln verwenden.
## Laufzeit und Speicher
- Lokal: `.data/sendewerk.sqlite` und `.data/uploads`, jeweils konfigurierbar.
- Tests: pro Lauf ein temporäres Verzeichnis unter dem Betriebssystem-Tempdir.
- Cloudron: `/app/data/sendewerk.sqlite` und `/app/data/uploads`.
- Migrationen liegen nummeriert unter `migrations/`, laufen in Transaktionen
und werden in `schema_migrations` protokolliert.
- Der Seed nutzt stabile IDs und Transaktionen. Wiederholung liefert denselben
fachlichen Ausgangsbestand, ohne Migrationstabellen anzutasten.
- Zeitpunkte werden als ISO-8601-UTC-Text gespeichert. Dauern sind nichtnegative
ganze Sekunden. Zeitzonendarstellung ist `Europe/Berlin`.
## Geplantes Gesamtdatenmodell
Etappe 1 materialisiert nur den für echte Vorschau- und Seed-Daten benötigten
Kern. Folgende Grenzen sind für spätere Migrationen bereits festgelegt.
| Bereich | Geplante Entitäten und Beziehungen |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Identität | `users`, Rollen, Sessions, Einladungen, Reset-Tokens und Sperren sind umgesetzt; technische Zugänge folgen später |
| Taxonomie | getrennte `section_categories`, `formats`, `topics`; Pieces und Vorschläge über Zuordnungstabellen mehrfach verknüpft |
| Inhalt | `pieces` mit Status, Dauer, Revision, getrennten Textbereichen, Quellen und privaten Medien; Vorlagen sind definierte Startwerte |
| Sendungsbau | `shows`, geordnete `show_sections`, kopierte `piece_usages`, Reservebank und Sendungsvorlagen; Original-ID bleibt Referenz |
| Live | expliziter Live-Stand, aktuelles Element, Status je Verwendung, tatsächliche Zeiten und getrennte Präsentationsausgabe |
| Historie | unveränderliche Abschluss-Snapshots und Verwendungshistorie; Übersprungenes zählt nicht als gesendet |
| Freigaben | widerrufbare, gehashte Lesetokens mit Sendung, Umfang, Passwortoption und Ablaufzeit |
| RSS | Feedquellen, Abrufläufe und externe Einträge mit stabiler Herkunfts-ID sowie Status neu/übernommen/verworfen |
| API/Audit | technische Berechtigungen, Idempotenzschlüssel und Audit-Ereignisse mit Akteur, Aktion und Ziel |
| Betrieb | Anwendungseinstellungen und spätere Job-Zustände, keine Geheimnisse im Klartext ohne begründete Schutzstrategie |
### Historien- und Kopierregel
Beim Einplanen entsteht eine bearbeitbare `piece_usage`-Kopie der
sendungsrelevanten Texte und Dauer. Das Original bleibt verknüpft, wird aber
nicht live gespiegelt. Beim Abschluss wird der damalige Sendungsinhalt stabil
eingefroren. Eine historische Sendung wird dupliziert, nie weiterbearbeitet.
### Dauerregel
Eine Abschnittsdauer ist entweder die Summe ihrer eingeplanten Verwendungen
oder eine explizite Abschnittsdauer. Beides wird nie addiert. Reserveelemente
zählen erst nach dem Einplanen. Geplante und tatsächliche Dauer bleiben
getrennt.
## Anmeldung und Rechte
Etappe 2 ergänzt lokale Konten mit den festen Rollen `barkeeper` und
`thekencrew`. Passwörter werden mit Argon2id gespeichert. Sitzungs-, Einladungs-
und Reset-Tokens entstehen aus kryptografischem Zufall; in SQLite liegt jeweils
nur ihr SHA-256-Hash. Der Browser erhält ausschließlich das rohe Sitzungstoken
in einem `HttpOnly`-, `SameSite=Lax`-Cookie.
`hooks.server.ts` löst die Sitzung für jeden Request auf. Die Datenbankabfrage
verbindet die Session immer mit dem aktuellen Nutzerzustand, sodass Sperren und
Rollenwechsel ohne verzögerte Token-Erneuerung wirken. Geschützte Routen
erfordern eine Anmeldung; Verwaltungs-Loads und jede Verwaltungs-Action prüfen
zusätzlich die Barkeeper-Rolle. Sperrung und Passwortwechsel löschen bestehende
Sessions transaktional.
Der Erstadmin benötigt sowohl eine leere Nutzertabelle als auch ein extern
konfiguriertes Setup-Token. Einladungs- und Reset-Links sind 24 Stunden gültig,
werden erst beim bestätigenden POST verbraucht und sind genau einmal nutzbar.
Fehlgeschlagene Loginversuche werden pro E-Mail-Schlüssel und Clientadresse in
einem Zeitfenster begrenzt, ohne das Konto dauerhaft zu sperren.
Mail verwendet einen Adapter: `local` schreibt geschützte Dateien in den
konfigurierten Outbox-Pfad, `smtp` nutzt Nodemailer nur nach expliziter Auswahl.
Die feste Anwendungs-Origin erzeugt Links und bestimmt den `Secure`-Status der
Cookies; sie wird nicht aus ungeprüften Request-Headern übernommen.
## Inhalte, Markdown und Medien
Etappe 3 führt die Inhaltslogik in `src/lib/server/content.ts` zusammen. Listen
erhalten ausschließlich kompakte Piece-Daten; private Recherche und vollständige
Redaktionstexte werden nur an den angemeldeten Editor ausgeliefert. Themen und
Abschnittskategorien sind Mehrfachzuordnungen, das Format bleibt eine einzelne
unabhängige Zuordnung. Quellen werden gemeinsam mit einem Piece atomar ersetzt.
Jedes Piece besitzt eine ganzzahlige Revision. Speichern, Archivieren und
Medienänderungen verwenden ein bedingtes Update auf die erwartete Revision.
Eine veraltete Bearbeitung liefert einen Konflikt und verändert weder Piece noch
Zuordnungen. Archivieren merkt sich den vorherigen Arbeitsstatus für die
Wiederherstellung. Duplikate erhalten neue IDs und unabhängige Bilddateien.
Markdown wird roh gespeichert, mit Marked serverseitig gerendert und danach mit
sanitize-html auf eine kleine Allowlist reduziert. Bilder werden nicht über
Markdown eingebettet. Uploads akzeptieren höchstens 5 MB und nur anhand ihrer
Dateisignatur erkannte JPEG-, PNG-, WebP- oder GIF-Dateien. Interne Dateinamen
sind zufällige IDs; Originalnamen werden nie als Pfad verwendet. `/media/[id]`
prüft die Anmeldung und liefert private Antworten mit `no-store`, `nosniff`,
Same-Origin-Ressourcenrichtlinie und restriktiver CSP aus.
## Oberflächenstruktur
Die Hauptnavigation umfasst Eingang, Fundus, Sendungen, Archiv und Verwaltung.
Auf dem Desktop bleibt eine feste Seitenleiste sichtbar; Verwaltung liegt an
ihrem unteren Ende. Bis einschließlich `52rem` ersetzen eine obere Leiste und
die untere Hauptnavigation die Seitenleiste vollständig. Auf breiten Ansichten
stehen Fundus und Ablauf nebeneinander. Bis einschließlich `72rem` werden sie
über eine gut erreichbare Umschaltung einzeln gezeigt, damit auch schmale
Desktopfenster ohne horizontalen Überlauf bedienbar bleiben.
Der Live-Modus ist eine eigene, fokussierte Ansicht; die spätere
Präsentationsausgabe erhält eine separate, strikt reduzierte Datenansicht.
Etappe 1 zeigt Fundus, Sendungsbau und Live-Modus als sichtbare
`Gestaltungsvorschau`. Die Daten kommen bereits aus SQLite, mutierende
Fachabläufe werden jedoch nicht vorgetäuscht.
## Sicherheitsgrenzen ab den Folgeetappen
Authentifizierung und Autorisierung werden seit Etappe 2 serverseitig an jeder
betroffenen Route und Action geprüft. Markdown und fremdes HTML werden vor
Ausgabe bereinigt. Uploadzugriff folgt denselben Rechten. Spätere
Netzwerkabrufe erhalten Schutz gegen interne Ziele, Größen-/Zeitlimits und
kontrollierte Redirects.