Files
sendewerk/docs/ARCHITECTURE.md
T
2026-10-22 00:28:49 +02:00

12 KiB

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.

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.