Files
sendewerk/docs/DECISIONS.md
T
2026-10-23 07:47:27 +02:00

313 lines
16 KiB
Markdown

# Entscheidungen
Stand der Quellenprüfung: 22. Oktober 2026. Verwendet wurden offizielle
Dokumentationen und die npm-Registry für konkrete Paket-Metadaten.
## D-001: SvelteKit 3 und Node-Unterstützung
Die Anwendung verwendet SvelteKit 3, Svelte 5, TypeScript und Vite 8. Das
Projekt wurde gemäß aktueller Empfehlung mit `npx sv create` angelegt. Die
Kit-Konfiguration liegt bei SvelteKit 3 in `vite.config.ts`, nicht in einer
`svelte.config.js`.
Die lokale Mindestversion ist Node 22.17. Das Cloudron-Image nutzt Node 24 LTS.
Der vorhandene lokale Lauf wurde mit Node 22.23.1 geplant.
Quellen:
- <https://svelte.dev/docs/kit/creating-a-project>
- <https://svelte.dev/docs/kit/migrating-to-sveltekit-3>
- <https://svelte.dev/docs/cli/sv-create>
- npm-Metadaten für `@sveltejs/kit` 3.1.0 am 21.10.2026
## D-002: Offizieller Node-Adapter
`@sveltejs/adapter-node` erzeugt den Produktionsserver in `build/`. Gebaut wird
mit `npm run build`, gestartet mit `node build`. `HOST` und `PORT` konfigurieren
den Server. Der Adapter behandelt SIGTERM/SIGINT und kontrolliertes Herunter-
fahren; ein eigener Server ist für Etappe 1 nicht erforderlich.
Quelle: <https://svelte.dev/docs/kit/adapter-node>
## D-003: SQLite mit better-sqlite3
`better-sqlite3` 13.x wird als gekapselter synchroner Treiber verwendet. Das
Paket ist MIT-lizenziert und verlangt Node 22 oder neuer. Gegenüber
`node:sqlite` wird damit in Etappe 1 keine API verwendet, die in der offiziellen
Node-24-Dokumentation noch als Release Candidate gekennzeichnet ist. Die
Datenbankgrenze bleibt in `src/lib/server` gekapselt.
Migrationen sind eigene nummerierte SQL-Dateien und laufen transaktional. WAL
wird in Etappe 1 nicht aktiviert; für die kleine Redaktion ist das
Rollback-Journal zunächst ausreichend und vereinfacht die frühe
Betriebsprüfung.
Quellen:
- <https://nodejs.org/docs/latest-v24.x/api/sqlite.html>
- <https://www.sqlite.org/lang_transaction.html>
- npm-Metadaten für `better-sqlite3` 13.0.3 am 21.10.2026
## D-004: Cloudron-Paket
Das Paket nutzt `manifestVersion: 2`, internen HTTP-Port 3000 und `/healthz`.
`localstorage.sqlite.paths` nennt `/app/data/sendewerk.sqlite`, damit Cloudron
eine konsistente Sicherung der aktiven SQLite-Datei erstellen kann. Das
Startskript stellt Verzeichnisse und Besitzrechte her, migriert als Benutzer
`cloudron` und ersetzt sich zuletzt per `exec` durch den Node-Prozess.
Das Image ist auf die am 21.10.2026 offiziell dokumentierte datierte Node-24-
Basis samt Digest gepinnt. Außer `/app/data`, `/run` und `/tmp` wird das
Laufzeitdateisystem als read-only behandelt.
Quellen:
- <https://docs.cloudron.io/packaging/>
- <https://docs.cloudron.io/packaging/manifest/>
- <https://docs.cloudron.io/packaging/addons/#localstorage>
- <https://docs.cloudron.io/packaging/cheat-sheet/>
## D-005: Theme ohne serverseitiges Nutzerkonto
Etappe 1 speichert `light`, `dark` oder `system` in `localStorage`. Ein kurzes
Inline-Skript in `app.html` setzt das resultierende Theme vor dem ersten Paint
auf dem Wurzelelement. Dies vermeidet einen falschen Theme-Blitz und benötigt
vor Etappe 2 weder Cookie noch Konto. Die Einstellung bleibt bei Neuladen im
gleichen Browser erhalten.
## D-006: Etappe-1-Vorschauen statt Attrappen
Vorschauseiten lesen echte persistente SQLite-Seed-Daten, bieten aber keine
funktionslosen Speichern- oder Live-Schaltflächen. Ein sichtbarer Hinweis
grenzt Gestaltungsvorschauen von den späteren Fachfunktionen ab. Damit wird
keine noch nicht implementierte Funktion als fertig dargestellt.
## D-007: Arbeitsansicht nach verfügbarem Platz
Die feste Desktop-Seitenleiste wird bis `52rem` verwendet und dann vollständig
durch mobile Kopf- und Fußnavigation ersetzt. Der Sendungsbau schaltet bereits
bis `72rem` von zwei Spalten auf eine bedienbare Pane-Umschaltung um. Dadurch
bleibt die Desktopnavigation in schmalen Fenstern erhalten, ohne Fundus und
Ablauf zusammenzudrücken.
Der Fundus erscheint auf dem Desktop als kompakte Liste und auf kleinen
Bildschirmen als Kartenfolge. Vorschau- und Schreibschutzinformationen sind in
einem nativen, tastaturbedienbaren `details`-Element gebündelt. Diese
Darstellungsentscheidung ändert weder Datenmodell noch Funktionsumfang.
## D-008: Widerrufbare Datenbanksitzungen und Argon2id
Sendewerk verwendet Datenbanksitzungen statt JWT. SvelteKit empfiehlt diesen
Ansatz, wenn sofortiger Widerruf wichtiger als eine zusätzliche Datenbankabfrage
ist. Ein zufälliges Sitzungstoken liegt nur als SHA-256-Hash in SQLite; der
Browser erhält es als `HttpOnly`-, `SameSite=Lax`-Cookie. Nutzerstatus und Rolle
werden bei jedem Request erneut verbunden. Lokal erlaubt eine explizite
`http:`-Origin Cookies ohne `Secure`, bei `https:` wird `Secure` gesetzt.
Passwörter hasht `@node-rs/argon2` 2.2.2 mit Argon2id und den dokumentierten
Paketparametern. Zufallstokens und SHA-256 stammen aus `node:crypto`; eigene
Kryptoprimitive werden nicht implementiert.
Quellen:
- <https://svelte.dev/docs/kit/auth>
- <https://svelte.dev/docs/kit/hooks>
- <https://github.com/napi-rs/node-rs/tree/main/packages/argon2>
## D-009: Form-Actions, geschlossenes Setup und Rollenprüfung
Alle Auth-Schreibvorgänge verwenden POST-Form-Actions beziehungsweise einen
POST-Endpunkt. SvelteKits gleichursprüngliche Formularprüfung bleibt aktiv. Der
Erstadmin erfordert ein mindestens 32 Zeichen langes Betreiber-Setup-Token und
eine transaktional geprüfte leere Nutzertabelle. Danach bleibt `/setup`
geschlossen. Barkeeper-Rechte werden in jeder Verwaltungsaktion erneut geprüft;
UI-Sichtbarkeit ist keine Sicherheitsgrenze.
Quelle: <https://svelte.dev/docs/kit/form-actions>
## D-010: Lokale Mail-Outbox als sichere Vorgabe
Der Mailmodus ist standardmäßig `local` und schreibt Einladungs-/Resetnachrichten
in einen konfigurierten persistenten Pfad. Dadurch wird lokal keine echte Mail
ungefragt versandt. Nur die explizite Auswahl `smtp` initialisiert Nodemailer;
bei SMTP sind Host und Absender verpflichtend. Outbox-Dateien sind private
Geheimnisträger und werden weder versioniert noch über eine Route ausgeliefert.
Nodemailer 10.1.7 unterstützt Node 20 oder neuer und ist MIT-0-lizenziert.
Quelle: <https://github.com/nodemailer/nodemailer>
## D-011: Form-Actions und optimistische Piece-Revisionen
Piece-Schreibvorgänge bleiben normale SvelteKit-Form-Actions und funktionieren
damit ohne clientseitiges JavaScript. `use:enhance` ergänzt den Editor um
verzögertes Autospeichern und sichtbare Zustände. Vor interner Navigation wird
eine ausstehende Änderung gespeichert; beim Verlassen der Seite greift die
Browserwarnung. Jede Mutation vergleicht eine erwartete ganzzahlige Revision in
derselben SQLite-Transaktion. Veraltete Änderungen liefern 409 und überschreiben
keine aktuelleren Inhalte oder Zuordnungen.
Quelle: <https://svelte.dev/docs/kit/form-actions>
## D-012: Markdown immer nach dem Parsen bereinigen
Marked 18.1.1 rendert die gespeicherten Markdown-Texte serverseitig. Marked
weist ausdrücklich darauf hin, dass seine HTML-Ausgabe nicht bereinigt ist.
Darum reduziert sanitize-html 2.18.1 das Ergebnis anschließend auf ausgewählte
Text-, Listen-, Code- und Linkelemente. Skripte, Bilder, Ereignisattribute,
relative Protokoll-URLs und andere Schemata werden nicht zugelassen.
Quellen:
- <https://github.com/markedjs/marked>
- <https://github.com/apostrophecms/apostrophe/tree/main/packages/sanitize-html>
## D-013: Private Rasterbilder außerhalb statischer Assets
Uploads liegen ausschließlich im konfigurierten privaten Uploadverzeichnis und
werden über zufällige interne Namen adressiert. file-type 22.2.0 erkennt den Typ
aus dem Dateiinhalt; Client-MIME und Originalname entscheiden nicht über Typ oder
Pfad. Zugelassen sind JPEG, PNG, WebP und GIF bis 5 MB. SVG und allgemeine
Dokumente bleiben ausgeschlossen. Eine authentifizierte ID-Route liefert Bilder
mit sicherheitsrelevanten Headern aus. Das Adapter-Requestlimit liegt mit 6 MB
knapp oberhalb des fachlichen Dateilimits.
Quelle: <https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload>
## D-014: Sendung als transaktionales Aggregat
Eine ganzzahlige Revision auf `shows` schützt in Etappe 4 sämtliche Metadaten,
Abschnitte, Verwendungen und Reihenfolgen gemeinsam. Jede Mutation erhöht die
Revision in derselben SQLite-Transaktion. Damit können voneinander abhängige
Listenänderungen nicht teilweise gespeichert oder durch einen veralteten
Browserstand unbemerkt überschrieben werden.
Verwendungen speichern vollständige redaktionelle Text- und Dauerkopien. Die
Original-ID bleibt Referenz, ist aber keine Live-Datenquelle. Eine interne,
eindeutige Reservesection pro Sendung erlaubt dieselben geprüften Verschiebe-
und Löschoperationen für Plan und Reserve; Dauerabfragen schließen sie aus.
## D-015: Drag-and-drop nur als zusätzliche Bedienung
Abschnitte, Verwendungen und gefilterte Quellen können über eindeutige Griffe
gezogen werden. Dieselben Ergebnisse sind mit sichtbaren
Hoch-/Runter-Schaltflächen und Zielauswahl erreichbar. Diese Bedienelemente
funktionieren mit einfachem Klick, Touch und Tastatur auch ohne Drag-and-drop
oder clientseitige Spezialgesten. Das folgt WCAG 2.2 SC 2.5.7;
Tastaturzugang allein wäre keine ausreichende Zeigeralternative. Die
zugehörigen Form-Actions werden progressiv verbessert: Ohne JavaScript bleiben
normale POSTs möglich, mit JavaScript erfolgt die Aktualisierung ohne
Dokumentreload und mit lokalem Speicherzustand.
Quellen:
- <https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements>
- <https://svelte.dev/docs/kit/form-actions>
## D-016: Live-Befehle idempotent, tatsächliche Zeiten als Intervalle
Die Live-Steuerung verwendet eine eigene Revision und zusätzlich eine pro
Bedienaktion erzeugte Befehls-ID. Die Datenbank akzeptiert jede Kombination aus
Sendung und Befehls-ID nur einmal. Ein nach Verbindungsabbruch wiederholter
Befehl liefert dieselbe Ergebnisrevision und erzeugt weder einen zweiten
Statuswechsel noch ein zweites Zeitintervall. Laufzeit wird nicht als einzelner
überschreibbarer Wert, sondern als Summe abgeschlossener und höchstens eines
aktiven Intervalls ermittelt. Dadurch bleiben Zurückgehen und Wiederaufnahme
korrekt.
## D-017: Präsentationsausgabe als Positivliste mit gehashtem Zugang
Die heinzlive-Ausgabe ist kein eingeschränkter Aufruf der internen Live-Ansicht,
sondern eine eigene Datenprojektion. Zulässig sind ausschließlich ausdrücklich
präsentierter Titel und Präsentationstext, öffentliche Quellen sowie zugehörige
Bilder und Alternativtexte. Recherche, Einleitung, Moderation, eigene Notizen,
Ablauf, Nutzer und interne Dateinamen werden nicht in HTML oder JSON übernommen.
Ein zufälliger 256-Bit-Token steht in der URL, während SQLite nur seinen
SHA-256-Hash speichert. Medienzugriff prüft Token, aktuelle Präsentation und
Bildzuordnung gemeinsam. Synchronisierung erfolgt über kleines `no-store`-
Polling statt über eine dauerhaft offene Serververbindung.
## D-018: Verwendung aus bestehenden Sendungsdaten ableiten
„Gesendet“ ist kein Zustand am Fundus-Piece. Die Anzeige wird ausschließlich
aus der vorhandenen Original-ID, einer erledigten Verwendung außerhalb der
Reservebank und dem Status `completed` ihrer Sendung abgeleitet. Dadurch können
Einplanung, Reserve und Überspringen ein Original nicht versehentlich als
gesendet markieren, und mehrere tatsächliche Verwendungen bleiben als Historie
sichtbar.
Eine Duplikation im Sendungsablauf kopiert den konkreten Verwendungssnapshot und
nicht das Original-Piece. Die neue ID, der Status `open` und fehlende
Live-Intervalle trennen sie von der bisherigen Durchführung. Der allgemeine
Verwendungseditor darf den Live-Status nicht mehr aus Formulardaten schreiben;
Statuswechsel bleiben der Live-Geschäftslogik vorbehalten.
Eine bestehende Einplanung wird nicht automatisch mit späteren Änderungen am
Original synchronisiert. Die ausdrücklich ausgelöste Aktion **Aus Fundus
aktualisieren** überschreibt nur Titel, Dauer und Redaktionstexte. Eigene
Notizen, Status und Messintervalle bleiben erhalten. Alle Zuordnungen und Filter
verwenden ausschließlich `original_piece_id`, niemals veränderliche Titel. Im
allgemeinen Fundus gilt „eingeplant“ global für jede nicht abgeschlossene
Sendung; im Sendungsbau gilt es lokal für die aktuelle Sendung samt Reserve.
## D-019: Lokale Linkmetadaten und streng abgeleitete Video-Embeds
Sendewerk verwendet Metascraper ausschließlich serverseitig, um Titel,
Beschreibung, Bild und Herausgeber einer ausdrücklich gewählten vorhandenen
Quelle zu ermitteln. Die Ergebnisse werden gespeichert und nicht bei jedem
Seitenaufruf neu geladen. Manuell korrigierte Felder haben Vorrang. Ein eigener
HTTP(S)-Client prüft Protokoll, Port, DNS-Ergebnisse und jedes Redirectziel,
bindet den Request an die geprüfte öffentliche Adresse und begrenzt Redirects,
Laufzeit und Antwortgröße. Damit ist weder ein externer Vorschaudienst noch die
Übernahme fremden HTMLs erforderlich.
Player werden nur für streng erkannte YouTube- und TikTok-URLs erzeugt. Aus der
validierten Anbieter-ID baut Sendewerk die offiziellen Embed-Adressen selbst;
gespeichertes oder fremd geliefertes Embed-HTML wird nicht verwendet. YouTube
nutzt die datenschutzreduzierte `youtube-nocookie.com`-Adresse und den
`start`-Parameter. TikTok erhält eine Startposition nach `onPlayerReady` über
die dokumentierte Player-`postMessage`-API. Autoplay bleibt ausgeschaltet, und
ein Inhaltswechsel ersetzt das Iframe vollständig.
Quellen:
- <https://github.com/microlinkhq/metascraper>
- <https://developers.google.com/youtube/player_parameters>
- <https://developers.google.com/youtube/terms/required-minimum-functionality>
- <https://developers.tiktok.com/doc/embed-player>
- <https://developers.tiktok.com/doc/embed-videos>
- <https://owasp.org/www-community/attacks/Server_Side_Request_Forgery>
## D-020: Stabile Ausgabekanal-Adresse mit Fragment-Schlüssel
OBS soll eine dauerhaft konfigurierte Adresse verwenden können, ohne dass ein
Zugangsschlüssel in HTTP-Requests, Serverlogs oder Referer-Headern erscheint.
Darum bleibt die Kanaladresse `/heinzlive/default` stabil und trägt den
zufälligen Schlüssel ausschließlich als URL-Fragment. Ein kleiner Browser-POST
tauscht ihn gegen ein `HttpOnly`-, `SameSite=Strict`-Cookie; danach greifen
Seite, Polling und erlaubte Proxys nur mit diesem Cookie zu. Für die Prüfung
liegt ein SHA-256-Hash vor. Der rohe Schlüssel muss zusätzlich in SQLite
gespeichert werden, weil die angemeldete Steuerung dieselbe stabile OBS-Adresse
nach einem Neustart erneut ausgeben können muss. Damit sind Datenbank und
Sicherungen bewusst Teil der Geheimnisgrenze. Wiederholtes Öffnen behält den
Schlüssel, eine ausdrückliche Rotation widerruft ihn sofort.
Die öffentliche Projektion folgt einer engeren Positivliste als der interne
Piece-Snapshot. Weder private Uploads noch alle Piece-Quellen werden implizit
veröffentlicht. Passieren dürfen nur öffentlicher Text, Präsentationstitel sowie
die ausdrücklich gewählte und erneut validierte Link- oder Videoquelle mit
erlaubten Vorschaufeldern.
## D-021: Vorlagen-Snapshots und idempotente Direktanlage
Persistente Piece-Vorlagen speichern validierte, tief kopierte JSON-Snapshots
mit eigener Revision. Damit bleiben sie unabhängig von späteren Änderungen am
Ausgangs-Piece, ohne für jedes optionale Piece-Feld eine zweite Tabellenstruktur
zu benötigen. Die virtuelle leere Vorlage wird nicht persistiert.
Eine Direktanlage im Sendungsplan ist ein einzelner fachlicher Befehl. Piece,
Verwendung und Sendungsrevision werden in derselben SQLite-Transaktion
geschrieben. `planning_commands` macht die Befehls-ID eindeutig und speichert
das Ergebnis, damit Wiederholungen nach Verbindungsabbrüchen keine Dubletten
erzeugen. Interner und öffentlicher Titel werden getrennt gespeichert und beim
Einplanen getrennt in den Verwendungssnapshot kopiert.