Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec596d442e | ||
|
|
bd9ffe6faa | ||
|
|
3389ba17f4 | ||
|
|
c05cea3a6d |
No files matched your search
@@ -3,7 +3,7 @@
|
||||
"title": "Sendewerk",
|
||||
"description": "Redaktionelle Planung und Durchführung von heinzlive-Sendungen.",
|
||||
"tagline": "Vom Fundstück bis zur Livesendung",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.12",
|
||||
"healthCheckPath": "/healthz",
|
||||
"httpPort": 3000,
|
||||
"addons": {
|
||||
|
||||
@@ -412,3 +412,44 @@
|
||||
- `bash -n start.sh`: erfolgreich
|
||||
- reale OBS-Browserquelle, echte YouTube-/TikTok-Wiedergabe und Safari auf einem
|
||||
iOS-Gerät nicht ausgeführt; Anbieterrequests wurden lokal abgefangen
|
||||
|
||||
## Verwaltungskorrektur 0.5.11
|
||||
|
||||
| Kriterium | Nachweis | Stand |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------- |
|
||||
| Werkzeugdialog ist initial geschlossen | Chromium prüft den Dialog vor jeder Bedienaktion bei 390 und 1280 Pixeln | erfüllt am 23.10.2026 |
|
||||
| Dialog öffnet nur nach bewusster Aktion | Browser legt ein Werkzeug an und öffnet ein bestehendes Werkzeug anschließend zum Ändern | erfüllt am 23.10.2026 |
|
||||
| Bestehender Werkzeugablauf bleibt erhalten | Browser prüft Icon, Format, Deaktivierung und Piece-Zuordnung nach erneutem Laden | erfüllt am 23.10.2026 |
|
||||
| Themen-Iconpicker bleibt im Viewport | Chromium prüft Öffnen und Auswahl bei 390, 960 und 1280 Pixeln | erfüllt am 23.10.2026 |
|
||||
|
||||
## Kompakte Live-Regie 0.5.12
|
||||
|
||||
| Kriterium | Nachweis | Stand |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------- |
|
||||
| Globale Navigation bleibt aus der Regie | Chromium findet weder Seitenleiste noch mobilen App-Header oder Bottomnavigation | erfüllt am 24.10.2026 |
|
||||
| Kleines Desktopfenster bleibt zweispaltig | Chromium prüft Moderation, Ablauf und feste Aktionen bei 900 × 450 Pixeln | erfüllt am 24.10.2026 |
|
||||
| Niedrige Desktophöhe wird berücksichtigt | 900 × 450 aktiviert kompakte Abstände ohne Transform oder CSS-Zoom | erfüllt am 24.10.2026 |
|
||||
| Lange Inhalte scrollen innerhalb der Fläche | Synthetischer Langtext und zusätzliche Ablaufabschnitte erzeugen zwei unabhängige Scroller | erfüllt am 24.10.2026 |
|
||||
| Mobile Regie verwendet ein Ablauf-Sheet | 390 × 700 prüft Endlage, Fokus, Escape, Aktionsschließen und Wechsel auf 900 Pixel | erfüllt am 24.10.2026 |
|
||||
| Leseposition bleibt am Piece | Nach tiefem Scrollen beginnt ein direkt gewähltes anderes Piece am oberen Rand | erfüllt am 24.10.2026 |
|
||||
| Zustände sind ausgeschrieben | Ablauf prüft Kommend, Aktuell, Erledigt und Übersprungen | erfüllt am 24.10.2026 |
|
||||
| Erstes und letztes Piece direkt wählbar | Browser wechselt per stabiler Usage-ID auf Desktop und aus dem mobilen Sheet in beide Richtungen | erfüllt am 24.10.2026 |
|
||||
| Navigation veröffentlicht nicht automatisch | Öffentliche Ausgabe bleibt bei direkter Auswahl unverändert | erfüllt am 24.10.2026 |
|
||||
| Timer und Livezustand überleben Reload | Aktuelles Piece und laufender Abschnittscountdown bleiben nach Neuladen erhalten | erfüllt am 24.10.2026 |
|
||||
| Wiederaufnahme nur bei Unterbrechung | Während eines aktiven Pieces fehlt die Aktion; nach unterbrochenem Stand ist sie prominent | erfüllt am 24.10.2026 |
|
||||
| Sendungsende ist eindeutig | Letztes Piece zeigt Endaktion ohne horizontalen Überlauf; danach fehlen Weiter-Aktionen | erfüllt am 24.10.2026 |
|
||||
|
||||
## Ausgeführte Prüfungen 0.5.12
|
||||
|
||||
- `npm run lint`: erfolgreich
|
||||
- `npm run check`: 0 Fehler und 0 Warnungen
|
||||
- `npm test`: 40 Tests in 7 Dateien erfolgreich
|
||||
- gezielter Live-Regie-Lauf: 1 von 1 Chromium-Test erfolgreich
|
||||
- `npm run test:e2e`: 16 von 16 Chromium-Browsertests erfolgreich
|
||||
- Screenshots der Live-Regie bei 1440 × 900, 900 × 450 und 390 × 700 Pixeln
|
||||
einschließlich mobilem Ablauf-Sheet visuell kontrolliert
|
||||
- `npm run build`: erfolgreich mit `@sveltejs/adapter-node`
|
||||
- Versionsgleichheit `0.5.12` in Paket, Lockdatei und Cloudron-Manifest geprüft
|
||||
- `bash -n start.sh` und `git diff --check`: erfolgreich
|
||||
- reale OBS-Browserquelle, echte YouTube-/TikTok-Wiedergabe und Safari auf einem
|
||||
iOS-Gerät nicht ausgeführt; Anbieterrequests wurden lokal abgefangen
|
||||
@@ -283,6 +283,19 @@ Der Live-Modus ist eine eigene, fokussierte Arbeitsansicht. Die
|
||||
Präsentationsausgabe verwendet eine separate, strikt reduzierte Datenansicht
|
||||
ohne interne Anwendungsnavigation.
|
||||
|
||||
Die private Route `/live` verwendet ebenfalls einen eigenen authentifizierten
|
||||
Layoutmodus ohne allgemeine Seitenleiste, mobilen App-Header oder
|
||||
Bottomnavigation. Ihre Regiefläche belegt die verfügbare Viewporthöhe als Raster
|
||||
aus fester Statuszeile, unabhängig scrollender Piece- und Ablaufspalte sowie
|
||||
fester Aktionszeile. Die Breite entscheidet über zwei Spalten oder das mobile
|
||||
Sheet; eine geringe Desktophöhe verdichtet zusätzlich Abstände und Leisten,
|
||||
ohne Schrift oder Oberfläche zu skalieren. Unterhalb des schmalen Breakpoints
|
||||
wird ausschließlich die Ablaufspalte als modales Sheet mit Fokusbegrenzung
|
||||
eingeblendet; beim Wechsel auf einen breiteren Viewport wird dieser Modalzustand
|
||||
aufgehoben. Dieser Layoutmodus ändert weder die Live-Actions noch die getrennte
|
||||
öffentliche `/heinzlive`-Projektion. Lokale Lesepositionen sind an die jeweilige
|
||||
Verwendung gebunden und werden nicht zwischen Pieces übertragen.
|
||||
|
||||
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
|
||||
|
||||
@@ -360,3 +360,19 @@ Validierung, während ein statischer SVG-Sprite über den gemeinsamen
|
||||
Pfaddaten in ihren JavaScript-Chunk. Es gibt weder einen externen Laufzeitabruf
|
||||
noch eine Laufzeitabhängigkeit vom benachbarten Quellverzeichnis; Herkunft und
|
||||
Lizenz stehen in `THIRD_PARTY_NOTICES.md`.
|
||||
|
||||
## D-024: Private Live-Regie als fokussierter Viewport
|
||||
|
||||
Die authentifizierte Live-Steuerung nutzt einen eigenen Root-Layoutmodus und
|
||||
rendert die allgemeine App-Navigation nicht. Kopf- und Aktionszeile bleiben fest;
|
||||
Moderation und Ablauf sind die einzigen unabhängig scrollenden Arbeitsflächen.
|
||||
Die Umschaltung zum mobilen Sheet hängt von der Fensterbreite ab, damit ein
|
||||
breites, niedriges Desktopfenster weiterhin zwei Spalten verwendet. Die Höhe
|
||||
steuert dort unabhängig die Dichte von Abständen und Bedienleisten. Textgröße
|
||||
und Gesamtoberfläche werden nicht skaliert.
|
||||
|
||||
Diese Darstellung ist keine neue Live-Zustandsgrenze. Start, direkte Auswahl,
|
||||
Erledigen, Überspringen, Reserveeinfügung und Präsentation bleiben unveränderte
|
||||
serverseitige Actions mit ihren bisherigen Revisions- und Idempotenzregeln. Ein
|
||||
Piece-Wechsel veröffentlicht daher weiterhin nichts; nur die ausdrückliche
|
||||
Präsentationsaction verändert die getrennte öffentliche Ausgabe.
|
||||
+135
-9
@@ -1,12 +1,126 @@
|
||||
# Release Notes
|
||||
|
||||
## 0.5.12 - Kompakte Live-Regie
|
||||
|
||||
Veröffentlicht am 24. Oktober 2026
|
||||
|
||||
Sendewerk 0.5.12 macht aus der privaten Live-Steuerung eine kompakte Regieansicht
|
||||
für Vollbild, Smartphone und kleine Desktopfenster neben OBS oder Firebot. Das
|
||||
aktuelle Piece, die Position im Ablauf, der Abschnittscountdown und die
|
||||
wichtigsten Aktionen bleiben gleichzeitig erreichbar. Die öffentliche
|
||||
heinzlive-Ausgabe und die gespeicherten Livezustände bleiben technisch getrennt.
|
||||
|
||||
### Highlights
|
||||
|
||||
- Die Live-Regie verwendet die gesamte Fensterfläche ohne normale
|
||||
App-Seitenleiste, mobilen App-Header oder Bottomnavigation.
|
||||
- Eine kompakte Kopfzeile zeigt aktuellen Abschnitt, Abschnittscountdown und den
|
||||
tatsächlich präsentierten Ausgabezustand.
|
||||
- Moderation und Ablauf bleiben auch bei einem nur 900 × 450 Pixel großen
|
||||
Desktopfenster nebeneinander. Lange Inhalte scrollen innerhalb ihrer
|
||||
Arbeitsfläche; Kopf- und Aktionszeile bleiben sichtbar.
|
||||
- Niedrige Desktopfenster verdichten Abstände und Bedienleisten anhand der
|
||||
verfügbaren Höhe, ohne Text oder die gesamte Oberfläche zu skalieren.
|
||||
- Die feste Fußleiste bündelt nächstes Piece, „Präsentieren“, „Überspringen“ und
|
||||
„Erledigen & weiter“. Beim letzten offenen Piece wird das Sendungsende
|
||||
eindeutig benannt.
|
||||
- Auf dem Smartphone öffnet „Ablauf · Piece X/Y“ den Ablauf als separates Sheet,
|
||||
während das aktuelle Piece die Hauptfläche bleibt. Fokusführung, Escape und
|
||||
der Wechsel zurück auf eine breite Ansicht schließen den Modalzustand sauber.
|
||||
|
||||
### Moderation und Materialien
|
||||
|
||||
- Interner Titel, Einleitung, Moderationsstichpunkte und Überleitung erscheinen
|
||||
in klarer Lesereihenfolge. Leere Bereiche werden weggelassen.
|
||||
- Recherche, Quellen, Werkzeuge und eigene Notizen sind kompakt beschriftet und
|
||||
werden nur bei Bedarf geöffnet.
|
||||
- Die Prompter-Schriftgröße bleibt einstellbar und wird weiterhin lokal
|
||||
gespeichert. Texte werden nicht automatisch verkleinert.
|
||||
|
||||
### Ablauf und Liveverhalten
|
||||
|
||||
- Abschnitte sind kompakt aufklappbar; der aktuelle Abschnitt ist geöffnet und
|
||||
das aktive Piece bleibt in der Ablaufspalte sichtbar.
|
||||
- Pieces kennzeichnen „Aktuell“, „Erledigt“, „Übersprungen“ und „Kommend“ auch
|
||||
textlich und lassen sich direkt auswählen, einschließlich des ersten Eintrags.
|
||||
- Die Reserve bleibt zusammengeklappt erreichbar. Dauerhafte Sortierpfeile und
|
||||
die separate große „Als Nächstes“-Karte entfallen; Umsortieren bleibt über
|
||||
eine kompakte Aktion am jeweiligen Eintrag erreichbar.
|
||||
- Die lokal gespeicherte Leseposition gehört zum jeweiligen Piece. Eine direkte
|
||||
Auswahl beginnt daher oben, ohne die Position des vorherigen Textes zu erben.
|
||||
- Direkte Auswahl verändert keinen Erledigungsstatus und veröffentlicht nichts.
|
||||
Nur „Präsentieren“ oder „Präsentation leeren“ verändert die öffentliche
|
||||
Ausgabe.
|
||||
- Abschnittstimer, tatsächliche Laufzeiten, Livefortschritt und Wiederaufnahme
|
||||
verwenden unverändert die vorhandenen persistenten Zustände.
|
||||
- „Live-Stand wiederaufnehmen“ erscheint nur bei einem tatsächlich
|
||||
unterbrochenen Ablauf, nicht während eines aktiven Pieces.
|
||||
|
||||
### Einstellungen
|
||||
|
||||
- Ausgabekanal verbinden, Schlüssel erneuern, Kanal trennen, Theme wechseln,
|
||||
Zeiten ein-/ausblenden und „Zur Planung“ bleiben im Regiemenü erreichbar.
|
||||
|
||||
### Update und Kompatibilität
|
||||
|
||||
- Dieses Release benötigt keine neue Datenbankmigration und keine Änderung der
|
||||
Konfiguration.
|
||||
- Bestehende Sendungen, Livezustände, Timer, Ausgabekanäle und
|
||||
Präsentationssnapshots bleiben erhalten.
|
||||
- Paket, Lockdatei und Cloudron-Manifest tragen gemeinsam Version `0.5.12`.
|
||||
|
||||
### Prüfung
|
||||
|
||||
- 40 Unit- und Verhaltenstests sowie 16 Chromium-E2E-Abläufe sind erfolgreich.
|
||||
- Die Live-Regie wurde bei 1440 × 900, 900 × 450 und 390 × 700 Pixeln mit
|
||||
langen Inhalten, unabhängigen Scrollflächen und mobilem Ablauf-Sheet geprüft;
|
||||
die erzeugten Screenshots wurden visuell kontrolliert.
|
||||
- Reales OBS, echte YouTube-/TikTok-Wiedergabe und Safari auf einem iOS-Gerät
|
||||
standen nicht zur Verfügung.
|
||||
|
||||
## 0.5.11 - Verwaltungskorrektur
|
||||
|
||||
Veröffentlicht am 23. Oktober 2026
|
||||
|
||||
- Der Dialog „Werkzeug hinzufügen“ bleibt beim Aufruf der Verwaltungsseite
|
||||
wieder zuverlässig geschlossen.
|
||||
- Der Dialog erscheint erst, wenn ein neues Werkzeug angelegt oder ein
|
||||
bestehendes Werkzeug bearbeitet wird.
|
||||
- Die Iconauswahl unter „Inhaltliche Zuordnung“ bleibt auf Desktop und
|
||||
Smartphone vollständig im sichtbaren Browserbereich.
|
||||
- Die Korrektur gilt gleichermaßen für Desktop- und Smartphone-Ansichten und
|
||||
verändert keine vorhandenen Werkzeuge oder Zuordnungen.
|
||||
|
||||
## 0.5.10 - Editor-Erlebnis
|
||||
|
||||
Veröffentlicht am 23. Oktober 2026
|
||||
|
||||
Sendewerk 0.5.10 beschleunigt die tägliche Redaktionsarbeit und schärft die
|
||||
Grenze zwischen privaten Medien und der öffentlichen Präsentation. Titel,
|
||||
Themen, Werkzeuge und Präsentationsbilder lassen sich direkter bearbeiten; die
|
||||
wichtigsten Listen und Dialoge benötigen auf Desktop und Smartphone weniger
|
||||
Platz. Bestehende Sendungsfassungen bleiben bei späteren Änderungen am Fundus
|
||||
stabil.
|
||||
|
||||
### Highlights
|
||||
|
||||
- Titel von Fundus- und Sendungsfassungen lassen sich direkt in der jeweiligen
|
||||
Ansicht ändern. Die Bedienung funktioniert auch vollständig per Tastatur.
|
||||
- Themen und Werkzeuge verwenden kompakte, durchsuchbare Auswahlflächen. Der
|
||||
Werkzeugkatalog folgt nun demselben Bedienmuster wie die Themenverwaltung.
|
||||
- Für Themen, Werkzeuge, Navigation und Aktionen stehen 3.671 lokal
|
||||
bereitgestellte Classic-Regular-Icons zur Verfügung. Zur Laufzeit werden
|
||||
keine Icons von externen Diensten geladen.
|
||||
- Präsentationsbilder werden bewusst aus den aktiven Piece-Medien gewählt und
|
||||
beim Einplanen in die Sendungsfassung übernommen. Private Uploads werden nicht
|
||||
allein durch das Hochladen öffentlich.
|
||||
- Pieces lassen sich wiederherstellbar löschen. Bereits vorbereitete
|
||||
Sendungsfassungen und ihre Medien-Snapshots bleiben dabei erhalten.
|
||||
- Die heinzlive-Ausgabe kann ausdrücklich geleert werden. Im leeren Zustand ist
|
||||
sie vollständig transparent und zeigt auch keinen Verbindungsstatus.
|
||||
|
||||
### Redaktion und Verwaltung
|
||||
|
||||
- Titel von Fundus- und Sendungsfassungen lassen sich direkt und per Tastatur
|
||||
bearbeiten.
|
||||
- Themen und Werkzeuge verwenden gemeinsame kompakte Auswahlflächen.
|
||||
- Der Werkzeugkatalog entspricht der Themenverwaltung: kompakte Glasfläche,
|
||||
Icon, Name, Status und Bearbeitungsaktion statt dauerhaft offener Formulare.
|
||||
- Anlegen und Bearbeiten eines Werkzeugs verwenden denselben responsiven Dialog
|
||||
@@ -14,9 +128,8 @@
|
||||
- Die Formatzuordnung nutzt dieselbe durchsuchbare Mehrfachauswahl wie andere
|
||||
Zuordnungen. Deaktivierte Werkzeuge und bestehende Piece-Zuordnungen bleiben
|
||||
erhalten.
|
||||
- Alle 3.671 Icons aus Font Awesome Pro Plus 7.0.0 Classic Regular stehen lokal
|
||||
und durchsuchbar zur Verfügung. Ein lokaler SVG-Sprite hält den allgemeinen
|
||||
Anwendungs-Chunk klein; es gibt keine externe Laufzeitabhängigkeit.
|
||||
- Fundus, Eingang, Archiv, Vorlagen und Sendungsansichten sind kompakter und
|
||||
bleiben per Klick, Touch und Tastatur bedienbar.
|
||||
|
||||
### Pieces und Präsentation
|
||||
|
||||
@@ -27,8 +140,10 @@
|
||||
- Pieces können wiederherstellbar gelöscht werden. Offene, nie live gelaufene
|
||||
Verwendungen und Abschnitte lassen sich unter geschützten Bedingungen
|
||||
entfernen.
|
||||
- Fundus, Eingang, Archiv, Vorlagen und Sendungsansichten sind kompakter und
|
||||
bleiben per Klick, Touch und Tastatur bedienbar.
|
||||
- Themenicons werden zusammen mit der jeweiligen Sendungsfassung gespeichert.
|
||||
Spätere Änderungen an Thema, Werkzeug oder Original-Piece verändern eine
|
||||
vorbereitete Sendung weiterhin nur nach ausdrücklicher Aktualisierung aus dem
|
||||
Fundus.
|
||||
|
||||
### Live-Ausgabe
|
||||
|
||||
@@ -37,13 +152,18 @@
|
||||
- Eine aktive Präsentation lässt sich unabhängig vom aktuell laufenden Piece
|
||||
leeren.
|
||||
|
||||
### Datenbank und Kompatibilität
|
||||
### Update und Kompatibilität
|
||||
|
||||
- `0010_editor_experience.sql` ergänzt Themenicons, Medien-Snapshots und Soft
|
||||
Delete.
|
||||
- `0011_tool_icons.sql` ergänzt persistente Werkzeugicons mit einem sicheren
|
||||
Standardwert. Werkzeug-IDs, Formatbeziehungen, Piece-Zuordnungen und
|
||||
Deaktivierungszustände bleiben erhalten.
|
||||
- Die Migrationen laufen beim nächsten Serverstart automatisch. Vor dem Update
|
||||
sollte wie üblich eine konsistente Sicherung der SQLite-Datenbank und des
|
||||
Upload-Verzeichnisses angelegt werden.
|
||||
- Für dieses Release sind keine manuellen Datenkonvertierungen und keine
|
||||
Änderungen an der Konfiguration erforderlich.
|
||||
|
||||
### Prüfung
|
||||
|
||||
@@ -53,3 +173,9 @@
|
||||
verglichen.
|
||||
- Reales OBS, echte YouTube-/TikTok-Wiedergabe und Safari auf einem iOS-Gerät
|
||||
standen nicht zur Verfügung.
|
||||
|
||||
### Noch nicht enthalten
|
||||
|
||||
Der Abschluss und die Archivierung vollständiger Sendungen, externe
|
||||
Lesefreigaben, RSS-Import und API gehören zu späteren Etappen und sind nicht Teil
|
||||
von 0.5.10.
|
||||
@@ -7,6 +7,47 @@ Anmeldung, Rollen, persistente Inhaltsbearbeitung, Sendungsplanung,
|
||||
Live-Steuerung, Präsentationsausgabe und lokaler Betriebsweg sind dokumentiert.
|
||||
Etappe 6 wurde nicht begonnen.
|
||||
|
||||
## Kompakte Live-Regie 0.5.12
|
||||
|
||||
- fokussierte private Liveansicht ohne normale App-Seitenleiste oder mobile
|
||||
Bottomnavigation; Planung, Theme, Schriftgröße, Zeitdarstellung und
|
||||
Ausgabekanal bleiben über das kompakte Regiemenü erreichbar
|
||||
- feste Kopfzeile mit aktuellem Abschnitt, persistentem Abschnittscountdown und
|
||||
öffentlichem Ausgabezustand sowie feste Fußleiste mit nächstem Piece,
|
||||
Präsentation, Überspringen und kontextgerechtem Erledigen
|
||||
- aktuelle Moderation und schmale Ablaufspalte bleiben auch bei 900 × 450 Pixeln
|
||||
nebeneinander; beide Arbeitsflächen scrollen unabhängig, ohne die ganze Seite
|
||||
oder einzelne Textblöcke zu verschachtelten Scrollcontainern zu machen
|
||||
- bei niedrigen Desktopfenstern werden Abstände und Bedienleisten anhand der
|
||||
verfügbaren Höhe verdichtet, ohne Text oder Gesamtoberfläche zu skalieren
|
||||
- auf schmalen Ansichten bleibt das Piece die Hauptfläche und der Ablauf öffnet
|
||||
als separates, safe-area-fähiges Sheet mit Fokusführung und Escape-Schließen
|
||||
- kompakter aufklappbarer Ablauf mit ausgeschriebenen Zuständen, direkter
|
||||
Auswahl jedes Pieces, automatisch sichtbarem aktuellen Eintrag und
|
||||
zusammengeklappter Reserve; Sortierung bleibt in einer kompakten Aktion
|
||||
erreichbar, dauerhafte Sortierpfeile entfallen
|
||||
- die Leseposition wird je Piece lokal erhalten; ein erstmals gewähltes Piece
|
||||
beginnt oben statt an der Position des vorherigen Textes
|
||||
- Recherche, Quellen, Werkzeuge und eigene Notizen erscheinen nur bei Bedarf;
|
||||
leere Moderationsbereiche werden nicht dargestellt
|
||||
- Livefortschritt, direkte Auswahl, Abschnittstimer und öffentliche Präsentation
|
||||
verwenden unverändert ihre getrennten persistenten Zustände
|
||||
- die Wiederaufnahmeaktion erscheint nur bei tatsächlich unterbrochenem
|
||||
Live-Stand statt während eines laufenden Pieces
|
||||
|
||||
Etappe 6 bleibt unverändert unbegonnen.
|
||||
|
||||
Die lokale Prüfmatrix umfasst 40 Verhaltenstests und 16 Chromium-Abläufe.
|
||||
Reales OBS, echte YouTube-/TikTok-Wiedergabe sowie Safari auf einem iOS-Gerät
|
||||
bleiben mangels verfügbarer Umgebung offen.
|
||||
|
||||
## Verwaltungskorrektur 0.5.11
|
||||
|
||||
- der Werkzeugdialog bleibt beim Laden geschlossen und öffnet erst nach einer
|
||||
bewussten Aktion
|
||||
- die Themen-Iconauswahl bleibt bei mobilen und breiten Ansichten vollständig
|
||||
innerhalb des Viewports
|
||||
|
||||
## Editor-Erlebnis 0.5.10
|
||||
|
||||
- direkte, tastaturbedienbare Titelbearbeitung in Fundus- und Sendungsfassungen
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sendewerk",
|
||||
"version": "0.5.10",
|
||||
"version": "0.5.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sendewerk",
|
||||
"version": "0.5.10",
|
||||
"version": "0.5.12",
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.2.2",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sendewerk",
|
||||
"private": true,
|
||||
"version": "0.5.10",
|
||||
"version": "0.5.12",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.17 <25"
|
||||
|
||||
+823
-402
File diff suppressed because it is too large.
Load diff
@@ -2,5 +2,6 @@ import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = ({ locals, url }) => ({
|
||||
user: url.pathname.startsWith('/heinzlive/') ? null : locals.user,
|
||||
presentationMode: url.pathname.startsWith('/heinzlive/')
|
||||
presentationMode: url.pathname.startsWith('/heinzlive/'),
|
||||
liveControllerMode: url.pathname === '/live'
|
||||
});
|
||||
@@ -34,6 +34,8 @@
|
||||
|
||||
{#if data.presentationMode}
|
||||
{@render children()}
|
||||
{:else if data.user && data.liveControllerMode}
|
||||
<main id="main-content" class="live-controller-shell">{@render children()}</main>
|
||||
{:else if data.user}
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
|
||||
+632
-351
File diff suppressed because it is too large.
Load diff
+226
-13
@@ -891,10 +891,33 @@ test('inbox, archive and administration stay concise', async ({ page }) => {
|
||||
]);
|
||||
expect(boxes[0]?.y).toBe(boxes[1]?.y);
|
||||
expect(boxes[1]?.y).toBe(boxes[2]?.y);
|
||||
|
||||
const topicIconPicker = page.locator('.topic-management-panel .taxonomy-form .icon-picker');
|
||||
for (const width of [1280, 960, 390]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await topicIconPicker.getByText('Icon auswählen', { exact: true }).click();
|
||||
const popover = topicIconPicker.locator('.icon-picker-popover');
|
||||
await expect(popover).toBeVisible();
|
||||
const popoverBox = await popover.boundingBox();
|
||||
expect(popoverBox?.x ?? -1).toBeGreaterThanOrEqual(0);
|
||||
expect((popoverBox?.x ?? width) + (popoverBox?.width ?? 1)).toBeLessThanOrEqual(width);
|
||||
await topicIconPicker.getByText('Icon auswählen', { exact: true }).click();
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await topicIconPicker.getByText('Icon auswählen', { exact: true }).click();
|
||||
await topicIconPicker.getByPlaceholder('Icon suchen').fill('Satellite Dish');
|
||||
await topicIconPicker.getByRole('button', { name: 'Satellite Dish', exact: true }).click();
|
||||
await expect(topicIconPicker.locator('input[type="hidden"]')).toHaveValue('satellite-dish');
|
||||
});
|
||||
|
||||
test('tools stay editable in pieces after catalog deactivation', async ({ page }, testInfo) => {
|
||||
await page.goto('/verwaltung');
|
||||
for (const width of [390, 1280]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await expect(page.locator('.tool-editor-dialog')).toBeHidden();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
}
|
||||
await page.getByRole('button', { name: 'Werkzeug hinzufügen' }).click();
|
||||
const create = page.getByRole('dialog', { name: 'Neues Werkzeug' });
|
||||
await create.getByLabel('Name').fill('E2E Tierlist');
|
||||
@@ -980,6 +1003,7 @@ test('validated YouTube and TikTok players switch without exposing private field
|
||||
})
|
||||
);
|
||||
await page.goto('/live?show=e2e-presentation');
|
||||
await page.getByText('Menü', { exact: true }).click();
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
||||
const output = await popupPromise;
|
||||
@@ -1053,10 +1077,17 @@ test('live control persists state and exposes only explicit presentation data',
|
||||
await plannedConsole.getByRole('button', { name: 'Aus Fundus aktualisieren' }).click();
|
||||
|
||||
await page.goto('/live?show=show-october');
|
||||
await expect(page.getByText('Noch nicht gestartet.')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Bereit für die Sendung' })).toBeVisible();
|
||||
await expect(page.locator('.sidebar')).toHaveCount(0);
|
||||
await expect(page.locator('.mobile-header')).toHaveCount(0);
|
||||
await expect(page.locator('.mobile-navigation')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: 'Sendung starten' }).click();
|
||||
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||
await expect(page.locator('.section-countdown')).toContainText(/Noch 0[45]:\d{2}/);
|
||||
await expect(page.locator('.section-countdown')).toContainText(/0[45]:\d{2}/);
|
||||
await expect(
|
||||
page.locator('.live-menu button', { hasText: 'Live-Stand wiederaufnehmen' })
|
||||
).toHaveCount(0);
|
||||
await page.getByText('Quellen · 1', { exact: true }).click();
|
||||
await expect(page.locator('.live-materials')).toContainText('Herstellerseite');
|
||||
await expect(
|
||||
page.getByText('Herstellerangaben gegen unabhängige Tests prüfen')
|
||||
@@ -1065,15 +1096,17 @@ test('live control persists state and exposes only explicit presentation data',
|
||||
const before = await page
|
||||
.locator('.live-current')
|
||||
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
||||
await page.getByText('Menü', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Schrift vergrößern' }).click();
|
||||
await page.reload();
|
||||
const restored = await page
|
||||
.locator('.live-current')
|
||||
.evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size'));
|
||||
expect(restored).not.toBe(before);
|
||||
await page.getByText('Manuelle Recherche öffnen').click();
|
||||
await page.locator('.research-details > summary').click();
|
||||
await expect(page.locator('.research-details')).toHaveAttribute('open', '');
|
||||
|
||||
await page.getByText('Menü', { exact: true }).click();
|
||||
const [popup] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click()
|
||||
@@ -1117,28 +1150,208 @@ test('live control persists state and exposes only explicit presentation data',
|
||||
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
|
||||
await page.getByRole('button', { name: 'Präsentieren', exact: true }).click();
|
||||
|
||||
await page.getByText('Reserve', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Als Nächstes' }).click();
|
||||
await expect(page.locator('.next-card')).toContainText('Pixelart');
|
||||
await page.getByRole('button', { name: 'Erledigen und weiter' }).click();
|
||||
await expect(page.locator('.live-next-summary')).toContainText('Pixelart');
|
||||
await page.getByRole('button', { name: 'Erledigen & weiter' }).click();
|
||||
await expect(page.locator('.live-current')).toContainText('Pixelart');
|
||||
await page.getByRole('button', { name: 'Überspringen' }).click();
|
||||
await page.getByRole('button', { name: 'Zurück' }).click();
|
||||
await expect(page.locator('.live-current')).toContainText('Pixelart');
|
||||
await expect(page.locator('#current-title')).toHaveText(
|
||||
'Warum praktische Effekte wieder auffallen'
|
||||
);
|
||||
await expect(
|
||||
page.locator('.live-run-item').filter({ hasText: 'Die kleine Konsole mit dem großen Dreh' })
|
||||
).toContainText('Erledigt');
|
||||
await expect(
|
||||
page.locator('.live-run-item').filter({ hasText: 'Pixelart, die nicht nostalgisch sein will' })
|
||||
).toContainText('Übersprungen');
|
||||
await expect(
|
||||
page.locator('.live-run-item').filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
|
||||
).toContainText('Aktuell');
|
||||
await page.locator('.live-current-scroll').evaluate((element) => {
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
const paragraph = document.createElement('p');
|
||||
paragraph.textContent = 'Temporärer Langtext für die Lesepositionsprüfung.';
|
||||
element.append(paragraph);
|
||||
}
|
||||
element.scrollTop = element.scrollHeight;
|
||||
element.dispatchEvent(new Event('scroll'));
|
||||
});
|
||||
expect(
|
||||
await page.locator('.live-current-scroll').evaluate((element) => element.scrollTop)
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece auswählen'
|
||||
})
|
||||
.click();
|
||||
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||
await expect
|
||||
.poll(() => page.locator('.live-current-scroll').evaluate((element) => element.scrollTop))
|
||||
.toBe(0);
|
||||
await expect(
|
||||
page.locator('.live-run-item').filter({ hasText: 'Warum praktische Effekte wieder auffallen' })
|
||||
).toContainText('Kommend');
|
||||
await expect(
|
||||
output.getByRole('heading', { name: 'Die kleine Konsole mit dem großen Dreh' })
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Warum praktische Effekte wieder auffallen als aktuelles Piece auswählen'
|
||||
})
|
||||
.click();
|
||||
await expect(page.locator('#current-title')).toHaveText(
|
||||
'Warum praktische Effekte wieder auffallen'
|
||||
);
|
||||
await expect(output.getByText('Wenn das Licht wirklich auf ein Modell fällt.')).toHaveCount(0);
|
||||
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await setTheme(page, 'dark');
|
||||
await page.reload();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await page.screenshot({ path: testInfo.outputPath('live-controller-desktop-dark.png') });
|
||||
|
||||
await page.setViewportSize({ width: 900, height: 450 });
|
||||
await setTheme(page, 'light');
|
||||
await page.reload();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||
await page.evaluate(() => {
|
||||
const title = document.querySelector('#current-title');
|
||||
if (title)
|
||||
title.textContent =
|
||||
'Warum praktische Effekte in einer außergewöhnlich langen Live-Moderation wieder auffallen';
|
||||
const copy = document.querySelector('.prompter-copy');
|
||||
if (copy) {
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
const paragraph = document.createElement('p');
|
||||
paragraph.textContent =
|
||||
'Zusätzlicher langer Moderationstext prüft die unabhängige Scrollfläche ohne die feste Bedienleiste zu verdrängen.';
|
||||
copy.append(paragraph);
|
||||
}
|
||||
}
|
||||
const rail = document.querySelector('.live-run-scroll');
|
||||
const section = rail?.querySelector('.rail-section');
|
||||
if (rail && section) {
|
||||
for (let index = 0; index < 8; index += 1) rail.append(section.cloneNode(true));
|
||||
}
|
||||
});
|
||||
const compactLayout = await page.evaluate(() => {
|
||||
const current = document.querySelector('.live-current')?.getBoundingClientRect();
|
||||
const rail = document.querySelector('.live-rail')?.getBoundingClientRect();
|
||||
const footer = document.querySelector('.live-action-bar')?.getBoundingClientRect();
|
||||
const pageStyle = getComputedStyle(document.querySelector('.live-page') as HTMLElement);
|
||||
const pieceScroll = document.querySelector('.live-current-scroll');
|
||||
const railScroll = document.querySelector('.live-run-scroll');
|
||||
return {
|
||||
pageFits: document.documentElement.scrollHeight <= innerHeight,
|
||||
twoColumns: Boolean(current && rail && rail.x > current.x + current.width - 1),
|
||||
footerVisible: Boolean(footer && footer.bottom <= innerHeight && footer.top >= 0),
|
||||
shortViewportDensity: parseFloat(pageStyle.gap) < 8,
|
||||
unscaled: pageStyle.transform === 'none',
|
||||
unzoomed: ['', '1', 'normal'].includes(pageStyle.getPropertyValue('zoom')),
|
||||
pieceScrolls: Boolean(pieceScroll && pieceScroll.scrollHeight > pieceScroll.clientHeight),
|
||||
railScrolls: Boolean(railScroll && railScroll.scrollHeight > railScroll.clientHeight)
|
||||
};
|
||||
});
|
||||
expect(compactLayout).toEqual({
|
||||
pageFits: true,
|
||||
twoColumns: true,
|
||||
footerVisible: true,
|
||||
shortViewportDensity: true,
|
||||
unscaled: true,
|
||||
unzoomed: true,
|
||||
pieceScrolls: true,
|
||||
railScrolls: true
|
||||
});
|
||||
await page.screenshot({ path: testInfo.outputPath('live-controller-compact-desktop.png') });
|
||||
|
||||
const countdownBeforeReload = await page.locator('.section-countdown strong').textContent();
|
||||
await page.reload();
|
||||
await expect(page.locator('#current-title')).toHaveText(
|
||||
'Warum praktische Effekte wieder auffallen'
|
||||
);
|
||||
const countdownAfterReload = await page.locator('.section-countdown strong').textContent();
|
||||
expect(countdownAfterReload).not.toBe('05:00');
|
||||
expect(countdownAfterReload).toMatch(/0[34]:\d{2}/);
|
||||
expect(countdownBeforeReload).toMatch(/0[34]:\d{2}/);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 700 });
|
||||
const liveRail = page.locator('.live-rail');
|
||||
const railClose = liveRail.getByRole('button', { name: 'Ablauf schließen' });
|
||||
await expect(liveRail).toBeHidden();
|
||||
await expect(page.locator('.live-action-bar')).toBeVisible();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
expect(
|
||||
await page.evaluate(() => document.documentElement.scrollHeight <= window.innerHeight)
|
||||
).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath('live-controller-mobile.png') });
|
||||
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
|
||||
await expect(liveRail).toHaveClass(/open/);
|
||||
await expect(railClose).toBeFocused();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const box = await liveRail.boundingBox();
|
||||
return box ? Math.ceil(box.x + box.width) : Number.POSITIVE_INFINITY;
|
||||
})
|
||||
.toBeLessThanOrEqual(390);
|
||||
const mobileRailBox = await liveRail.boundingBox();
|
||||
expect(mobileRailBox?.width ?? 0).toBeGreaterThan(340);
|
||||
await expect(page.locator('.live-run-item.current')).toBeInViewport();
|
||||
await page.screenshot({ path: testInfo.outputPath('live-controller-mobile-run.png') });
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(liveRail).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: /Ablauf · Piece/ })).toBeFocused();
|
||||
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
|
||||
await page.setViewportSize({ width: 900, height: 450 });
|
||||
await expect(liveRail).not.toHaveAttribute('role', 'dialog');
|
||||
await expect(page.locator('.live-command-header')).not.toHaveAttribute('inert', '');
|
||||
await page.setViewportSize({ width: 390, height: 700 });
|
||||
await expect(liveRail).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
|
||||
await liveRail
|
||||
.getByRole('button', {
|
||||
name: 'Die kleine Konsole mit dem großen Dreh als aktuelles Piece auswählen'
|
||||
})
|
||||
.click();
|
||||
await expect(liveRail).toBeHidden();
|
||||
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||
await page.getByRole('button', { name: /Ablauf · Piece/ }).click();
|
||||
await liveRail
|
||||
.getByRole('button', {
|
||||
name: 'Warum praktische Effekte wieder auffallen als aktuelles Piece auswählen'
|
||||
})
|
||||
.click();
|
||||
await expect(liveRail).toBeHidden();
|
||||
await expect(page.locator('#current-title')).toHaveText(
|
||||
'Warum praktische Effekte wieder auffallen'
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Erledigen' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Live-Stand wiederaufnehmen' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Live-Stand wiederaufnehmen' }).click();
|
||||
await expect(page.locator('#current-title')).toHaveText('Die kleine Konsole mit dem großen Dreh');
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await page.getByRole('button', { name: 'Erledigen & Sendungsende' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Alle Pieces bearbeitet' })).toBeVisible();
|
||||
await expect(page.locator('.live-finished-label')).toHaveText('Ablauf abgeschlossen');
|
||||
await expect(page.getByRole('button', { name: 'Erledigen & weiter' })).toHaveCount(0);
|
||||
|
||||
await output.route('**/state', (route) => route.abort());
|
||||
await expect(output.getByText('Verbindung unterbrochen')).toBeVisible({ timeout: 5_000 });
|
||||
await output.unroute('**/state');
|
||||
await expect(output.getByText('Verbunden')).toBeVisible({ timeout: 5_000 });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('live-controller-mobile.png'),
|
||||
fullPage: true
|
||||
});
|
||||
await page.getByRole('button', { name: 'Präsentation leeren' }).click();
|
||||
await expect(output.locator('.output-content')).toHaveCount(0);
|
||||
await expect(output.locator('.heinzlive-output')).toHaveClass(/empty/);
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.open = () => null;
|
||||
});
|
||||
await page.getByText('Menü', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ausgabe verbinden und öffnen' }).click();
|
||||
await expect(
|
||||
page
|
||||
|
||||
Reference in new issue
Block a user