41 KiB
Architektur
Systemgrenze
Sendewerk ist ein einzelner SvelteKit-Prozess. Browserseiten, serverseitige Actions, 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
+-> Mail/RSS über begrenzte 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.
Navigation, Suche und Arbeitsflächen
+layout.svelte stellt für angemeldete Nutzer die globalen Bereiche Start,
Eingang, Pieces, Streams, Live und Post bereit. Desktop verwendet eine horizontale
Kopfzeile, Mobilgeräte eine obere Such-/Kontoleiste und eine Safe-Area-fähige
Bottom-Navigation. UserMenu.svelte bündelt Verwaltung, Abmeldung und die weiter
in localStorage gespeicherte explizite Auswahl Hell, Dunkel oder System.
Die Root-Route komponiert das Dashboard ausschließlich aus listShows(),
getShow(), listPieces() und den vorhandenen Verwendungskontexten. Sie besitzt
keinen eigenen persistenten Zustand. Pieces und Streams verwenden getrennte,
lokal gespeicherte Ein-/Ausklappzustände ihrer Kontextnavigation; URL-Parameter
bleiben die fachliche Quelle für lokale Suche und Filter.
global-search.ts ist eine serverseitige, ausschließlich hinter requireUser
verwendete Projektion. Sie durchsucht aktive und archivierte Pieces einschließlich
redaktioneller Langtexte sowie Metadaten und Usage-Snapshots aller Streams. Die
Route /suche gibt gruppierte Positivprojektionen mit ID, Titel, Status, internem
Ziel, Feldbezeichnung und begrenztem Ausschnitt aus. Gelöschte Pieces werden nicht
global angeboten. Es gibt keinen externen Index und keine neue Persistenz.
Die normale Piece-Arbeitsmenge schließt Originale mit materialisierter
Verwendungshistorie aus. Dieselben Originale werden unter view=completed mit
ihren verlinkten Streams angezeigt und bleiben über die bestehenden
Planungsfilter erneut einplanbar. Abgeschlossene Streams werden nicht mehr als
eigener globaler Archivbereich behandelt, sondern von listShows() in der
Streamübersicht gefiltert. Der technische /archiv-Pfad bleibt für archivierte
und gelöschte Pieces bestehen.
StreamInspector.svelte ist die dauerhafte rechte Projektion des Streamplans.
Seine flüchtige Auswahl referenziert Show, Abschnitt oder Usage nur über stabile
IDs. Der Standardzustand bearbeitet Show- und Twitch-Metadaten; Abschnitt und
Usage verwenden ihre bestehenden revisionsgeschützten Actions. Die zusätzliche
Action updateUsageMetadata liest die aktuelle Usage serverseitig und führt
nicht dargestellte Langtexte, Präsentationswerte, Medien und Werkzeuge in
updatePieceUsage() unverändert mit. Ein schmales Clientformular kann private
Felder daher weder versehentlich leeren noch die Geschäftsvalidierung umgehen.
Seit 0.8.1 liest /suche die optionalen URL-Filter type, status, topic
und format serverseitig. Themen und Formate stammen direkt aus den bestehenden
Taxonomietabellen; Piece-Filter wirken nicht auf Streams. Die Trefferprojektion
und ihre begrenzten Ausschnitte bleiben unverändert, es entsteht weder ein
Suchindex noch persistenter Filterzustand.
Die Piece-, Eingangs-, Post-, Planungs- und Live-Arbeitsflächen verwenden eine
gemeinsame Oberflächensprache. Pieces, Eingang und Post teilen die Klasse unified-workspace:
links Navigation oder Quellen, mittig die auswählbare Inhaltsprojektion und
rechts content-quick-inspector für häufige Felder und Aktionen. Die Auswahl ist
flüchtiger URL- beziehungsweise Browserzustand und erzeugt kein Fachobjekt. Im Streamplan ist der Ablauf die Hauptfläche, der Inspector die
dauerhafte rechte Metadatenspalte und die Piece-Auswahl eine bei Bedarf geöffnete
Nebenspalte. Eine Auswahl schließt diese temporäre Bibliothek. Unterhalb der
Desktopbreite sind Pieces, Ablauf und Details gleichwertige Pane-Zustände.
Revisionen, Drag-Daten und Fokusgrenzen bleiben die Interaktionsgrenze. Live ist
eine reguläre Seite der App-Shell und projiziert Ablauf, Moderationsdokument und
Details ausschließlich lesend. Die lokale Piece-Auswahl und Darstellungsoptionen
ändern weder den tatsächlichen Live-Stand noch Usage-Status, Reihenfolge, Timer
oder öffentliche Ausgabe.
Die gemeinsamen Klassen sind bewusst keine bloße Rasterabstraktion. Pieces und
Post verwenden für linke Kataloge dieselbe fundus-navigation-Basis wie Streams
und für rechte Projektionen stream-inspector zusammen mit builder-pane.
Dadurch bleiben Kopf, Formularrhythmus, Scrollgrenze und Aktionsfuß identisch.
Post ergänzt nur fachliche Felder und eine kompakte Inhaltswerkzeugzeile.
Laufzeit und Speicher
- Lokal:
.data/sendewerk.sqliteund.data/uploads, jeweils konfigurierbar. - Tests: pro Lauf ein temporäres Verzeichnis unter dem Betriebssystem-Tempdir.
- Cloudron:
/app/data/sendewerk.sqliteund/app/data/uploads. - Migrationen liegen nummeriert unter
migrations/, laufen in Transaktionen und werden inschema_migrationsprotokolliert. - 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 sieben Etappen materialisieren Identität, Inhaltsverwaltung, Sendungsplanung, Live-Durchführung, Historie, Lesefreigaben, RSS-Import und eine technische API. 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 sowie getrennte technische Zugänge |
| Taxonomie | getrennte section_categories, formats, topics samt optionalem Themenicon; Pieces und Vorschläge mehrfach verknüpft |
| Inhalt | pieces mit Status, Soft Delete, Favorit, Revision, Texten, Quellen, Werkzeugen und privaten Medien; Vorlagen speichern Snapshots |
| Sendungsbau | shows, geordnete show_sections, piece_usages samt Quellen-/Werkzeug-/Themen-/Mediensnapshots, Reservebank und Vorlagen |
| Live | expliziter Live-Stand, aktuelles Element, tatsächliche Piece-Zeiten, persistente Abschnitts-/Gesamttimer 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 |
| Post | Aufzeichnungsteile, private Markdown-Quellen, revisionierte YouTube-/Clip-/Highlight-Elemente, Thumbnails und valide Exporte |
| 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.
Ein Live-Abschnittstimer entsteht ausschließlich beim tatsächlichen Eintritt in einen Abschnitt mit positiver expliziter Dauer. Er speichert dieses Soll als unveränderlichen Wert und summiert eigene Laufintervalle; Piece-Wechsel im selben Abschnitt erzeugen deshalb weder Reset noch doppelte Piece-Laufzeit. Der Gesamttimer speichert ebenfalls mehrere Laufintervalle. Eine gemeinsame Pause schließt die offenen Piece-, Abschnitts- und Gesamtintervalle; Fortsetzen öffnet neue Intervalle, ohne die Pause als Laufzeit zu zählen. Abschnitts- und Gesamtreset ersetzen ausschließlich die jeweilige Zeitbasis.
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.
Technische Zugänge sind keine Browsersitzungen. Sie besitzen zufällige
Bearer-Tokens, von denen nur SHA-256-Hashes gespeichert werden, und genau einen
der Umfänge read oder read_write. Widerruf wirkt beim nächsten Request.
/api/v1 umgeht lediglich den Browser-Loginredirect; jeder Endpunkt prüft Token
und erforderlichen Umfang selbst. Schreibzugriffe hinterlassen ein
audit_event; Neuanlage und Planungsänderungen verlangen zusätzlich einen je
Zugang eindeutigen, persistent gespeicherten Idempotenzschlüssel.
RSS-Import
src/lib/server/feeds.ts bildet Abruf, XML-Verarbeitung und Übernahme gemeinsam
für manuelle Aktionen und den Prozess-Scheduler ab. Der Scheduler startet beim
ersten Request und prüft minütlich höchstens zehn fällige aktive Feeds. Jeder
Feed wird getrennt behandelt, sodass ein Fehler weitere Abrufe nicht stoppt.
HTTP-Abrufe erlauben nur HTTP(S) auf Standardports, prüfen DNS sowie jedes
Redirectziel gegen interne Adressen und begrenzen Laufzeit, Redirects und
Antwortgröße. RSS/Atom wird mit deaktivierter Entity-Verarbeitung gelesen;
höchstens 200 Einträge je Antwort werden berücksichtigt und fremdes HTML wird zu
reinem Text reduziert.
feed_entries ist ein eigener Eingang. (feed_id, external_id) verhindert
Dubletten; Aktualisierungen ändern den redaktionellen Status nicht. discarded
bleibt deshalb auch nach späteren Abrufen erhalten. Erst die bewusste
Mehrfachauswahl erzeugt innerhalb einer Transaktion normale Inbox-Pieces mit
Kategorien und Original-URL als Quelle und setzt den Eintrag auf imported.
Technische REST-API
Die Routen unter /api/v1 bilden keine zweite Fachlogik, sondern rufen
content.ts und shows.ts auf. Piece- und Sendungsrevisionen schützen weiterhin
vor verlorenen Änderungen. api_idempotency speichert Request-Hash, Status und
JSON-Antwort, sodass wiederholte Schreibbefehle dasselbe Ergebnis liefern und
ein gleicher Schlüssel mit verändertem Inhalt abgewiesen wird. Die OpenAPI-3.1-
Beschreibung ist unter /api/v1/openapi.json erreichbar. Ein MCP-Adapter oder
produktspezifischer KI-Client gehört nicht zur Laufzeit und ist nicht
Voraussetzung für die Bedienung.
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.
is_favorite ist eine globale Eigenschaft des Originals und keine Nutzung oder
Historienmarkierung. Der Werkzeugkatalog liegt getrennt von Formaten; Formate
sind nur Eignungszuordnungen. piece_tools hält pro Piece den privaten
Arbeitslink und eine Notiz, während Deaktivierung das Katalogobjekt und alle
bestehenden Zuordnungen erhält. Ein serverseitig validiertes icon_name gehört
zum Werkzeugkatalog und wird im Originaleditor angezeigt, ohne bestehende
Usage-Snapshots nachträglich zu verändern.
Ein Piece kann für die Präsentation Text oder genau eine vorhandene Quelle als
Linkkarte beziehungsweise Video auswählen. Abgerufene Metadaten und Kennzeichen
für manuell korrigierte Felder liegen am Piece; ein erneuter Abruf überschreibt
diese Felder nicht.
Piece-Vorlagen liegen als validierte JSON-Snapshots in piece_templates. Jeder
Speichervorgang kopiert den vollständigen Vorlageninhalt tief und erhöht bei
einer Aktualisierung die Revision; spätere Änderungen am Ausgangs-Piece wirken
nicht auf die Vorlage zurück. Die leere Vorlage bleibt virtuell. Interner Titel
und optionaler Präsentationstitel sind getrennte Felder; ohne öffentlichen Titel
verwendet die Präsentationsprojektion den internen Titel als Fallback.
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.
Ein Piece kann genau eines seiner aktiven Medien ausdrücklich als
Präsentationsbild auswählen. Die Auswahl liegt in piece_presentation_media;
das Entfernen setzt am Medium einen Zeitstempel statt die Datei zu löschen.
Damit bleiben bereits erzeugte usage_media_snapshots auflösbar, während neue
Einplanungen und der Originaleditor nur aktive Medien berücksichtigen. Das
Soft Delete eines Pieces setzt ebenfalls nur deleted_at_utc: normale Listen
und neue Einplanungen schließen es aus, bestehende Verwendungsreferenzen und
Snapshots bleiben unverändert erhalten.
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,
Interaktion, Überleitung und der vollständige Präsentationszustand aus dem sendefertigen
Piece kopiert. Dazu gehören Modus, ausgewählte Quelladresse samt Zeitmarke,
Metadaten und validierte Anbieter-ID. Zusätzlich speichern eigene geordnete
Tabellen alle Quellen, Werkzeugnamen samt Start-/Arbeitslink und Notiz, die
Themennamen samt Icon sowie das ausdrücklich ausgewählte Präsentationsmedium.
Eigene Notizen liegen ausschließlich an
der Verwendung. Die Referenz zum Original bleibt für Historie und
Nachvollziehbarkeit erhalten, wird aber nicht live gespiegelt. Eine explizite
Aktualisierung aus dem Fundus ersetzt Titel, Dauer, alle fünf Redaktionstexte
und den Präsentationszustand atomar aus dem referenzierten Original. Eigene
Notizen, Status und vorhandene Live-Intervalle werden dabei nicht verändert;
fehlt das Original oder ist die Sendungsrevision veraltet, wird die gesamte
Mutation verworfen.
Eine Einplanung zu duplizieren kopiert genau diesen Snapshot mit derselben
Original-ID direkt hinter das Ausgangselement. Die neue Verwendung erhält den
Status open, eine neue ID und damit weder Live-Referenzen noch Zeitintervalle.
Die Verwendungshistorie wird nicht separat gepflegt. Sie wird über die
Original-ID aus Verwendungen mit Status done in abgeschlossenen Sendungen
abgeleitet; Reservebank und alle anderen Verwendungs- oder Sendungsstatus sind
ausgeschlossen. Über dieselbe Original-ID wird unabhängig davon ermittelt, ob
mindestens eine Verwendung in einer noch nicht abgeschlossenen Sendung liegt.
Der allgemeine Fundus blendet diesen globalen Kontext standardmäßig aus. Die
Quellspalte einer Sendung betrachtet für ihren separaten Einplanungsfilter nur
die aktuelle Sendung einschließlich Reservebank.
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 Sendungsübersicht leitet Liste, Desktop-Monatsraster und mobile Monatsagenda
aus derselben listShows()-Abfrage ab. UTC-Zeitpunkte werden erst für Gruppierung
und Anzeige nach Europe/Berlin in zivile Kalendertage übersetzt. Sendungen ohne
Startzeit bleiben in einem eigenen Bereich sichtbar. users.show_overview_view
speichert ausschließlich die kontobezogene Darstellungspräferenz; der gewählte
Monat bleibt URL-Zustand und verändert keine Sendungsdaten.
Die Direktanlage aus einer Sendung erzeugt Piece und Verwendung innerhalb einer
verschachtelbaren SQLite-Transaktion. planning_commands speichert die
Befehls-ID und das Ergebnis, sodass ein wiederholter Request dieselben IDs
zurückgibt und weder ein zweites Piece noch eine zweite Einplanung erzeugt.
Die Editorformulare bleiben progressive SvelteKit-Form-Actions, werden im
Browser aber mit use:enhance gesendet. Der Verwendungseditor liegt in einem
nativen Dialog, den ein beschrifteter Icon-Button öffnet. 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.
Die Ausschlussfilter für gesendete und bereits eingeplante Pieces arbeiten clientseitig auf der serverseitig berechneten Kontextmenge. Ihr Zustand wird ohne Navigation in die URL übernommen und bei nachfolgenden Suchfiltern mitgesendet. Planmutationen invalidieren anschließend die Seitendaten, sodass das Entfernen der letzten Einplanung ein Original unmittelbar wieder anbietet.
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.
live_show_timer_runs hält die unabhängige Gesamtzeit. Pause und Fortsetzen
behandeln Gesamt-, Abschnitts- und Piece-Intervalle gemeinsam. Beenden schließt
laufende Intervalle und setzt ended_at_utc, finalisiert die Sendung aber noch
nicht. Eine ausdrückliche Wiederaufnahme bewahrt Zeiten und Fortschritt; nur
„Neuer Durchlauf“ verwirft Laufintervalle und setzt die Piece-Status zurück. Die
Restzeitschätzung verwendet je Abschnitt entweder die positive Abschnittsdauer
oder die verbleibenden Piece-Dauern und zählt beide nie zusammen.
Die interne Route /live benötigt weiterhin eine aktive Sitzung und darf
Recherche, Moderation und eigene Arbeitsdaten lesen. Die separate Ausgabe nutzt
den stabilen Kanal /heinzlive/default in einem reduzierten Layout ohne
Navigation oder Nutzerdaten. Der geheime 256-Bit-Schlüssel steht nur im
URL-Fragment und wird per POST gegen ein HttpOnly-, SameSite=Strict-Cookie
getauscht. SQLite speichert seinen SHA-256-Hash für die Zugangsprüfung sowie den
rohen Schlüssel, weil die angemeldete Steuerung dieselbe stabile OBS-Adresse
erneut ausgeben können muss. Datenbank und Sicherungen sind deshalb Teil der
Geheimnisgrenze. Eine Rotation widerruft den bisherigen Schlüssel, normales
erneutes Öffnen behält ihn bei.
Das private Markdownfeld „Interaktion“ wird am Original und als
Verwendungssnapshot gespeichert und ausschließlich in der authentifizierten
Live-Regie gerendert. Page, Polling-JSON und Proxyrouten leiten ihre Daten aus einer eigenen
Positivliste ab. Private Quellen-/Werkzeug-Snapshots, Arbeitslinks, Notizen und
Uploads werden nicht veröffentlicht; nur Themennamen sowie die ausdrücklich
gewählte Link- oder Videoquelle und deren erlaubte Vorschau-URL können die
Grenze passieren.
Lokale Betrachtungsnavigation, Fortschaltung oder Wiederaufnahme verändern den
Präsentationsstand nicht. Zurück und die Auswahl einer Ablaufzeile wechseln nur
den im Browser betrachteten Verwendungssnapshot; tatsächliches Live-Piece,
Status, Timer und Ausgabe bleiben serverseitig unverändert. Die öffentliche
Projektion liest ausschließlich den ausdrücklich präsentierten
Verwendungssnapshot; spätere Änderungen am Original erscheinen erst nach einer
ausdrücklichen Snapshot-Aktualisierung.
Linkmetadaten werden nur auf ausdrückliche Aktion durch
src/lib/server/presentation.ts geladen. Jeder HTTP(S)-Abruf löst den Zielhost
serverseitig auf, sperrt lokale und private IPv4-/IPv6-Ziele und bindet den
Request an die geprüfte Adresse. Dieselbe Prüfung gilt nach höchstens drei
Redirects. Zeit- und Größenlimits begrenzen HTML, Anbieter-Metadaten und Bilder.
Externe Vorschaubilder gelangen nur über token- beziehungsweise
sitzungsgebundene Proxyrouten in die Ausgabe; fremdes HTML und fremde Skripte
werden nie übernommen.
Videoquellen werden aus der gespeicherten URL erneut auf eine streng validierte
YouTube- oder TikTok-ID reduziert. Der Browser erhält nur daraus erzeugte
offizielle Embed-URLs, startet sie nicht automatisch und ersetzt das Iframe bei
jedem Inhaltswechsel. YouTube erhält die Startsekunde als Embed-Parameter;
TikTok wird nach dessen Bereitschaft über die offizielle Player-postMessage-
Schnittstelle positioniert. Nicht unterstützte oder unvollständige Videoquellen
fallen auf eine normale Linkkarte zurück. Eine authentifizierte Testansicht
verwendet denselben Renderer, schreibt aber keinen Live-Zustand.
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.
Abschluss, Historie und Lesefreigaben
src/lib/server/completion.ts ist die Transaktionsgrenze des endgültigen
Sendungsabschlusses. Er verlangt einen beendeten Live-Lauf sowie passende Show-
und Live-Revisionen. Eine Befehls-ID in show_finalizations macht Wiederholungen
idempotent. In derselben Transaktion werden offene Intervalle geschlossen,
Abschnittskategorien eingefroren, ausschließlich erledigte Verwendungen außerhalb
der Reservebank in piece_usage_history materialisiert, bewusst gewählte
Fundus-Originale archiviert und die Sendung auf completed gesetzt. Jede normale
Planungs- und Live-Mutation sperrt diesen Zustand.
Die eigentlichen Sendungstexte, Quellen, Werkzeuge, Themen und Medien liegen weiterhin in den beim Einplanen erzeugten Usage-Snapshots. Finalisierung friert die zuvor noch katalogabhängige Abschnittskategorie ein. Abgeschlossene Sendungen werden ausschließlich gelesen oder vollständig in einen neuen Entwurf dupliziert. Das Duplikat erhält neue Show-, Abschnitts-, Usage- und Relations-IDs, offene Usage-Status und weder Live-, Historien- noch Freigabedaten.
src/lib/server/shares.ts erzeugt für Barkeeper zufällige 256-Bit-Lesetokens;
SQLite speichert nur SHA-256-Hashes. Eine Freigabe verweist direkt auf eine
Sendung und darf deshalb bereits im Entwurf, im bereiten oder im laufenden
Zustand entstehen. Der Umfang ist eine Positivliste aus rundown, moderation
oder research. Optionale Passwörter liegen als Argon2id-Hash vor. Nach
erfolgreicher Prüfung entsteht eine höchstens zwölf Stunden gültige, ebenfalls
nur gehashte Gast-Sitzung; Fehlversuche werden pro Link und Clientadresse
begrenzt. Änderungen an Umfang, Passwort oder Ablauf invalidieren bestehende
Gast-Sitzungen. Ablauf und Löschung werden bei jedem HTML-, Seitendaten- und
Medienrequest erneut geprüft.
Die öffentliche Route /freigabe/[token] erhält keine interne Sendungs-ID und
keinen ungefilterten Show-Datensatz. Sie projiziert genau die am Token gebundene
Sendung und nur die Felder ihres Umfangs. Vor dem Abschluss werden aktuelle
Sendungs- und Usage-Snapshots bei jedem Aufruf neu gelesen; danach liefert
dieselbe Projektion die durch die Finalisierung stabilisierten Snapshots. Die
separate Medienroute akzeptiert ausschließlich das ausdrücklich gewählte
Präsentationsmedium einer eingeplanten Nicht-Reserve-Verwendung derselben
Sendung. Private /media-Dateien bleiben weiterhin an eine Redaktionssitzung
gebunden.
Die wiederverwendbare Barkeeper-Komponente ShareManagement.svelte liegt direkt
in der Sendungsbearbeitung und in der Verwaltung. Beide Routen verwenden
dieselben Servicefunktionen für Anlage, Bearbeitung und Löschung; die
Sendungsroute bindet Mutationen zusätzlich an ihre eigene Sendungs-ID. Die
Verwaltung gruppiert bestehende Freigaben nach Sendung und zeigt deren aktuellen
Status und Termin.
Postproduktion
src/lib/server/post.ts ist die gemeinsame Geschäftsgrenze für den Hauptbereich
/post. Jede Übersicht beginnt bei shows.status = 'completed'; Post besitzt
keine eigene Sendungsidentität und kann weder Entwürfe noch laufende Streams
nachträglich als abgeschlossen markieren. post_items hält gemeinsame Identität,
Typ, Workflowstatus, Herkunft, Position und Revision. Typbezogene 1:1-Tabellen
speichern YouTube-, Social-Clip- oder Twitch-Highlight-Felder.
Migration 0022_post_quick_workbench.sql ergänzt social_caption an
post_social_clips und führt vorhandene TikTok-/Instagram-Captions und Hashtags
verlustarm in das gemeinsame Feld über. YouTube Shorts verwendet im aktuellen
Formvertrag ausschließlich shorts_title; TikTok und Instagram teilen die
Caption inklusive Hashtags. Die älteren Spalten bleiben für die additive
Migration bestehen, werden von der neuen Oberfläche aber nicht mehr getrennt
bearbeitet.
post_recording_parts beschreibt mehrere Teile einer Aufzeichnung. Medienpfade
sind redaktionelle Verweise und müssen für Resolve-Export absolut oder als
file:// angegeben sein; Framerate und Timebase werden separat validiert.
post_source_files speichert Originalinhalt, privaten zufälligen Speichernamen,
SHA-256, Größe sowie ausschließlich aus tatsächlich gefundenen Zeilen abgeleitete
Transkript- und Zeitmarkenprojektionen. (show_id, sha256) verhindert Dubletten
innerhalb einer Sendung, ohne Dateien sendungsübergreifend zusammenzuführen.
Kontextdateien und post_thumbnails liegen im konfigurierten Uploadverzeichnis.
Die Routen unter /post/[id]/source und /post/[id]/thumbnail verlangen eine
Redaktionssitzung, prüfen die Zuordnung zur abgeschlossenen Sendung und liefern
private Sicherheitsheader. Originalnamen sind Metadaten und niemals Pfade. Pro
YouTube-Element kann ein Thumbnail ausdrücklich ausgewählt werden.
Exporte unter /post/[id]/export lesen dieselbe positiv validierte Projektion.
Resolve-XML benötigt Aufzeichnungspfad, Framerate, Timebase und einen positiven
Start-/Endbereich. SRT wird cueweise vollständig geprüft; unvollständige Daten
führen zu EXPORT_NOT_READY statt zu einer scheinbar gültigen Datei. Text- und
JSON-Exporte erzeugen keine externe Veröffentlichung.
src/lib/server/post-ai.ts enthält nur Typen für einen später konfigurierbaren
Provideradapter. Es gibt weder Netzwerkaufruf noch automatisch erzeugten Inhalt.
Importierte Quellen und manuelle Bearbeitung bleiben die vollständige
Produktfunktion.
Oberflächenstruktur
Die Hauptnavigation umfasst Start, Eingang, Pieces, Streams, Live und Post;
Verwaltung bleibt im Benutzermenü.
Auf dem Desktop liegt sie als feste horizontale Glasleiste oberhalb der
Arbeitsfläche. Bis einschließlich 52rem ersetzen eine kompakte obere Leiste
und die untere Hauptnavigation diesen Desktopkopf 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.
Die Sendungsübersicht schaltet über zwei gleichwertige Formularaktionen zwischen
Liste und Kalender um. Auf breiten Ansichten erscheint ein siebenspaltiges
Monatsraster ohne Termin-Drag-and-drop; bis 52rem ersetzt es eine lineare,
touchfreundliche Agenda desselben Monats.
Die private Route /live läuft in derselben authentifizierten App-Shell wie
Streams. Ein Streamkopf zeigt Titel, Termin, Umfang und den expliziten
Nur-Lesen-Zustand. Darunter folgen auf breiten Ansichten Ablauf, Lesedokument und
Details als drei builder-pane-/stream-inspector-artige Flächen. Bis
einschließlich 72rem werden diese Bereiche über eine gleichwertige Umschaltung
einzeln gezeigt; bis 52rem bleiben der kompakte App-Header und die mobile
Bottomnavigation sichtbar.
src/routes/live/+page.server.ts bietet neben load ausschließlich die getrennte
Präsentationsaktion an. Das Auswählen einer Usage, Vor/Zurück, Schriftgröße,
sichtbare Zeiten und das Kopieren von Twitch-Tags bleiben lokaler
Browserzustand. Status-, Fortschritts-, Sortier- und Abschlussänderungen sind
nicht Teil dieser Route. Präsentieren ändert nur presentation_usage_id und
damit weder Show-/Usage-Status noch Reihenfolge. Die öffentliche /heinzlive-
Projektion und die serverseitigen historischen Live-Dienste bleiben erhalten.
Das gemeinsame Arbeitsraster besteht aus einer linken, über Kopf und Inhalt
reichenden Navigation sowie einem rechten Hauptbereich. Dessen freie Kopfzeile
besitzt bewusst keine Pane-Fläche; darunter beginnen Inhalt und optionaler
Inspector. unified-workspace, Post-Detail und Live verwenden dieselben
Spaltenwerte 13–16rem / minmax(0, 1fr) / 19–22rem und 0.85rem Abstand. Bei
mittleren Breiten folgt der Inspector unter dem Inhalt, mobil übernimmt die
bereichsspezifische Pane-Umschaltung.
Dialog- und Pane-Bewegungen sind reine Oberflächenzustände mit 140–170 ms Dauer.
Der Piece-Dialog animiert Öffnen, explizites Schließen und Backdrop; Disclosure-
Inhalte und mobile Pane-Wechsel blenden kurz ein. Unter
prefers-reduced-motion: reduce werden Animationen und Übergänge entfernt.
Icons werden aus dem lokalen Font-Awesome-Classic-Regular-Sprite unter
static/icons gerendert. Die Verwaltung lädt zusätzlich die durchsuchbaren
Metadaten aller 3.671 Icons, zeigt je Suchergebnis aber höchstens 120 Einträge
gleichzeitig. Allgemeine Seiten wie Login oder Navigation laden den Katalog
nicht in ihren Client-Chunk.
Der Fundus ist auf breiten Ansichten ein Raster aus Bibliotheksnavigation,
mittlerer Liste und rechter Schnellbearbeitung. Kleinere Desktopfenster ordnen
den Inspector unter der Liste an; mobil werden Navigation, Liste und Inspector
gestapelt. URL-Parameter tragen Suche, Filter und ausgewählte Piece-ID über den
Routenwechsel, sessionStorage ausschließlich die zugehörige flüchtige
Listenposition. Reduzierte Schnellformulare lesen nicht dargestellte Werte
serverseitig aus dem aktuellen Piece und führen sie unverändert mit.
Der eigenständige Piece-Editor ordnet denselben Formvertrag als modales
redaktionelles Arbeitsblatt mit eigener URL an. Titel, Einleitung, Moderationsstichpunkte,
Interaktion, Überleitung, private Recherche, Quellen und Werkzeuge bilden die
mittlere Schreibfläche. Status, Dauer, Einordnung, Präsentationsmodi und Medien
liegen auf breiten Ansichten rechts. Alle Werte werden weiterhin gemeinsam über
die bestehende save-Action und Piece-Revision gespeichert; separate Bild- und
Präsentationsactions bleiben eigene serverseitige Mutationen.
Der Eingang verwendet dieselbe Darstellung, behält aber zwei Datenarten: neue
feed_entries und bereits erzeugte Inbox-Pieces. Die rechte Projektion ruft für
RSS weiterhin ausschließlich Import-/Verwerfactions auf. Post beginnt ebenfalls
in einer dreiteiligen Übersicht; die vollständige Streamzeile und die rechte
Öffnen-Aktion verlinken direkt auf /post/[id]. Die Route ist eine normale
Anwendungsseite und übernimmt Seitenkopf, Datumsdarstellung, Feldgeometrie und
responsive Pane-Umschaltung der Streambearbeitung. Darunter stehen
Schnellnavigation, Editor/Übersicht und Inspector. Die linke Navigation
gruppiert YouTube, Social-Clips und Twitch-Highlights und ruft deren Sofortanlage
über dieselbe Post-Action auf. Ohne item-Queryparameter bleibt die Auswahl
leer: Die Mitte projiziert alle vorhandenen Post-Inhalte mit Kopieraktionen,
rechts stehen Streamzähler. Mit item zeigt dieselbe Mitte den Editor und rechts
dessen Eigenschaften. Mutations-URLs führen item mit, damit Upload oder
Speichern die aktuelle Auswahl nicht verlieren.
createPostItem() setzt source_file_id bereits beim Anlegen, wenn genau eine
Kontextdatei zum Stream existiert. listPostPieceOptions(showId) verbindet
pieces, piece_usages und show_sections; dadurch sind nur Original-Pieces
des geöffneten Streams zuordenbar. Editor und Inspector nutzen dieselben Pane-,
Feld- und Aktionsgrundlagen wie der Streamplan. Mobil werden Navigation,
Bearbeitung und Details als drei explizite Pane-Zustände angeboten.
Checkboxen und Radios bleiben im DOM native Formularsteuerungen. Das globale
Stylesheet blendet nur ihre sichtbaren Kästchen aus und projiziert :checked und
:focus-visible auf das umgebende Label. Damit sind ganze Flächen per Maus,
Touch und Tastatur wählbar, ohne Semantik oder Formübermittlung zu ersetzen. Der
aktive Zustand verwendet wie Post-Navigation und Post-Register den cyanfarbenen
Oberflächenakzent; Gold bleibt Primäraktionen vorbehalten.
RichMarkdownField erstellt je sichtbarem Langtext genau eine Tiptap-Instanz mit
StarterKit und offizieller Markdown-Erweiterung. Der Formularwert bleibt rohes
Markdown. Eine Bearbeitung aktualisiert ihn aus getMarkdown(), während das
bloße Initialisieren keinen Input auslöst. Weicht die erste Serialisierung vom
gespeicherten Text ab, startet das Feld in der Markdown-Quellansicht. Der Nutzer
kann diese Ansicht jederzeit erreichen; Autosave erzeugt die Editorinstanz nicht
neu.
Sicherheitsgrenzen
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. Netzwerkabrufe für Präsentationsmetadaten und Vorschaubilder schützen interne Ziele auch nach DNS-Auflösung und Redirects und besitzen Größen-, Zeit- und Redirectlimits.
Wartungsfelder 0.6.1
Migration 0017 ergänzt den optionalen Vorbereitungsaufwand am Original und
kommagetrennt validierte Twitch-Tags am Abschnitt. Der Bildmodus wird wegen der
bestehenden SQLite-Check-Constraints als zusätzliches Kennzeichen an Original
und Usage-Snapshot gespeichert; nach außen bleibt er der eindeutige Modus
image. Öffentlich passiert weiterhin nur das ausdrücklich gewählte
Präsentationsmedium.
Wartungskorrekturen 0.7.1
Migration 0019 ergänzt preparation_status_snapshot und
preparation_effort_snapshot an piece_usages und übernimmt für vorhandene
Einplanungen einmalig die damaligen Originalwerte. Danach ist ausschließlich die
konkrete Sendungsfassung maßgeblich. Einplanen, Duplizieren und bewusstes
Aktualisieren aus dem Fundus kopieren beide Werte; der Usage-Editor ändert sie
unabhängig. Die Übersicht berücksichtigt vorbereitende Nicht-Reserve-Usages
aktiver Sendungen, priorisiert zuerst deren Termin und führt direkt zum Editor.
Ein Freigabe-Ersatzlink rotiert ausschließlich den gehashten Token derselben Freigabe. Umfang, Passwort-Hash und Ablauf bleiben erhalten, der alte Token und alle bisherigen Gastsitzungen werden in derselben Transaktion ungültig. Der neue Klartexttoken wird weiterhin nur in der Action-Antwort ausgegeben.
Bedienkorrekturen 0.7.2
Der RSS-Auswahldialog erhält weiterhin ausschließlich new-Einträge aus
listFeedEntries. Quellenfilter, Seiteneinteilung und noch nicht bestätigte
Auswahl sind flüchtiger Browserzustand. Erst das Formular im erreichbaren
Dialogfuß übergibt die gewählten IDs an die unveränderte transaktionale
Übernahme; dadurch bleiben persistenter Dublettenschutz, Herkunft und
Standardkategorien die einzige fachliche Wahrheit.
Feed- und Zugangsdialoge verwenden dieselben globalen Management-Dialog- und Katalogstile wie Werkzeuge. Das Bearbeiten eines technischen Zugangs ändert nur Name und Umfang des bestehenden gehashten Tokens und wird auditiert. Die Live-Route projiziert zusätzlich nur, ob der feste Ausgabekanal getrennt, mit dieser oder mit einer anderen Sendung verbunden ist. Das Öffnen von Menü und Ausgabedetails führt selbst keine Mutation aus.
Lesender MCP-Zugang 0.7.3
/mcp ist ein stateless Streamable-HTTP-Adapter über content.ts und
shows.ts. Er unterstützt die aktuelle per Request deklarierte MCP-Fassung
2026-07-28 einschließlich server/discover, Header-/Body-Abgleich und
resultType. Für derzeitige OpenAI-Clients bleiben die Initialisierungsfassungen
2025-11-25, 2025-06-18 und 2025-03-26 verfügbar. Es gibt keine
MCP-Sitzung und keinen unabhängigen SSE-Strom; jede Anfrage trägt erneut ihre
Authentifizierung.
Die fünf Werkzeuge sind ausschließlich lesend. Eigene Positivprojektionen lassen Mediennamen, interne Speicherpfade und Werkzeug-Arbeitsdaten aus. Die Detailprojektionen dürfen nach ausdrücklicher Zustimmung redaktionell private Recherche, Moderation, eigene Notizen, Quellen und Reserveinhalte enthalten. Keine MCP-Methode ruft Mutationsdienste auf.
Sendewerk ist zugleich OAuth-Autorisierungs- und Ressourcenserver. Geschützte
Ressourcen- und Authorization-Server-Metadaten verweisen ausschließlich auf die
konfigurierte Origin. Öffentliche PKCE-Clients werden per begrenztem DCR
registriert; die Autorisierung verlangt exakte Redirect-URI, S256, den Scope
sendewerk:read und den exakten Ressourcenindikator <Origin>/mcp. Codes sind
fünf Minuten, Zugriffstokens eine Stunde gültig. SQLite speichert nur SHA-256-
Hashes. Tokens sind direkt an Nutzer, Client, Scope und Ressource gebunden;
Kontosperre und Passwortwechsel widerrufen sie, und jeder MCP-Aufruf verbindet
erneut den aktuellen Nutzerzustand.
DCR ist für die von OpenAI weiterhin unterstützte Kompatibilität vorhanden, aber pro Quelladresse und insgesamt begrenzt. Clientnamen sind nicht verifiziert; der Zustimmungsdialog kennzeichnet dies und zeigt die genaue Rücksprung-Domain. Öffentliche Produktion verlangt HTTPS, während HTTP nur an Loopback-Adressen zulässig bleibt. Der bestehende technische REST-Token wird weder gelesen noch weitergereicht.