199 lines
12 KiB
Markdown
199 lines
12 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`.
|
|
|
|
## Gesamtdatenmodell
|
|
|
|
Die ersten fünf Etappen materialisieren Identität, Inhaltsverwaltung,
|
|
Sendungsplanung und Live-Durchführung. Folgende Grenzen gelten für umgesetzte
|
|
und spätere Bereiche.
|
|
|
|
| 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 sind umgesetzt; 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.
|
|
Piece-Dauern werden in den Fundus- und Verwendungseditoren als Minuten erfasst,
|
|
an der Servergrenze auf volle Sekunden gerundet und ausschließlich als ganze
|
|
Sekunden persistiert.
|
|
|
|
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.
|
|
|
|
## Sendungsbau
|
|
|
|
Etappe 4 bündelt alle Planungsmutationen in `src/lib/server/shows.ts`. Eine
|
|
Sendung ist die Konflikt- und Transaktionsgrenze: Jede Änderung an Metadaten,
|
|
Abschnitten, Verwendungen oder Reihenfolge vergleicht die erwartete Revision und
|
|
erhöht sie atomar. Abgeschlossene Sendungen sind bereits an dieser Grenze gegen
|
|
weitere Bearbeitung geschützt.
|
|
|
|
Beim Einplanen werden Titel, Dauer, Einleitung, Recherche, Moderation,
|
|
Überleitung und Präsentation aus dem sendefertigen Piece kopiert. Eigene Notizen
|
|
liegen ausschließlich an der Verwendung. Die Referenz zum Original bleibt für
|
|
Historie und Nachvollziehbarkeit erhalten, wird aber nicht live gespiegelt.
|
|
|
|
Jede Sendung besitzt intern genau einen ausgeblendeten Reserveabschnitt. Dadurch
|
|
verwenden Plan und Reserve dieselbe geprüfte Verwendungslogik, während die
|
|
Reserve in der öffentlichen Struktur und allen Dauerberechnungen getrennt
|
|
bleibt. Positionen werden nach jeder Transaktion kollisionsfrei normalisiert.
|
|
Eine explizite Abschnittsdauer ersetzt die Summe der Verwendungen vollständig.
|
|
|
|
Sendungsvorlagen und ihre Abschnitte sind eigene Tabellen. Beim Anlegen werden
|
|
die Abschnitte mit neuen IDs kopiert; spätere Änderungen an der Sendung können
|
|
die Vorlage deshalb nicht verändern.
|
|
|
|
Die Editorformulare bleiben progressive SvelteKit-Form-Actions, werden im
|
|
Browser aber mit `use:enhance` gesendet. Dadurch bleibt das Dokument bei
|
|
Planungsmutationen bestehen; nur die serverseitigen Seitendaten werden
|
|
aktualisiert. Ladeanzeige und FLIP-Animation liegen am betroffenen Abschnitt
|
|
oder an der betroffenen Verwendung. Eindeutige Drag-Griffe übertragen entweder
|
|
eine Quellenkopie, einen Abschnitt oder eine Verwendung auf dieselben
|
|
serverseitig geprüften Actions wie die alternativen Schaltflächen. Während des
|
|
Ziehens bleibt das Original transparent an seinem Ausgangsort; eine flüchtige
|
|
Vorschau in der Liste belegt bereits die anhand der Zeigerposition berechnete
|
|
Zielhöhe und verdrängt dadurch die benachbarten Elemente. Persistiert wird erst
|
|
beim Loslassen.
|
|
|
|
## Live-Modus und Präsentationsgrenze
|
|
|
|
`src/lib/server/live.ts` ist die Transaktionsgrenze der Live-Durchführung. Der
|
|
Live-Stand besitzt eine eigene Revision. Jeder steuernde Befehl trägt zusätzlich
|
|
eine vom Browser erzeugte Befehls-ID; `(show_id, command_id)` ist eindeutig und
|
|
liefert bei Wiederholung das bereits gespeicherte Ergebnis. Tatsächliche Zeiten
|
|
liegen als mehrere Start-/Endintervalle je Verwendung vor. Damit bleiben
|
|
Zurückgehen, Wiederaufnahme und bereits abgeschlossene Laufzeit getrennt von der
|
|
geplanten Dauer nachvollziehbar.
|
|
|
|
Die interne Route `/live` benötigt weiterhin eine aktive Sitzung und darf
|
|
Recherche, Moderation und eigene Arbeitsdaten lesen. Die separate Ausgabe
|
|
`/heinzlive/[token]` liegt dagegen in einem reduzierten Layout ohne Navigation
|
|
oder Nutzerdaten. In SQLite liegt nur der SHA-256-Hash des 256-Bit-Tokens; ein
|
|
neu erzeugter Ausgabezugang widerruft den vorherigen. Page, Polling-JSON und
|
|
Medienroute leiten ihre Daten aus einer eigenen Positivliste ab. Bilder werden
|
|
nur ausgeliefert, wenn sie zum aktuell ausdrücklich präsentierten Piece gehören.
|
|
Normale Auswahl, Fortschaltung oder Wiederaufnahme verändern den
|
|
Präsentationsstand nicht.
|
|
|
|
Die Ausgabe synchronisiert ihren kleinen öffentlichen Zustand per
|
|
`no-store`-Polling. Der letzte gültige Inhalt bleibt bei einem Verbindungsfehler
|
|
sichtbar; die Oberfläche kennzeichnet den Abbruch und versucht die Verbindung
|
|
mit verlangsamtem Intervall erneut. Private Live-Daten werden nicht Teil dieses
|
|
Zustandsobjekts.
|
|
|
|
## 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 Arbeitsansicht. Die
|
|
Präsentationsausgabe verwendet eine separate, strikt reduzierte Datenansicht
|
|
ohne interne Anwendungsnavigation.
|
|
|
|
## 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.
|