diff --git a/docs/ui-flow-view-sollmodell.md b/docs/ui-flow-view-sollmodell.md new file mode 100644 index 0000000..70ae75b --- /dev/null +++ b/docs/ui-flow-view-sollmodell.md @@ -0,0 +1,1029 @@ +# UI-Sollmodell – ferrite.fm Signal Flow View + +Status: Entwurf / Arbeitsgrundlage +Zweck: Neuer zusätzlicher UI-View neben der bestehenden Control-Plane-Oberfläche +Arbeitsname: `Flow View` + +--- + +## 1. Zielbild + +Der neue View soll **nicht** die bestehende UI sofort ersetzen, sondern eine zweite, domänengerechtere Hauptansicht für professionelle Broadcast- und Signalpfad-Arbeit liefern. + +Die Grundidee: +- Statt primär Formulare, Panels und technische Gruppierungen zu zeigen, visualisiert der View den **realen Signalfluss** der Applikation. +- Das UI wird damit näher an Broadcast-, Routing-, Audio- und DSP-Systemen. +- Der Nutzer soll auf einen Blick sehen: + - **wo** sich das Signal gerade befindet, + - **welche Stufe** gesund / degradiert / fehlerhaft / inaktiv ist, + - **welches Modul** welchen Einfluss auf das On-Air-Signal hat, + - **wo** eine Störung tatsächlich sitzt, + - und **wo** man für Eingriffe klicken muss. + +Kurzform: +**Weniger Settings-App, mehr Signal- und Sendeweg-Konsole.** + +--- + +## 2. Produktprinzipien + +### 2.1 Operator-first +Der View muss innerhalb von 1–2 Sekunden lesbar sein. +Keine dekorative Technikgrafik, kein animiertes Spielzeug, kein "Dashboard um des Dashboards willen". + +### 2.2 Systemmodell statt Menüstruktur +Die Oberfläche bildet nicht zuerst Menüpunkte ab, sondern die tatsächliche Kette: +**Input → Buffer/Ingest → Audio → Processing → Stereo/RDS/MPX → TX** + +### 2.3 Status ist primär, Settings sekundär +Die erste Funktion des Views ist Verstehen und Überwachen. +Die zweite Funktion ist Eingreifen. +Darum gilt: +- **Farbe und Form** zeigen Zustand. +- **Hover** erklärt. +- **Click** öffnet Steuerung. + +### 2.4 Bestehende UI bleibt erhalten +Die aktuelle tab-basierte Oberfläche bleibt vorerst als: +- Detailansicht +- Fallback +- Vollkonfigurationsraum +- Diagnose-/Engineering-Oberfläche + +Der neue Flow View ist also ein **zusätzlicher View**, kein sofortiger kompletter Ersatz. + +--- + +## 3. Hauptnutzen des Views + +Der View soll drei Dinge gleichzeitig leisten: + +1. **Observability** + Wo steht das System? Welche Stufe ist gesund? Was ist degradet? Wo liegt der Fehler? + +2. **Navigation** + Klick auf ein Modul führt direkt zur passenden Steuerung, statt Tabs/Panels suchen zu müssen. + +3. **Control** + Häufige und domänennah passende Eingriffe können direkt am jeweiligen Modul stattfinden. + +--- + +## 4. Platz im Produkt + +## Top-Level-Position +Empfohlene Tabs / Hauptviews: +- `Flow` +- `Control` +- `Diagnostics` +- `Activity` + +Dabei gilt: +- **Flow** = Hauptansicht für Operatoren und für systemisches Verständnis +- **Control** = Form-/Panel-basierte Bearbeitung und Vollkonfiguration +- **Diagnostics** = tiefe Runtime-/Health-/Audit-Ansicht +- **Activity** = Logs / Historie + +Der neue View soll also idealerweise als erster oder linker Haupttab erscheinen. + +--- + +## 5. Grundlayout des Flow Views + +## 5.1 Grundstruktur +Der View besteht aus vier Zonen: + +### A. Top Status Bar +Persistente Betriebsleiste mit: +- Markenname `ferrite.fm` +- TX-Status (`ON AIR`, `OFF AIR`, `DEGRADED`, `FAULT`) +- Frequency (Applied / Target) +- Connection / Control-Plane-Status +- Last issue / active alarm +- Emergency Actions (`Stop TX`, `Reset Fault`) + +### B. Horizontale Signalfluss-Zeile +Die zentrale Fläche des Views. +Hier liegen die Module als horizontale Pipeline mit Verbindungsstrecken dazwischen. + +### C. Detail / Popover-Ebene +Nicht dauerhaft offen. Öffnet sich: +- per Hover als kompakte Statusblase +- per Klick als interaktiver Popover / Inspector + +### D. Optionaler Bottom Strip / Runtime Summary +Schmale sekundäre Leiste für: +- aktive Warnings +- Queue Health +- Ingest state +- last change +- runtime age + +--- + +## 6. Die Module im Signalfluss + +Der Flow View soll echte, betrieblich relevante Knoten zeigen. +Keine dekorativen Zwischenboxen. + +Empfohlene Basiskette: + +1. **Source** +2. **Ingest / Buffer** +3. **Audio** +4. **Processing** +5. **Stereo** +6. **RDS** +7. **Composite / MPX** +8. **TX / RF** + +### 6.1 Source +**Bedeutung:** Ursprungsquelle des Programmsignals. + +**Mögliche Zustände:** +- no source / idle +- connected +- reconnecting +- missing +- blocked + +**Beispiele für Daten:** +- `ingest.kind` +- active origin / URL / stream name +- stream title (falls relevant) +- transport / codec +- connected / disconnected + +**Popover-Steuerung:** +- Source kind +- Source URL / stream target +- Decoder mode +- grundlegende source-spezifische Optionen +- Link zur vollen Ingest-Konfiguration + +### 6.2 Ingest / Buffer +**Bedeutung:** Annahme, Vorpufferung, Runtime-Stabilisierung. + +**Mögliche Zustände:** +- healthy +- low buffer +- write blocked +- reconnecting +- stalled +- critical + +**Beispiele für Daten:** +- prebuffer state +- buffered seconds +- reconnect count +- source state +- runtime state +- last chunk time + +**Popover-Steuerung:** +- Prebuffer +- reconnect settings +- ggf. source-related runtime controls +- Link zu tiefer Diagnose + +### 6.3 Audio +**Bedeutung:** Programmaudio-Grundparameter vor Stereo-/MPX-Pfaden. + +**Mögliche Zustände:** +- active +- tone mode active +- muted / no input +- warning if gain/levels suspicious + +**Beispiele für Daten:** +- input gain +- tone left / right / amplitude +- source activity summary + +**Popover-Steuerung:** +- input gain +- tone controls +- ggf. source audio mode + +### 6.4 Processing +**Bedeutung:** Eingriffe in Klangbearbeitung und Compliance-nahe Audio-/Processing-Stufen. + +**Mögliche Zustände:** +- healthy +- limiter active +- compliance pending restart +- clipper active +- warning if inconsistent config/runtime impact exists + +**Beispiele für Daten:** +- limiter enabled / ceiling +- pre-emphasis +- BS.412 enabled +- composite clipper enabled / iterations / knee / lookahead + +**Popover-Steuerung:** +- limiter +- ceiling +- pre-emphasis +- compliance settings +- composite clipper settings + +### 6.5 Stereo +**Bedeutung:** Stereo-Multiplex-Aufbereitung. + +**Mögliche Zustände:** +- stereo on +- mono / stereo disabled +- pilot warning +- degraded if stereo mode or pilot abnormal + +**Beispiele für Daten:** +- stereo enabled +- stereo mode +- pilot level + +**Popover-Steuerung:** +- stereo on/off +- stereo mode +- pilot level + +### 6.6 RDS +**Bedeutung:** Metadaten- und RDS-Subsystem. + +**Mögliche Zustände:** +- active +- disabled +- active text relay +- degraded if metadata mismatch / relay issue / config inconsistency + +**Beispiele für Daten:** +- RDS enabled +- PI / PTY +- PS / RadioText +- TP / TA +- active on-air text +- RDS2 summary if enabled + +**Popover-Steuerung:** +- enable/disable +- PI / PTY +- PS / RT +- TP / TA +- RDS features shortcut + +### 6.7 Composite / MPX +**Bedeutung:** Endstufe des zusammengesetzten Multiplexsignals vor TX. + +**Mögliche Zustände:** +- healthy +- high injection +- compliance risk +- degraded if MPX gain / injection values suspicious + +**Beispiele für Daten:** +- pilot level +- RDS injection +- MPX gain +- clipper state +- BS.412 state + +**Popover-Steuerung:** +- pilot +- RDS injection +- MPX gain +- clipper toggle shortcut + +### 6.8 TX / RF +**Bedeutung:** Ausgang / SDR / Sendezustand. + +**Mögliche Zustände:** +- on air +- idle +- arming +n- degraded +- muted +- faulted +- stopping + +**Beispiele für Daten:** +- applied frequency +- desired frequency +- runtime state +- queue health +- underruns +- last fault +- backend / driver + +**Popover-Steuerung:** +- start / stop TX +- reset fault +- frequency +- quick runtime summary + +--- + +## 7. Statusmodell + +Farben und Zustände müssen über alle Module konsistent sein. + +### 7.1 Primäre Modulzustände +- **Green** → active / healthy / normal +- **Amber** → degraded / warning / pending / attention needed +- **Red** → fault / error / blocked / disconnected in fault-relevant stage +- **Gray** → disabled / bypassed / inactive / not configured +- **Blue outline** → selected / focused in UI + +### 7.2 Zustandslogik +Nicht jedes Modul nutzt dieselben Regeln, aber dieselbe Farbe bedeutet denselben operativen Charakter. + +Beispiele: +- Source disconnected = rot, wenn ingest erwartet wird +- RDS disabled = grau, nicht rot +- TX idle = grau oder neutral, nicht rot +- queue critical = rot +- restart required = amber indicator, aber nicht automatisch roter Modulstatus + +### 7.3 Zusätzliche Marker +Neben der Grundfarbe kann ein Modul kleine sekundäre Marker tragen: +- `pending` indicator +- `restart required` +- `reload required` +- `draft exists` + +Diese Marker sollen aber klein und sekundär bleiben. + +--- + +## 8. Hover-Verhalten + +Hover soll eine **kompakte Statusblase** öffnen. + +### Prinzipien +- Schnell erfassbar +- Kein Formular +- Kein Scrollmonster +- Max. 5–7 Zeilen +- Klare Typografie + +### Inhalt pro Hover-Blase +1. Modulname +2. Zustand in Klartext +3. 2–5 wichtigste Metriken / Werte +4. optional letzter Hinweis / letzter Fehler +5. evtl. Zeitbezug (`updated 4s ago`) + +### Beispiel RDS Hover +- RDS +- Status: Active +- PI: BEEF +- PS: FERRITE +- RT: Now playing ... +- TP/TA: off / off +- updated 2s ago + +### Beispiel TX Hover +- TX / RF +- State: Running +- Applied: 99.5 MHz +- Target: 99.5 MHz +- Queue: normal (67%) +- Underruns: 0 +- Runtime age: 18m + +--- + +## 9. Click-Verhalten / Popover + +Ein Klick auf ein Modul öffnet einen **Popover / Inspector**, nicht sofort ein Fullscreen-Modal. + +## 9.1 Ziel +- Schnell editierbare Kernparameter +- Kontext bleibt sichtbar +- Fokus bleibt auf dem Signalfluss + +## 9.2 Struktur des Popovers +- Modulname + Statuskopf +- kleine Summary +- relevante Einstellfelder +- Apply-/Action-Bereich +- Link / Button zu "Open full controls" + +## 9.3 Regeln +- Nur die wichtigsten Bedienelemente inline +- Tiefe Spezialkonfiguration weiter in der bestehenden Control-View +- Kritische Aktionen klar trennen + +### Beispiele + +#### TX Popover +- Start TX / Stop TX +- Reset Fault +- Frequency field +- Applied / target summary + +#### RDS Popover +- Enable RDS +- PI +- PTY +- PS +- RT +- TP/TA + +#### Processing Popover +- Limiter on/off +- Ceiling +- Pre-emphasis +- BS.412 on/off +- Composite clipper toggle + +--- + +## 10. Navigations- und Wechselmodell + +Der Flow View soll sich nicht isoliert anfühlen. +Jedes Modul kann zusätzlich einen Übergang in die bestehende UI bieten: +- `Open full controls` +- `Open diagnostics` +- `Open ingest panel` +- direkter Link zum **entsprechenden Tab bzw. Panel** der bestehenden Detail-UI + +So wird der Flow View zur Primärnavigation, ohne alles selbst lösen zu müssen. + +### Festlegung +Popover bleiben **Quick Control** und sollen bewusst flach bleiben. Für tiefere Arbeit gibt es immer einen direkten Sprung in die bestehende Detail-UI an die passende Stelle. + +--- + +## 11. Visual Language + +## 11.1 Stilprinzipien +- nüchtern +- präzise +- professionell +- ruhig +- hohe Informationsdichte ohne Chaos + +## 11.2 Look & Feel +- heller bis neutraler Hintergrund bleibt möglich +- weniger Card-Sammlung, mehr zusammenhängende technische Arbeitsfläche +- Module als klar definierte Knoten mit Verbindungslinien +- Verbindungslinien dezent, aber lesbar +- Symbole sachlich und funktional, nicht verspielt + +## 11.3 Symbolik +Jedes Modul erhält ein einfaches, wiedererkennbares Icon, z. B.: +- Source → Stecker / Input / Antenne / Link +- Buffer → Stack / Queue / Reservoir +- Audio → Waveform / Fader +- Processing → Filter / dynamics / knob cluster +- Stereo → linked channels / L-R icon +- RDS → text / metadata / broadcast tag +- Composite → multiplex / spectrum-like icon +- TX → antenna / RF output + +Wichtig: +Icons unterstützen, ersetzen aber nie die Lesbarkeit. + +--- + +## 12. Responsive Verhalten + +Der View ist primär Desktop-/Operator-first. +Mobile ist sekundär, aber nicht ignoriert. + +### Desktop +- horizontale Kette in einer Zeile +- Popover seitlich / unterhalb des aktiven Moduls + +### Tablet / kleinere Fenster +- Kette darf umbrechen in 2 Zeilen, aber logisch lesbar bleiben +- Verbindungslogik optisch erhalten + +### Mobile +- keine ambitionierte Vollsimulation der Horizontalkonsole +- stattdessen vereinfachte vertikale Step-Ansicht +- Hover-Konzept wird zu Tap-Details + +--- + +## 13. Technische Datenquellen + +Der View soll möglichst aus bereits vorhandenen Daten gespeist werden. + +### Bereits vorhanden / nutzbar +- `/status` +- `/runtime` +- `/config` +- bestehende Draft-/Apply-Logik in `ui.html` + +### Erwartete Zuordnung +- Source / Ingest / Buffer ← `runtime.ingest`, `config.ingest` +- Audio / Processing / Stereo / RDS / MPX ← `config`, `runtime.engine`, teilweise `status` +- TX / RF ← `runtime.engine`, `runtime.driver`, `status` + +### Wichtige Designregel +Der View darf zunächst **read-mostly + selectively writable** sein. +Nicht alles muss in Version 1 voll editierbar sein. + +--- + +## 14. MVP-Scope + +Für eine erste brauchbare Version reichen: + +### Muss enthalten +- neuer `Flow`-Tab +- horizontale Modulkette +- Statusfarben pro Modul +- Hover-Details pro Modul +- Click öffnet kompakten Popover +- pro Modul 1–3 relevante Kernaktionen +- klare Verbindung zur bestehenden Control-Ansicht + +### Darf zunächst fehlen +- hochkomplexe Inline-Editoren +- Drag & Drop +- Mini-Sparklines in jedem Modul +- Animationen entlang des Flows +- komplette Mobile-Perfektion + +--- + +## 15. Anti-Ziele + +Der Flow View soll **nicht** werden: +- eine dekorative Netzwerkgraph-Spielerei +- eine überanimierte "wow"-Fläche +- ein unlesbares Technikposter +- ein Vollersatz für jede Detailkonfiguration in Version 1 +- eine Oberfläche, bei der Farbe das einzige Statussignal ist + +--- + +## 16. Nächste Umsetzungsstufe + +### Stufe 1 — UX-Skelett +- neuen Tab `Flow` anlegen +- statische Modulkette rendern +- Icons + Titel + Statusfarben +- ausgewählte Runtime-/Config-Werte anbinden + +### Stufe 2 — Interaktion +- Hover-Bubbles +- Click-Popover +- Quick actions für TX / RDS / Frequency / Source + +### Stufe 3 — Integration +- Draft-/Apply-Mechanik für Kernmodule +- Übergänge zu bestehender Detail-UI +- sauberer Pending-/Restart-/Reload-Status + +### Stufe 4 — Broadcast-Polish +- strengere visuelle Hierarchie +- Alarm-Layer +- optional Operator-/Engineering-Abstufung + +--- + +## 17. Getroffene Designentscheidungen + +### 17.1 Stereo und RDS +**Entscheidung:** `Stereo` und `RDS` werden als **getrennte Knoten** dargestellt. + +**Begründung:** +- fachlich klarer +- RDS bleibt als eigenes Broadcast-Subsystem sichtbar +- Fehlerursachen werden präziser lesbar +- passt besser zu einer professionellen Broadcast-/Signalpfad-Sicht + +### 17.2 Composite / MPX +**Entscheidung:** `Composite / MPX` wird als **eigener Knoten** dargestellt. + +**Begründung:** +- Broadcast-technisch zentrale Stufe +- trennt Audio-/Processing-Welt sauber von der Multiplex-/Deviation-Ebene +- macht Pilot, RDS-Injection, MPX Gain und Composite-Verhalten sichtbar an der richtigen Stelle + +### 17.3 Popover-Tiefe +**Entscheidung:** Popover bleiben **Quick Control**. + +**Regel:** +- wenige Kernaktionen / Kernfelder direkt im Popover +- keine Vollkonfiguration im Flow View +- tiefe Bearbeitung erfolgt über direkten Link in den passenden Tab bzw. das passende Panel der bestehenden Detail-UI + +### 17.4 Globaler Alarm-Banner +**Entscheidung:** Ein globaler Alarm-Banner oberhalb des Flows ist sinnvoll, aber **nur kontextuell sichtbar**. + +**Regel:** +- sichtbar bei aktiven Faults / relevanten Degraded-Zuständen / betrieblich relevanten Warnings +- nicht als permanentes Alarmband +- kompakt, klar, operator-first + +### 17.5 Stil +**Entscheidung:** Der View wird **technisch-klar mit leichter visueller Abstraktion** umgesetzt. + +**Regel:** +- echte Modulnamen +- echte Signalpfad-Logik +- präzise Zustände +- nüchterne, moderne technische Konsole statt Spielerei oder retro-technischer Härte + +--- + +## 18. Festgelegte Architektur für die erste Realisierung + +Empfehlung für die erste Realisierung: +- eigener `Flow`-Tab +- acht klare Knoten +- strikte Statusfarben +- Hover für Status +- Click für kleine Popover +- Popover als Quick Control +- direkter Link in passende Detail-Tabs/Panels +- globaler Alarm-Banner nur bei Relevanz +- bestehende UI bleibt Vollkonfigurations-/Fallback-Ebene + +### Festgelegte Modulkette +- `Source` +- `Ingest / Buffer` +- `Audio` +- `Processing` +- `Stereo` +- `RDS` +- `Composite / MPX` +- `TX / RF` + +Das ist der pragmatischste Weg zu einer professionelleren Broadcast-Oberfläche, ohne die vorhandene UI sofort wegzuwerfen. + +--- + +## 19. Konkretes Modul- und Wireframe-Schema + +Dieser Abschnitt definiert den ersten baubaren MVP des Flow Views. + +## 19.1 Gesamt-Wireframe + +### Obere Leiste +Von links nach rechts: +1. `ferrite.fm` Wortmarke / Produktname +2. globaler TX-Zustand +3. Applied / Target Frequency +4. Source Summary +5. Active Alarm / Warning Banner (nur wenn relevant) +6. Emergency Actions (`Stop TX`, `Reset Fault`) + +### Flow-Zeile +Eine horizontale Kette mit 8 Modulknoten. +Jeder Knoten besitzt: +- Icon +- Namen +- Statusfarbe +- 1 kurze Sekundärzeile +- Hover-Bubble +- Click-Popover + +### Verbindungssegmente +Zwischen den Knoten liegen simple horizontale Linien / Segmente. +Optional können Segmente dieselbe Statusfarbe wie der nachfolgende Knoten aufnehmen oder neutral bleiben. Für MVP besser neutral und ruhig halten. + +### Untere Sekundärzone +Schmale Zeile für: +- queue health +- ingest state +- runtime age +- last update + +Keine große zweite Dashboard-Wand. Nur kompakte Betriebszusammenfassung. + +--- + +## 19.2 Knoten-Spezifikation + +## A. Source +### Icon +- Input / link / connector symbol + +### Primärlabel +- `Source` + +### Sekundärzeile +- aktiver Typ, z. B. `icecast`, `srt`, `aes67`, `stdin`, `http-raw`, `none` + +### Primärstatuslogik +- green → connected / active source +- amber → reconnecting / degraded +- red → source expected but disconnected / failed +- gray → no source / none configured + +### Hover-Inhalt +- Source kind +- Origin / endpoint / stream name +- Transport / codec (falls verfügbar) +- Connection state +- Reconnect count / last error (falls vorhanden) + +### Quick-Control-Popover +- source kind (read-only oder einfacher selector nur wenn sinnvoll) +- URL / endpoint (wenn einfache Bearbeitung vertretbar) +- decoder mode (bei icecast) +- reconnect enabled +- Button: `Open Input details` + +### Linkziel im Detail-UI +- Tab: `Ingest` +- Fokus: `Ingest Config` + +--- + +## B. Ingest / Buffer +### Icon +- queue / buffer / stacked blocks + +### Primärlabel +- `Ingest / Buffer` + +### Sekundärzeile +- z. B. `1.5s buffered`, `prebuffering`, `write blocked`, `healthy` + +### Primärstatuslogik +- green → healthy ingest runtime +- amber → low buffer / reconnecting / prebuffering / delayed +- red → critical / stalled / write blocked with active fault relevance +- gray → ingest inactive + +### Hover-Inhalt +- Runtime state +- Buffered seconds +- Prebuffer state +- Last chunk age +- Reconnect count +- Write blocked yes/no + +### Quick-Control-Popover +- prebuffer ms +- reconnect enabled +- initial/max backoff +- Button: `Open Ingest config` +- Button: `Open Diagnostics` + +### Linkziel im Detail-UI +- Tab: `Ingest` +- Fokus: `Ingest Config` + +--- + +## C. Audio +### Icon +- waveform / fader + +### Primärlabel +- `Audio` + +### Sekundärzeile +- z. B. `gain 1.00`, `tones off`, `tones active` + +### Primärstatuslogik +- green → active / healthy +- amber → unusual tone mode / suspicious gain / weak signal condition +- red → no usable audio in relevant runtime state +- gray → inactive / no source + +### Hover-Inhalt +- input gain +- tone left/right +- tone amplitude +- audio activity summary + +### Quick-Control-Popover +- input gain +- tone left +- tone right +- tone amplitude +- Button: `Open Audio controls` + +### Linkziel im Detail-UI +- Tab: `TX Control` +- Fokus: `Audio & Drive` + +--- + +## D. Processing +### Icon +- filter / dynamics / processor block + +### Primärlabel +- `Processing` + +### Sekundärzeile +- z. B. `limiter on`, `pre-emphasis 50µs`, `BS.412 off` + +### Primärstatuslogik +- green → healthy / nominal +- amber → restart required / compliance attention / unusual setting combination +- red → critical processing/compliance state (nur falls wirklich fault-relevant) +- gray → inactive / bypassed if such mode exists + +### Hover-Inhalt +- limiter on/off + ceiling +- pre-emphasis +- BS.412 enabled + threshold +- clipper enabled summary + +### Quick-Control-Popover +- limiter enabled +- limiter ceiling +- pre-emphasis +- BS.412 enabled +- Button: `Open Processing controls` + +### Linkziel im Detail-UI +- Tab: `TX Control` +- Fokus: `Audio & Drive` oder `MPX Compliance` + +--- + +## E. Stereo +### Icon +- linked L/R or stereo channels icon + +### Primärlabel +- `Stereo` + +### Sekundärzeile +- z. B. `DSB`, `stereo on`, `mono` + +### Primärstatuslogik +- green → stereo enabled and healthy +- amber → non-default mode / pilot attention / pending state +- gray → stereo disabled / mono +- red → only if runtime actually indicates a fault-worthy stereo stage problem + +### Hover-Inhalt +- stereo enabled +- stereo mode +- pilot level + +### Quick-Control-Popover +- stereo enabled +- stereo mode +- pilot level +- Button: `Open Stereo controls` + +### Linkziel im Detail-UI +- Tab: `TX Control` +- Fokus: `Switches` + +--- + +## F. RDS +### Icon +- metadata / text / radio-data symbol + +### Primärlabel +- `RDS` + +### Sekundärzeile +- z. B. `PS FERRITE`, `RT active`, `disabled` + +### Primärstatuslogik +- green → enabled and active +- amber → enabled but metadata stale / pending / mixed runtime impact / relay mismatch +- gray → disabled +- red → only if a real operational RDS fault state is surfaced + +### Hover-Inhalt +- enabled / disabled +- PI +- PTY +- PS +- active RT +- TP / TA +- RDS2 summary if applicable + +### Quick-Control-Popover +- enable RDS +- PI +- PTY +- PS +- RT +- TP / TA +- Button: `Open RDS details` + +### Linkziel im Detail-UI +- Tab: `RDS` +- Fokus: `Station Identity` or `On-Air Text` + +--- + +## G. Composite / MPX +### Icon +- multiplex / layered-spectrum symbol + +### Primärlabel +- `Composite / MPX` + +### Sekundärzeile +- z. B. `pilot 9%`, `RDS 4%`, `mpx 1.00` + +### Primärstatuslogik +- green → nominal multiplex state +- amber → high injection / compliance attention / restart pending / unusual MPX values +- red → critical compliance or composite issue if surfaced +- gray → inactive when TX chain inactive + +### Hover-Inhalt +- pilot level +- RDS injection +- MPX gain +- composite clipper state +- BS.412 state + +### Quick-Control-Popover +- pilot level +- RDS injection +- MPX gain +- clipper enabled shortcut +- Button: `Open MPX controls` + +### Linkziel im Detail-UI +- Tab: `TX Control` or `RDS` +- Fokus: `MPX Compliance` or `Injection Levels` + +--- + +## H. TX / RF +### Icon +- antenna / RF output / transmitter symbol + +### Primärlabel +- `TX / RF` + +### Sekundärzeile +- z. B. `99.5 MHz`, `ON AIR`, `idle`, `faulted` + +### Primärstatuslogik +- green → running / on air +- amber → arming / degraded / muted / stopping +- red → faulted +- gray → idle / off air + +### Hover-Inhalt +- runtime state +- applied frequency +- target frequency +- queue health +- underruns +- backend / driver +- last fault summary + +### Quick-Control-Popover +- start TX / stop TX +- reset fault +- frequency +- refresh runtime +- Button: `Open TX control` + +### Linkziel im Detail-UI +- Tab: `Overview` or `TX Control` +- Fokus: `Frequency` / Hero area + +--- + +## 19.3 Globale Interaktion + +### Hover +- Öffnet kompakte Bubble +- bubble schließt automatisch beim Verlassen +- kein interaktives Formular im Hover + +### Click +- öffnet Popover am Knoten +- nur ein Popover gleichzeitig offen +- Klick außerhalb schließt Popover +- ESC schließt Popover + +### Statusbanner +- erscheint oberhalb des Flows nur bei Relevanz +- enthält Severity + Kurztext + optionalen Deep-Link + +--- + +## 19.4 MVP-Bauplan + +### Phase MVP-1 +- statischer `Flow`-Tab +- acht Knoten +- Statusfarben +- Sekundärzeilen +- read-only Hover-Bubbles + +### Phase MVP-2 +- interaktive Popover +- erste Quick Controls für TX, RDS, Source, Processing +- Deep-Links in bestehende Tabs/Panels + +### Phase MVP-3 +- Draft-/Apply-Anbindung +- konsistente Pending-/Restart-/Reload-Marker +- Alarm-Banner diff --git a/internal/control/ui.html b/internal/control/ui.html index 2c6ea18..6d710a9 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -39,6 +39,59 @@ button,input,select{font:inherit}button{user-select:none} .led.on-amber{background:var(--amber);box-shadow:0 0 0 3px rgba(183,121,31,.14)} .led.on-blue{background:var(--accent);box-shadow:0 0 0 3px rgba(31,77,157,.14)} .status-text{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em} +.flow-banner{display:none;align-items:center;gap:10px;margin:0 0 14px;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow)} +.flow-banner.show{display:flex} +.flow-banner.good{border-color:rgba(13,148,74,.25);background:var(--green-soft)} +.flow-banner.warn{border-color:rgba(183,121,31,.3);background:var(--amber-soft)} +.flow-banner.err{border-color:rgba(176,48,48,.32);background:var(--red-soft)} +.flow-banner-title{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase} +.flow-banner-text{font-size:12px;color:var(--text-dim)} +.flow-board{padding:16px;display:flex;flex-direction:column;gap:16px} +.flow-topbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px} +.flow-summary{padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface2)} +.flow-summary-label{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:6px} +.flow-summary-value{font-size:16px;font-weight:700} +.flow-chain{display:grid;grid-template-columns:repeat(8,minmax(120px,1fr));gap:16px;align-items:stretch} +.flow-node{position:relative;display:flex;flex-direction:column;gap:8px;min-height:136px;padding:14px 14px 16px;border:1px solid var(--border);border-radius:12px;background:linear-gradient(180deg,#ffffff 0%, #f7f9fc 100%);box-shadow:0 10px 26px rgba(15,23,42,.08);cursor:pointer;transition:border-color .16s,transform .16s,box-shadow .16s} +.flow-node::after{content:'';position:absolute;top:50%;right:-17px;width:18px;height:2px;background:linear-gradient(90deg,var(--border-strong),rgba(188,197,206,.2));transform:translateY(-50%)} +.flow-node:last-child::after{display:none} +.flow-node:hover{transform:translateY(-1px);border-color:var(--border-strong);box-shadow:0 14px 30px rgba(15,23,42,.11)} +.flow-node.selected{outline:2px solid rgba(31,77,157,.22);outline-offset:0;box-shadow:0 0 0 4px rgba(31,77,157,.06),0 14px 30px rgba(15,23,42,.12)} +.flow-node.good{border-color:rgba(13,148,74,.3);background:linear-gradient(180deg,#fff 0%, rgba(13,148,74,.045) 100%)} +.flow-node.warn{border-color:rgba(183,121,31,.32);background:linear-gradient(180deg,#fff 0%, rgba(183,121,31,.06) 100%)} +.flow-node.err{border-color:rgba(176,48,48,.34);background:linear-gradient(180deg,#fff 0%, rgba(176,48,48,.07) 100%)} +.flow-node.idle{border-color:var(--border);background:linear-gradient(180deg,#fff 0%, #f8fafc 100%)} +.flow-node-head{display:flex;align-items:center;justify-content:space-between;gap:10px} +.flow-node-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);font-size:16px;line-height:1} +.flow-node-state{width:10px;height:10px;border-radius:50%;flex-shrink:0} +.flow-node.good .flow-node-state{background:var(--green);box-shadow:0 0 0 3px rgba(13,148,74,.14)} +.flow-node.warn .flow-node-state{background:var(--amber);box-shadow:0 0 0 3px rgba(183,121,31,.14)} +.flow-node.err .flow-node-state{background:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.14)} +.flow-node.idle .flow-node-state{background:#9aa5b1;box-shadow:0 0 0 3px rgba(154,165,177,.15)} +.flow-node-title{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.08em} +.flow-node-sub{font-size:12px;color:var(--text-dim);min-height:34px;font-weight:600} +.flow-node-detail{font-size:11px;color:var(--text-muted);line-height:1.35} +.flow-node-actions{margin-top:auto;padding-top:8px;border-top:1px solid rgba(188,197,206,.45);font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:.08em} +.flow-bottom{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px} +.flow-tooltip{position:fixed;z-index:1500;display:none;max-width:280px;padding:12px;border:1px solid var(--border);border-radius:10px;background:rgba(255,255,255,.98);box-shadow:0 14px 34px rgba(15,23,42,.16);pointer-events:none} +.flow-tooltip.show{display:block} +.flow-tooltip-title{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px} +.flow-tooltip-status{font-size:11px;font-weight:700;margin-bottom:6px} +.flow-tooltip-lines{display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--text-dim)} +.flow-popover{position:fixed;z-index:1600;display:none;width:min(360px,calc(100vw - 24px));padding:14px;border:1px solid var(--border-strong);border-radius:12px;background:linear-gradient(180deg,rgba(255,255,255,.995) 0%, rgba(245,248,252,.995) 100%);box-shadow:0 20px 42px rgba(15,23,42,.2)} +.flow-popover.show{display:block} +.flow-popover-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid rgba(188,197,206,.45)} +.flow-popover-title{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.08em} +.flow-popover-status{font-size:11px;color:var(--text-dim);margin-top:2px;text-transform:uppercase;letter-spacing:.08em} +.flow-popover-close{min-height:32px;padding:0 10px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em} +.flow-popover-lines{display:flex;flex-direction:column;gap:5px;margin-bottom:12px;font-size:11px;color:var(--text-dim)} +.flow-popover-lines div{display:flex;justify-content:space-between;gap:12px;padding:2px 0;border-bottom:1px dashed rgba(188,197,206,.35)} +.flow-popover-lines div:last-child{border-bottom:none} +.flow-popover-fields{display:flex;flex-direction:column;gap:10px} +.flow-popover-row{display:flex;flex-direction:column;gap:5px} +.flow-popover-row label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em} +.flow-popover-row input,.flow-popover-row select{width:100%} +.flow-popover-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px} /* Tabs */ .tab-bar{display:flex;gap:2px;border-bottom:1px solid var(--border);margin:0 0 20px;flex-wrap:wrap;position:sticky;top:0;background:rgba(247,248,251,.95);backdrop-filter:blur(8px);z-index:10} .tab-btn{border:none;border-bottom:3px solid transparent;border-radius:0;background:transparent;color:var(--text-dim);font-size:13px;font-weight:700;padding:12px 14px 11px;cursor:pointer;transition:color .16s,border-color .16s} @@ -220,8 +273,8 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 .toast.ok{background:var(--green);color:var(--bg)}.toast.err{background:var(--accent);color:#fff} .toast.warn{background:var(--amber);color:#141414}.toast.info{background:var(--text-dim);color:#fff} /* Responsive */ -@media(max-width:980px){.tab-columns.two{grid-template-columns:1fr}.tx-bar{grid-template-columns:1fr}.tx-state-wrap{align-items:flex-start}.status-hint{text-align:left}.quick-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.signal-grid{grid-template-columns:1fr}} -@media(max-width:640px){.app{padding:12px}.header{flex-direction:column;gap:10px}.header h1{font-size:22px}.badge{width:100%;justify-content:space-between}.badge strong{max-width:52%;overflow:hidden;text-overflow:ellipsis}.quick-grid{grid-template-columns:1fr 1fr;gap:8px}.ctrl-row{flex-direction:column;align-items:stretch}.ctrl-label-wrap{min-width:auto}.ctrl-input{flex-wrap:wrap}.ingest-grid{grid-template-columns:1fr}input[type="number"]{width:100%;text-align:left}.actions-row,.tx-actions{flex-direction:column}.tx-btn,.ghost-btn,.apply-btn,.preset-btn,.danger-btn{width:100%}.freq-display{font-size:31px}.shortcuts-grid{grid-template-columns:1fr}} +@media(max-width:980px){.tab-columns.two{grid-template-columns:1fr}.tx-bar{grid-template-columns:1fr}.tx-state-wrap{align-items:flex-start}.status-hint{text-align:left}.quick-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.signal-grid{grid-template-columns:1fr}.flow-topbar,.flow-bottom{grid-template-columns:repeat(2,minmax(0,1fr))}.flow-chain{grid-template-columns:repeat(4,minmax(140px,1fr))}} +@media(max-width:640px){.app{padding:12px}.header{flex-direction:column;gap:10px}.header h1{font-size:22px}.badge{width:100%;justify-content:space-between}.badge strong{max-width:52%;overflow:hidden;text-overflow:ellipsis}.quick-grid{grid-template-columns:1fr 1fr;gap:8px}.ctrl-row{flex-direction:column;align-items:stretch}.ctrl-label-wrap{min-width:auto}.ctrl-input{flex-wrap:wrap}.ingest-grid{grid-template-columns:1fr}input[type="number"]{width:100%;text-align:left}.actions-row,.tx-actions{flex-direction:column}.tx-btn,.ghost-btn,.apply-btn,.preset-btn,.danger-btn{width:100%}.freq-display{font-size:31px}.flow-topbar,.flow-bottom,.flow-chain{grid-template-columns:1fr}} @@ -243,7 +296,12 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
connecting
+
+
Status
+
No active issues.
+
+ @@ -252,6 +310,26 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
+ +
+
+
+
TX State
--
+
Applied / Target
--
+
Source
--
+
Active Alert
--
+
+
+
+
Queue Health
--
+
Ingest Runtime
--
+
Runtime Age
--
+
Last Update
--
+
+
+
+
+
@@ -772,6 +850,16 @@ const $=id=>document.getElementById(id); const RUNTIME_MS=1000,CONFIG_MS=8000,SPARK_LIMIT=40,TRANS_LIMIT=6; const FREQ_PRESETS=[87.6,94.5,99.5,100.0,107.9]; const PTY_NAMES=['None','News','Current Affairs','Information','Sport','Education','Drama','Culture','Science','Varied','Pop Music','Rock Music','Easy Listening','Light Classical','Serious Classical','Other Music','Weather','Finance',"Children's",'Social Affairs','Religion','Phone-In','Travel','Leisure','Jazz Music','Country Music','National Music','Oldies Music','Folk Music','Documentary','Alarm Test','Alarm']; +const FLOW_NODES=[ + {key:'source',label:'Source',icon:'IN'}, + {key:'ingest',label:'Ingest / Buffer',icon:'BUF'}, + {key:'audio',label:'Audio',icon:'AUD'}, + {key:'processing',label:'Processing',icon:'DSP'}, + {key:'stereo',label:'Stereo',icon:'ST'}, + {key:'rds',label:'RDS',icon:'RDS'}, + {key:'mpx',label:'Composite / MPX',icon:'MPX'}, + {key:'tx',label:'TX / RF',icon:'RF'}, +]; const mobileMq=window.matchMedia('(max-width:640px)'); let toastTimer=null; @@ -779,6 +867,7 @@ const S={ server:{config:null,runtime:null,configOk:false,runtimeOk:false,lastConfigAt:0,lastRuntimeAt:0}, lastRTState:'',draft:{},errors:{},dirty:new Set(), fieldErrors:{}, + flowSelected:null,flowHover:null,flowAnchor:null, pending:0,txBusy:false,faultBusy:false,toggleBusy:{}, cfgDraft:{},cfgDirty:{},cfgErrors:{}, ingestDraft:null,ingestDirty:false,ingestSaving:false,ingestError:'', @@ -887,6 +976,40 @@ function parseTs(v){if(!v)return Date.now();if(typeof v==='number')return v;cons function normState(s){return(typeof s==='string'?s.trim().toLowerCase():'')||'idle';} function stateSev(s){switch(normState(s)){case 'running':return'ok';case 'degraded':case 'muted':return'warn';case 'faulted':return'err';default:return'info';}} function stateClass(s){switch(normState(s)){case 'faulted':return'err';case 'muted':case 'degraded':case 'prebuffering':case 'arming':case 'stopping':case 'idle':return'warn';default:return'good';}} +function flowSeverityFromRuntime(){const rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},src=ing.source||{},ir=ing.runtime||{};if(normState(eng.state)==='faulted')return{sev:'err',title:'Fault',text:eng.lastError||eng.runtimeAlert||'TX faulted'};if(eng.runtimeAlert)return{sev:'warn',title:'Runtime warning',text:String(eng.runtimeAlert)};if(ir.writeBlocked)return{sev:'err',title:'Ingest blocked',text:'Ingest runtime is write-blocked'};if(src.state&&String(src.state).toLowerCase()==='reconnecting')return{sev:'warn',title:'Source reconnecting',text:'Input source is reconnecting'};return null;} +function flowNodeData(){const cfg=S.server.config||{},rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},src=ing.source||{},ir=ing.runtime||{},active=ing.active||{},rds=cfg.rds||{},fm=cfg.fm||{},audio=cfg.audio||{};const txState=normState(eng.state);const queueHealth=String(eng.queue?.health||'').toLowerCase();const sourceKind=String(active.kind||cfg.ingest?.kind||'none');const sourceState=String(src.state||'').toLowerCase(); + return { + source:{state:sourceKind==='none'?'idle':(sourceState==='reconnecting'?'warn':(src.connected===false?'err':'good')),sub:sourceKind,detail:(active.origin?.endpoint||active.origin?.streamName||'No source configured'),lines:[`Kind: ${sourceKind}`,`Origin: ${active.origin?.endpoint||active.origin?.streamName||'--'}`,`State: ${src.state||'idle'}`,`Reconnects: ${src.reconnects??0}`],applyMode:'reload'}, + ingest:{state:ir.writeBlocked?'err':(String(ir.state||'').toLowerCase()==='degraded'||sourceState==='reconnecting'?'warn':(sourceKind==='none'?'idle':'good')),sub:ir.state||src.state||'idle',detail:joinParts([isFinite(Number(src.bufferedSeconds))?`${Number(src.bufferedSeconds).toFixed(2)}s buffered`:'',ir.prebuffering?'prebuffering':'']).trim()||'Buffer status unavailable',lines:[`Runtime: ${ir.state||'--'}`,`Buffered: ${isFinite(Number(src.bufferedSeconds))?Number(src.bufferedSeconds).toFixed(2)+'s':'--'}`,`Last chunk: ${ageFrom(ir.lastChunkAt||src.lastChunkAt)}`,`Write blocked: ${ir.writeBlocked?'yes':'no'}`],applyMode:'reload'}, + audio:{state:audio.ToneAmplitude>0?'warn':(sourceKind==='none'?'idle':'good'),sub:`gain ${Number(audio.gain??0).toFixed(2)}`,detail:audio.toneAmplitude>0?`Tones active · ${audio.toneLeftHz}/${audio.toneRightHz} Hz`:'Tones off',lines:[`Gain: ${Number(audio.gain??0).toFixed(2)}`,`Tone L: ${audio.toneLeftHz??'--'} Hz`,`Tone R: ${audio.toneRightHz??'--'} Hz`,`Tone Amp: ${Number(audio.toneAmplitude??0).toFixed(2)}`],applyMode:'mixed'}, + processing:{state:fm.bs412Enabled?'warn':'good',sub:fm.limiterEnabled?'Limiter on':'Limiter off',detail:`Pre-emphasis ${fm.preEmphasisTauUS||0} µs`,lines:[`Limiter: ${fm.limiterEnabled?'on':'off'}`,`Ceiling: ${Number(fm.limiterCeiling??0).toFixed(2)}`,`Pre-emphasis: ${fm.preEmphasisTauUS||0} µs`,`BS.412: ${fm.bs412Enabled?'on':'off'}`],applyMode:'mixed'}, + stereo:{state:fm.stereoEnabled?'good':'idle',sub:fm.stereoEnabled?(fm.stereoMode||'DSB'):'mono',detail:`Pilot ${(Number(fm.pilotLevel??0)*100).toFixed(1)}%`,lines:[`Enabled: ${fm.stereoEnabled?'yes':'no'}`,`Mode: ${fm.stereoMode||'DSB'}`,`Pilot: ${(Number(fm.pilotLevel??0)*100).toFixed(1)}%`],applyMode:'live'}, + rds:{state:rds.enabled?'good':'idle',sub:rds.enabled?`PS ${String(eng.activePS||rds.ps||'--')}`:'disabled',detail:String(eng.activeRadioText||rds.radioText||'No RadioText').slice(0,48),lines:[`Enabled: ${rds.enabled?'yes':'no'}`,`PI: ${rds.pi||'--'}`,`PTY: ${fmtPTY(rds.pty)}`,`PS: ${eng.activePS||rds.ps||'--'}`,`RT: ${eng.activeRadioText||rds.radioText||'--'}`],applyMode:'mixed'}, + mpx:{state:queueHealth==='critical'?'err':(fm.bs412Enabled||fm.compositeClipper?.enabled?'warn':'good'),sub:`Pilot ${(Number(fm.pilotLevel??0)*100).toFixed(1)}% · RDS ${(Number(fm.rdsInjection??0)*100).toFixed(1)}%`,detail:`MPX gain ${Number(fm.mpxGain??1).toFixed(2)}`,lines:[`Pilot: ${(Number(fm.pilotLevel??0)*100).toFixed(1)}%`,`RDS inj: ${(Number(fm.rdsInjection??0)*100).toFixed(1)}%`,`MPX gain: ${Number(fm.mpxGain??1).toFixed(2)}`,`Clipper: ${fm.compositeClipper?.enabled?'on':'off'}`],applyMode:'mixed'}, + tx:{state:txState==='running'?'good':(txState==='faulted'?'err':(txState==='idle'?'idle':'warn')),sub:isFinite(Number(eng.appliedFrequencyMHz))?`${Number(eng.appliedFrequencyMHz).toFixed(1)} MHz`:(isFinite(Number(fm.frequencyMHz))?`${Number(fm.frequencyMHz).toFixed(1)} MHz`:'--'),detail:String(eng.state||'idle').toUpperCase(),lines:[`State: ${String(eng.state||'idle').toUpperCase()}`,`Applied: ${isFinite(Number(eng.appliedFrequencyMHz))?Number(eng.appliedFrequencyMHz).toFixed(1)+' MHz':'--'}`,`Target: ${isFinite(Number(fm.frequencyMHz))?Number(fm.frequencyMHz).toFixed(1)+' MHz':'--'}`,`Queue: ${eng.queue?.health||'--'}`,`Underruns: ${eng.underruns??'--'}`],applyMode:'live'}, + }; +} +function renderFlow(){const chain=$('flow-chain');if(!chain)return;const data=flowNodeData();chain.innerHTML=FLOW_NODES.map(node=>{const d=data[node.key]||{state:'idle',sub:'--',detail:'--'};const sel=S.flowSelected===node.key?' selected':'';return ``;}).join(''); + chain.querySelectorAll('[data-flow-node]').forEach(el=>{const key=el.dataset.flowNode;el.addEventListener('mouseenter',e=>showFlowTooltip(key,e.currentTarget));el.addEventListener('mouseleave',hideFlowTooltip);el.addEventListener('focus',e=>showFlowTooltip(key,e.currentTarget));el.addEventListener('blur',hideFlowTooltip);el.addEventListener('click',()=>openFlowPopover(key,el));}); + const issue=flowSeverityFromRuntime();const banner=$('flow-banner');if(banner){if(issue){banner.className=`flow-banner show ${issue.sev}`;$('flow-banner-title').textContent=issue.title;$('flow-banner-text').textContent=issue.text;}else{banner.className='flow-banner';$('flow-banner-title').textContent='Status';$('flow-banner-text').textContent='No active issues.';}} + setText('flow-top-state',String((S.server.runtime?.engine?.state||'idle')).toUpperCase());setText('flow-top-frequency',joinParts([isFinite(Number(S.server.runtime?.engine?.appliedFrequencyMHz))?`${Number(S.server.runtime.engine.appliedFrequencyMHz).toFixed(1)} MHz applied`:'Applied --',isFinite(Number(S.server.config?.fm?.frequencyMHz))?`${Number(S.server.config.fm.frequencyMHz).toFixed(1)} MHz target`:'Target --'])||'--');setText('flow-top-source',String(S.server.runtime?.ingest?.active?.kind||S.server.config?.ingest?.kind||'none'));setText('flow-top-alert',issue?issue.text:'None');setText('flow-bottom-queue',String(S.server.runtime?.engine?.queue?.health||'--'));setText('flow-bottom-ingest',String(S.server.runtime?.ingest?.runtime?.state||S.server.runtime?.ingest?.source?.state||'--'));setText('flow-bottom-age',fmtTime(Number(S.server.runtime?.engine?.runtimeStateDurationSeconds)));setText('flow-bottom-update',ageStr(Math.max(S.server.lastConfigAt||0,S.server.lastRuntimeAt||0)));renderFlowPopover(); +} +function showFlowTooltip(key,anchor){const tip=$('flow-tooltip');if(!tip||S.flowSelected===key)return;const d=flowNodeData()[key];if(!d)return;tip.innerHTML=`
${FLOW_NODES.find(n=>n.key===key)?.label||key}
${String(d.state||'idle').toUpperCase()}
${(d.lines||[]).map(line=>`
${line}
`).join('')}
`;const r=anchor.getBoundingClientRect();tip.style.left=`${Math.min(window.innerWidth-300,Math.max(12,r.left + window.scrollX))}px`;tip.style.top=`${r.bottom + window.scrollY + 8}px`;tip.classList.add('show');} +function hideFlowTooltip(){const tip=$('flow-tooltip');if(tip)tip.classList.remove('show');} +function openFlowPopover(key,anchor){S.flowSelected=S.flowSelected===key?null:key;S.flowAnchor=S.flowSelected?anchor:null;hideFlowTooltip();render();} +function closeFlowPopover(){S.flowSelected=null;S.flowAnchor=null;render();} +function flowJump(tab){document.querySelectorAll('.tab-btn[data-tab]').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));document.querySelectorAll('.tab-panel[data-tab-panel]').forEach(p=>p.classList.toggle('active',p.dataset.tabPanel===tab));closeFlowPopover();} +function renderFlowPopover(){const pop=$('flow-popover');if(!pop)return;if(!S.flowSelected||!S.flowAnchor){pop.classList.remove('show');return;}const key=S.flowSelected;const d=flowNodeData()[key];if(!d){pop.classList.remove('show');return;}const cfg=S.server.config||{};let fields='';let actions='';let modeNote='';if(key==='tx'){modeNote='Applies immediately.';fields=`
`;actions=``;}else if(key==='rds'){modeNote='Mixed runtime impact: enable/PS/RT apply now; PI/PTY belong in detailed controls.';fields=`
`;actions=``;}else if(key==='source'||key==='ingest'){modeNote='Requires hard reload via detailed ingest configuration.';fields=`
`;actions=``;}else if(key==='processing'){modeNote='Mixed runtime impact: limiter and ceiling apply now; pre-emphasis/BS.412 stay in detailed controls.';fields=`
`;actions=``;}else if(key==='mpx'){modeNote='Mixed runtime impact: pilot/RDS injection apply now; MPX gain and structural clipper settings stay in detailed controls.';fields=`
`;actions=``;}else if(key==='audio'){modeNote='Mixed runtime impact: tone controls apply now; input gain remains in detailed controls.';fields=`
`;actions=``;}else if(key==='stereo'){modeNote='Applies immediately.';fields=`
`;actions=``;} + pop.innerHTML=`
${FLOW_NODES.find(n=>n.key===key)?.label||key}
${String(d.state||'idle').toUpperCase()} · ${d.sub||''}
${(d.lines||[]).map(line=>`
${line}
`).join('')}
${modeNote}
${fields}
${actions}
`; + const r=S.flowAnchor.getBoundingClientRect();pop.style.left=`${Math.min(window.innerWidth-380,Math.max(12,r.left + window.scrollX))}px`;pop.style.top=`${r.bottom + window.scrollY + 10}px`;pop.classList.add('show');$('flow-popover-close')?.addEventListener('click',closeFlowPopover); + $('flow-action-start')?.addEventListener('click',()=>{closeFlowPopover();txAction('start');});$('flow-action-stop')?.addEventListener('click',()=>{closeFlowPopover();txAction('stop');});$('flow-action-reset-fault')?.addEventListener('click',()=>{closeFlowPopover();resetFault();});$('flow-open-control')?.addEventListener('click',()=>flowJump('tx'));$('flow-open-rds')?.addEventListener('click',()=>flowJump('rds'));$('flow-open-ingest')?.addEventListener('click',()=>flowJump('ingest'));$('flow-open-diagnostics')?.addEventListener('click',()=>flowJump('diagnostics'));$('flow-open-processing')?.addEventListener('click',()=>flowJump('tx'));$('flow-open-audio')?.addEventListener('click',()=>flowJump('tx'));$('flow-open-stereo')?.addEventListener('click',()=>flowJump('tx')); + $('flow-action-save-frequency')?.addEventListener('click',async()=>{const frequencyMHz=Number($('flow-field-frequency')?.value||cfg.fm?.frequencyMHz||100);closeFlowPopover();await sendPatch({frequencyMHz},{ok:'Frequency updated',clearKeys:[]});}); + $('flow-action-save-rds')?.addEventListener('click',async()=>{const rdsEnabled=$('flow-field-rds-enabled')?.value==='true';const ps=$('flow-field-ps')?.value||'';const radioText=$('flow-field-rt')?.value||'';const tp=$('flow-field-rds-tp')?.value==='true';const ta=$('flow-field-rds-ta')?.value==='true';closeFlowPopover();await sendPatch({rdsEnabled,ps,radioText,tp,ta},{ok:'RDS updated',clearKeys:[]});}); + $('flow-action-save-processing')?.addEventListener('click',async()=>{const limiterEnabled=$('flow-field-limiter')?.value==='true';const limiterCeiling=Number($('flow-field-ceiling')?.value||1);closeFlowPopover();await sendPatch({limiterEnabled,limiterCeiling},{ok:'Processing updated',clearKeys:[]});}); + $('flow-action-save-mpx')?.addEventListener('click',async()=>{const pilotLevel=Number($('flow-field-pilot')?.value||cfg.fm?.pilotLevel||0.09);const rdsInjection=Number($('flow-field-rdsinj')?.value||cfg.fm?.rdsInjection||0.04);const compositeClipperEnabled=$('flow-field-compclip')?.value==='true';closeFlowPopover();await sendPatch({pilotLevel,rdsInjection,compositeClipperEnabled},{ok:'MPX updated',clearKeys:[]});}); + $('flow-action-save-audio')?.addEventListener('click',async()=>{const toneLeftHz=Number($('flow-field-tone-left')?.value||cfg.audio?.toneLeftHz||1000);const toneRightHz=Number($('flow-field-tone-right')?.value||cfg.audio?.toneRightHz||1600);const toneAmplitude=Number($('flow-field-tone-amp')?.value||cfg.audio?.toneAmplitude||0);closeFlowPopover();await sendPatch({toneLeftHz,toneRightHz,toneAmplitude},{ok:'Tone controls updated',clearKeys:[]});}); + $('flow-action-save-stereo')?.addEventListener('click',async()=>{const stereoEnabled=$('flow-field-stereo-enabled')?.value==='true';const stereoMode=$('flow-field-stereo-mode')?.value||'DSB';closeFlowPopover();await sendPatch({stereoEnabled,stereoMode},{ok:'Stereo updated',clearKeys:[]});}); +} // ── Formatters ───────────────────────────────────────────────────────────── function fmt(n){if(n==null)return'--';if(n>=1e9)return(n/1e9).toFixed(2)+'G';if(n>=1e6)return(n/1e6).toFixed(2)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'k';return String(n);} @@ -1156,6 +1279,7 @@ function _render(){ // Fault history const fh=$('fault-history');if(fh){const hist=Array.isArray(eng.faultHistory)?eng.faultHistory:[];if(!hist.length){fh.innerHTML='
No faults recorded yet.
';}else fh.innerHTML=hist.slice().reverse().map(e=>{const t=e?.time?new Date(e.time):null,tl=t&&!isNaN(t)?t.toLocaleTimeString():'--',sev=String(e?.severity||'warn').toLowerCase();return`
${tl}${String(e?.severity||'').toUpperCase()} ${e?.reason||''}${e?.message?' · '+e.message:''}
`;}).join('');} + renderFlow(); applyMobilePanels(); } @@ -1167,6 +1291,8 @@ function log(msg,type=''){const el=$('log');const em=el.querySelector('.empty-lo // ── Bindings ─────────────────────────────────────────────────────────────── function bindAll(){ + document.addEventListener('keydown',e=>{if(e.key==='Escape'&&S.flowSelected)closeFlowPopover();}); + document.addEventListener('click',e=>{const pop=$('flow-popover');if(!S.flowSelected||!pop)return;const inPopover=pop.contains(e.target);const inNode=e.target.closest&&e.target.closest('[data-flow-node]');if(!inPopover&&!inNode)closeFlowPopover();}); // Tabs const tbs=Array.from(document.querySelectorAll('.tab-btn[data-tab]')),tps=Array.from(document.querySelectorAll('.tab-panel[data-tab-panel]')); tbs.forEach(b=>b.addEventListener('click',()=>{tbs.forEach(x=>x.classList.toggle('active',x===b));tps.forEach(p=>p.classList.toggle('active',p.dataset.tabPanel===b.dataset.tab));}));