LogHub Panel-Dashboard

August 5, 2026 · View on GitHub

🌐 Generisch — keine Plattform vorausgesetzt. Das Dashboard ist eine reine HTTP-Schnittstelle. Quelle kann alles sein, was POSTs senden kann: ein Python-Skript, eine C#-/WinForms-App, ein Node-Server, Shell + curl, der Browser, ein Mikrocontroller. Der Store weiß nichts ĂŒber die Bedeutung der Werte. Wo unten ein ESP32 / der GeminiPod auftaucht, ist das nur ein Beispiel fĂŒr eine Quelle — keine Voraussetzung. Bilder-Quelle ist zufĂ€llig das GeminiPod-Dashboard (ESP32 „Sophia"), weil es viele Widget-Typen gleichzeitig zeigt.

📎 Single Source of Truth fĂŒr die Feld-Referenz ist src/loghub/panel-types.ts (PanelHttpEntry); die HTTP-Endpunkte stehen in src/loghub/log-server.ts. Wenn der Code sich Ă€ndert, gilt der Code — diese Seite nachziehen.


1. Was ist das Panel-Dashboard?

Der Log-Stream im Viewer ist eine Chronik: jede Zeile scrollt nach oben weg. Das ist perfekt fĂŒr Ereignisse („Verbindung aufgebaut", „Datei geladen", „Fehler X"), aber unbrauchbar fĂŒr Werte, die sich stĂ€ndig Ă€ndern. Ein Audio-Pegel, der 50-mal pro Sekunde gemeldet wird, wĂŒrde den Stream fluten und wĂ€re trotzdem nie ablesbar.

Genau dafĂŒr ist das Panel-Dashboard da. Es ist die zweite Ansicht im selben Logs-Tab (oben umschaltbar zwischen Stream und Dashboard) und funktioniert nach einem anderen Prinzip:

  • Feste Kacheln statt Scroll. Jedes Widget hat eine id. Sendest du dieselbe id erneut, wird der Wert an Ort und Stelle ĂŒberschrieben — die Kachel bleibt stehen, nur die Zahl/der Balken aktualisiert sich. Damit kannst du beliebig hochfrequent senden, ohne irgendetwas zuzumĂŒllen.
  • Gedacht fĂŒr laufende Messwerte: Audio-Pegel, Puffer-FĂŒllstand, FPS, freier Heap, WLAN-RSSI, Temperatur, Status-LEDs — alles, was du normalerweise nur mĂŒhsam aus Log-Zeilen herauslesen wĂŒrdest.
  • Zwei Richtungen. Die meisten Widgets sind Anzeige (Quelle → AiDex). Vier Typen — slider, number, toggle und button — sind interaktiv: AiDex rendert ein Bedienelement, der User betĂ€tigt es, und der neue Wert fließt zurĂŒck an die Quelle (→ Sektion 3b). Damit steuert man eine laufende App live vom Browser aus: Werte einstellen (Regler, Zahl), etwas an-/ausschalten (Schalter), etwas auslösen (Taster).
  • DurchgĂ€ngiges Prinzip: der Sender entscheidet, AiDex rendert nur. Skala, Farbe, Einheit, Nachkommastellen — all das bestimmt die Quelle. Der Viewer hat keine eigene Logik, er zeigt an, was ankommt.
  • Zero-cost und library-frei. Solange du nichts sendest, existiert kein Widget. Der EmpfĂ€nger ist reines HTTP — kein SDK, keine Bibliothek, kein Build-Schritt. Ein curl reicht.

Das Debug-Dashboard in Betrieb — drei AEC-Waveform-Plots, Gauges und interaktive Tuning-Slider

▶ In Bewegung: loghub-dashboard.gif zeigt das Dashboard live — die Tuning-Slider werden verstellt und die Waveform-Plots reagieren in Echtzeit. (Volles Video: loghub-dashboard.mp4.) Das gezeigte Board ist eine AEC-Tuning-Konsole nach dem Vorbild des GeminiPod-Satelliten.


2. Schnellstart (3 Schritte)

  1. LogHub starten — der HTTP-EmpfĂ€nger lĂ€uft auf Port 3335:

    aidex_log({ action: "init" })
    
  2. Viewer öffnen und auf den Logs-Tab → Dashboard umschalten:

    aidex_viewer({ path: "." })
    
  3. Erstes Widget senden. Ein POST /panel legt die Kachel an und setzt den Wert in einem Aufruf (Upsert). Hier eine einfache Text-Kachel:

    curl -X POST http://localhost:3335/panel \
      -H "Content-Type: application/json" \
      -d '{"id":"status","type":"label","value":"running","group":"Demo","label":"State"}'
    

    Die Kachel „State" erscheint sofort im Dashboard. Schickst du denselben id noch einmal mit einem anderen value, Ă€ndert sich nur der angezeigte Text — die Kachel bleibt:

    curl -X POST http://localhost:3335/panel \
      -H "Content-Type: application/json" \
      -d '{"id":"status","value":"paused"}'
    

    Beim zweiten Aufruf fehlt type — das ist Absicht. type braucht nur das erste Senden zum Anlegen; danach genĂŒgen id + value fĂŒrs Update.

[BILD: leeres Dashboard → erstes Widget erscheint]


3. HTTP-API

Alles lĂ€uft ĂŒber http://localhost:3335. Jeder Request ist Content-Type: application/json. Es gibt keine Authentifizierung und keinen Zustand zwischen Verbindungen — jeder POST steht fĂŒr sich.

3a. Anzeige-Endpunkte

Methode & PfadZweck
POST /panelEin Widget anlegen oder aktualisieren (beides per Upsert ĂŒber die id).
POST /panelsArray von Widgets in einem Rutsch (Batch — ein POST pro Dashboard-Tick).
POST /panel/clearBody {} = alle Widgets löschen, {"id":"x"} = nur eines. RÀumt zugehörige Controls mit weg (s.u.).
GET /healthStatus-Check (Server lebt, Buffer-Auslastung).
  • Arbeitsmuster: einmal alle Definitionen senden (mit type, group, label, Skala/Farbe), danach im Betrieb nur noch schlanke {"id":
, "value":
}-Updates. Weil POST /panel ein Upsert ist, braucht das Update kein type mehr.
  • Unbekannte JSON-Felder werden still verworfen. Nur die in Sektion 5 gelisteten Felder landen im Store. Ein Tippfehler im Feldnamen ist also kein Fehler, hat aber auch keine Wirkung — im Zweifel die Feld-Referenz gegenchecken.

3b. Control-RĂŒckkanal (interaktive Widgets)

Die Widget-Typen slider, number, toggle und button sind interaktiv: AiDex rendert ein Bedienelement, und wenn der User es betĂ€tigt, fließt der neue Wert zurĂŒck an die Quelle. Das ist der einzige Weg, auf dem Daten von AiDex zurĂŒck zur App laufen — ansonsten ist alles Einbahnstraße (Quelle → AiDex). Der Mechanismus ist bewusst dumm und quell-agnostisch: ein flacher { id: value }-Speicher, der nichts ĂŒber die Bedeutung der Werte weiß.

Zustand vs. Ereignis — der wichtige Unterschied:

  • slider, number, toggle sind Zustand. Der Wert steht einfach da; wer ihn spĂ€ter liest, bekommt trotzdem die richtige Antwort.
  • button ist ein Ereignis. Ereignisse gehen bei einem Poll-Modell verloren: drĂŒckt der User zwischen zwei Abfragen der Quelle, wĂ€re ein einfaches Ja/Nein-Flag lĂ€ngst wieder zurĂŒckgesetzt. Deshalb ist der Wert eines button ein monoton steigender ZĂ€hler. Die Quelle merkt sich den zuletzt gesehenen Stand und liest an der Differenz ab, dass und wie oft gedrĂŒckt wurde — auch bei fĂŒnf Klicks zwischen zwei Polls.
Methode & PfadZweck
POST /controlSetzt einen Control-Wert. Body { id, value }. Schreibt in den Control-Store und spiegelt den Wert auf die Kachel (alle offenen Viewer sehen die Änderung). Das ruft der Viewer auf, wenn der User am Regler zieht oder einen Schalter umlegt.
POST /control/pressMeldet einen Tastendruck. Body { id } — ohne Wert. Der Hub zĂ€hlt hoch, nicht der Aufrufer: bei zwei offenen Dashboards wĂŒrden beide denselben Folgewert schicken und ein Druck ginge verloren. Antwort enthĂ€lt den neuen ZĂ€hlerstand.
GET /controlLiefert den ganzen Store als flaches { id: value }-Objekt. Das pollt die Quelle, um die aktuellen Set-Points zu erfahren.

ZĂ€hler richtig auswerten: Ein button-Wert lĂ€uft bei 1.000.000 auf 1 ĂŒber, und POST /panel/clear setzt ihn auf 0 zurĂŒck. Die Quelle muss deshalb jeden Sprung nach unten als „Neustart, Wert ĂŒbernehmen" behandeln — nicht als knapp eine Million TastendrĂŒcke. VorwĂ€rtssprĂŒnge sind echte DrĂŒcke.

  • Fluss in vier Schritten: (1) Quelle definiert ein slider-Widget mit Startwert → (2) User schiebt den Regler im Viewer → (3) der Viewer schickt POST /control → (4) die Quelle holt sich den neuen Wert per GET /control in ihrem eigenen Takt und reagiert darauf (z.B. einen Schwellwert anpassen).
  • Die Quelle ist dieselbe wie beim Senden der Anzeige-Werte — irgendein HTTP-fĂ€higes Programm. Im GeminiPod-Beispiel ist es der ESP32, der so seinen Barge-In-Schwellwert live tunen lĂ€sst; genauso gut wĂ€re es ein Python-Loop am PC.
  • AufrĂ€umen: Es gibt keinen eigenen /control/clear-Endpunkt. POST /panel/clear entfernt das Widget und seinen Control-Wert gemeinsam (sie gehören zusammen).

Ein slider anlegen und seinen Wert pollen — minimal mit curl:

# 1) Control-Widget definieren (Startwert 40, Bereich 0..100, Schrittweite 5)
curl -X POST http://localhost:3335/panel \
  -H "Content-Type: application/json" \
  -d '{"id":"threshold","type":"slider","value":40,"min":0,"max":100,"step":5,"group":"Tuning","label":"VAD-Schwelle"}'

# 2) ... User schiebt den Regler im Viewer ...

# 3) Aktuelle Set-Points abholen — liefert z.B. {"threshold":55}
curl http://localhost:3335/control

Tuning-Slider werden verstellt, die Waveform-Plots reagieren live

Die 4 Tuning-Slider unten werden verschoben (Speaker volume, AEC ref boost 
) — die Plots in 1 AEC reagieren sofort. Genau dieser RĂŒckkanal ist gemeint: der Wert fließt vom Viewer zurĂŒck an die Quelle.


4. Widget-Typen (type)

Sechs Typen, in zwei Gruppen. Das type-Feld wird nur beim Anlegen gebraucht; danach reicht id + value.

Anzeige (Quelle → AiDex):

  • label — Text/Zahl als Wert. Mit state als farbige LED nutzbar.
  • progress — horizontaler Balken, min..max, warn/crit-Schwellen → Farbzonen.
  • gauge — Radial-Gauge (Afterburner-Style), min..max, warn/crit. Auch als LED-Feld: state = Farbe (ok/warn/error/
), value = freier Text.
  • plot — Linien-Graph (HWiNFO/Afterburner-Style) mit History-Ring (PLOT_HISTORY = 200 Samples). Footer: cur/min/max/avg. Wert-Update: einzelne Zahl (an Ring anhĂ€ngen) oder Array (ganzer Frame ersetzt die History).

Interaktiv (User im Viewer → zurĂŒck an die Quelle, via Control-Store — NEU):

  • slider — Schieberegler. Felder: min/max/step/value/label/group/order.
  • number — Zahlen-Eingabe. Gleiche Felder. Beide schreiben per POST /control zurĂŒck.
  • toggle — Schalter (Zustand), Wert 0 oder 1. Optionale Beschriftung beider Stellungen ĂŒber unit im Format "AN|AUS" (default ON/OFF). Schreibt per POST /control zurĂŒck.
  • button — Taster (Ereignis). Der Wert ist ein Druck-ZĂ€hler, kein Flag — siehe Abschnitt 3b. value beim Anlegen wird ignoriert, der ZĂ€hler startet immer bei 0. Beschriftung kommt aus label. Schreibt per POST /control/press.

[BILD: je ein Beispiel pro Typ nebeneinander — inkl. slider/number]


5. Widget-Felder — vollstĂ€ndige Referenz

Jedes Feld ist optional außer id (immer Pflicht) und type (Pflicht nur beim Anlegen). Was ein Feld bewirkt, hĂ€ngt vom Widget-Typ ab — die Spalte „Gilt fĂŒr" sagt, wo es wirkt; bei anderen Typen wird es ignoriert. Abgeglichen gegen PanelHttpEntry in src/loghub/panel-types.ts.

FeldTypGilt fĂŒrBedeutung
idstringalle (Pflicht)Eindeutiger SchlĂŒssel. Gleiche id = Update in place.
typestringerstellen (Pflicht)label/progress/gauge/plot/slider/number/toggle/button. Nur beim Anlegen nötig.
valuenumber|string|number[]alleAktueller Wert. Bei plot: Zahl = anhĂ€ngen, Array = ganzer Frame. Bei toggle: 0/1. Bei button ignoriert — der ZĂ€hler startet immer bei 0.
groupstringalleGruppen-Box. Viewer sortiert Gruppen alphabetisch → Zahlen-PrĂ€fix ("1 Boot", "2 Audio") erzwingt Reihenfolge.
labelstringalleAnzeigename der Kachel.
unitstringalleEinheit (z.B. dB, %, ms). Bei toggle: Beschriftung beider Stellungen als `"AN
minnumberprogress/gauge/plot/slider/numberSkala/Range-Untergrenze.
maxnumberprogress/gauge/plot/slider/numberSkala/Range-Obergrenze.
stepnumberslider/numberSchrittweite pro Tick (default 1).
warnnumbergauge/progressSchwelle → gelbe Zone.
critnumbergauge/progressSchwelle → rote Zone.
colorstringalleAccent-Name (cyan/green/orange/purple/
) oder Hex.
ordernumberalleSortierung innerhalb der Gruppe.
statestringgauge/labelLED-Farbe getrennt vom value-Text (ok/warn/error/
). Farbige LED + lesbarer Text gleichzeitig.
scalestringplotY-Achse: "linear" (default) | "log". → Sektion 6.
decimalsnumberplotNachkommastellen im Footer (0 = ganzzahlig).
autoMinbooleanplotUntergrenze folgt dem Daten-Minimum (Decke bleibt max). → Sektion 6.

Hinweis: step, scale, decimals, autoMin sind alle sender-gesteuert — der Renderer rendert nur, was ankommt.


6. Plot-Skalierung im Detail

Der Plot ist der anspruchsvollste Widget-Typ, weil seine Y-Achse ĂŒber die Lesbarkeit entscheidet. Vier Stellschrauben, alle sender-gesteuert:

  • Autoskala (Standard). LĂ€sst du min/max weg, skaliert der Plot fortlaufend auf den Inhalt der History (+10 % Luft). Bequem — aber ein einzelner Riesen-Peak drĂŒckt danach die ganze normale Kurve platt an den unteren Rand.
  • Feste Skala. Setzt du min und max, steht die Achse still. Kleine Schwankungen lassen den Plot nicht mehr zappeln, und Werte außerhalb des Bereichs werden geclamped statt aus dem Canvas zu laufen. Erste Wahl, sobald du den Wertebereich kennst.
  • Log-Skala (scale:"log"). FĂŒr Signale mit großer Dynamik — Audio-Pegel, alles dB-artige. Leises Sprechen und ein lauter Peak werden gleichzeitig sichtbar, weil die Achse logarithmisch staucht. Die Grenzen werden dabei auf ≄ 1 gehoben (log von 0 gibt es nicht).
  • autoMin (true). Hebt den „toten" Bereich unterhalb des Grundrauschens weg: die Untergrenze folgt dem tatsĂ€chlichen Daten-Minimum (die Decke bleibt fest auf max). So sitzt das Grundrauschen am unteren Rand und die volle Plot-Höhe steht dem eigentlichen Signal zur VerfĂŒgung.

Rezept fĂŒr einen Audio-Pegel-Plot: scale:"log" + autoMin:true + max = Vollausschlag + decimals:0. Das war das Kern-Learning aus dem GeminiPod-Einsatz — erst diese Kombination machte den Mic-Pegel ĂŒber die ganze LautstĂ€rke-Spanne ablesbar.

[BILD: Vorher/Nachher — linear-Autoskala (Peak erschlĂ€gt alles) vs log+autoMin (Rauschen unten, Signal sichtbar)]


Jeder Plot zeigt unter der Kurve vier Kennzahlen ĂŒber die sichtbare History: cur (aktueller Wert), min, max und avg (Durchschnitt). Sie stehen vertikal gestapelt — so werden sie nie abgeschnitten, egal wie schmal die Kachel ist.

Die Anzahl der Nachkommastellen steuerst du mit decimals: 0 fĂŒr ganze Zahlen (z.B. Pegel, FPS), 1–2 fĂŒr feinere GrĂ¶ĂŸen. Der Footer ĂŒbernimmt diesen Wert automatisch — du musst die Zahlen nicht selbst formatieren.


8. Best Practices / Stolpersteine (aus echtem Einsatz)

Die folgenden Punkte sind alle aus echtem Dashboard-Betrieb (u.a. dem GeminiPod auf ressourcenarmer ESP32-Hardware) entstanden — sie ersparen die typischen ersten Fehler:

  • Definitionen vor Live-Werten: erst /panels mit allen Defs, dann Updates.
  • Batch nutzen (/panels): EIN POST pro Dashboard-Tick statt vieler einzelner.
  • Senden vom Echtzeit-Pfad entkoppeln: HTTP-POSTs blockieren. Aus zeitkritischen Schleifen (Audio-Callback, Render-Loop, ISR) NICHT direkt senden — stattdessen ein eigener Dashboard-Task/Thread mit fester Rate (z.B. 5–10 Hz). Gilt ĂŒberall, ist auf ressourcenarmen Quellen (z.B. einem MCU) nur besonders spĂŒrbar.
  • Peak-Hold fĂŒr kurze Events: wenn der Sende-Takt langsamer ist als das Ereignis, das MAX seit dem letzten Tick senden (Reset beim Auslesen), sonst verschluckt der Plot kurze Peaks zwischen zwei Ticks.
  • Gruppen-Reihenfolge ĂŒber Zahlen-PrĂ€fix im group-Namen erzwingen.
  • Dynamische Skala vom Sender: Gesamtwerte, die erst zur Laufzeit bekannt sind (z.B. Gesamt-Heap), in die Definition rechnen + in den Titel („Heap frei (von N)").
  • Anlaufverzögerung: bei Plots mit Auto-History die ersten ein bis zwei Sekunden Einschwing-Werte ĂŒberspringen, sonst dominiert ein Boot-Peak die History.
  • state vs value: fĂŒr farbige Status-LEDs state (Farbe) und value (Text) trennen.
  • Controls entkoppeln: GET /control im eigenen Takt pollen, nicht synchron zum Senden — der Set-Point Ă€ndert sich selten, das Polling darf langsam sein.

9. VollstÀndiges Beispiel

Ein kleines Audio-Pegel-Dashboard, das alle Bausteine zeigt: einmaliges Anlegen aller Widgets per Batch (inkl. eines slider zum Live-Tuning), zyklische Wert-Updates und das ZurĂŒcklesen des Slider-Werts. Das Beispiel ist in Python, weil das kompakt liest — es ist aber reines HTTP und in jeder Sprache identisch (nur die POST-Syntax Ă€ndert sich).

Schritt 1 — alle Widgets einmal definieren (POST /panels, ein Batch):

import requests, time, math
HUB = "http://localhost:3335"

# Eine Definition pro Widget. Gruppen mit Zahlen-PrĂ€fix → feste Reihenfolge.
requests.post(f"{HUB}/panels", json=[
    # Plot mit dem Audio-Rezept aus Sektion 6 (log + autoMin + feste Decke).
    {"id":"mic","type":"plot","group":"1 Audio","label":"Mic Pegel","unit":"dB",
     "min":0,"max":90,"scale":"log","autoMin":True,"decimals":0,"color":"cyan"},
    # Balken fĂŒr die Puffer-FĂŒllung, mit Schwellen fĂŒr gelb/rot.
    {"id":"buf","type":"progress","group":"1 Audio","label":"Buffer","unit":"%",
     "min":0,"max":100,"warn":75,"crit":90},
    # Status-LED: Farbe kommt aus state, der Text aus value (Sektion 5).
    {"id":"link","type":"gauge","group":"2 System","label":"Verbindung",
     "state":"ok","value":"connected"},
    # Interaktiver Slider — der Wert fließt zurĂŒck an dieses Skript (Sektion 3b).
    {"id":"gain","type":"slider","group":"3 Tuning","label":"Eingangs-Gain",
     "value":50,"min":0,"max":100,"step":5},
])

Schritt 2 — im Betrieb laufend Werte senden und den Slider zurĂŒcklesen:

gain = 50
while True:
    level = measure_mic_db()          # deine Messung
    buf   = ring_buffer_fill_pct()

    # Ein Batch pro Tick — ein POST statt vieler einzelner.
    requests.post(f"{HUB}/panels", json=[
        {"id":"mic", "value":level},   # Zahl → wird an die Plot-History angehĂ€ngt
        {"id":"buf", "value":buf},
    ])

    # Set-Point in eigenem (langsamerem) Takt abholen — Ă€ndert sich selten.
    controls = requests.get(f"{HUB}/control").json()   # z.B. {"gain": 65}
    if "gain" in controls:
        gain = controls["gain"]        # neuen Gain anwenden

    time.sleep(0.1)                    # ~10 Hz Dashboard-Rate

Das war's: ein laufendes Dashboard mit Plot, Balken, Status-LED und einem Regler, ĂŒber den du die App live steuerst — ganz ohne Library, nur HTTP.

[BILD: das fertige laufende Dashboard]


10. Screenshots erstellen — GeminiPod als Demo-Objekt

Arbeitsnotiz fĂŒr die Bebilderung (die [BILD: 
]-Platzhalter oben). Kein Teil der Anleitung selbst — beschreibt nur, wie die Screenshots entstehen sollen.

  • Bild-Quelle = GeminiPod-Dashboard (ESP32 „Sophia"). Zeigt alle Widget-Typen in einem realen System: Plots (Mic Peak, Speaker, Heap), Progress (Mic RMS, Spk Ring, PSRAM), Gauge (RSSI, Heartbeat), Status-LEDs (Wake Active/Detections/Last), Labels (Modus, Uptime, Task-Stacks). [+ falls vorhanden: slider/number-Controls zeigen]
  • ERST wenn GeminiPod feature-vollstĂ€ndig und alle Felder leben — insbesondere Speaker-Plot und Spk-Ring mĂŒssen im Normalbetrieb echte Werte zeigen (nicht nur kurz bei der „Ja"-Quittung). Also erst nach laufender Gemini-Live-Session mit dauerhaftem Streaming-TTS in den Speaker. Vorher wĂ€ren Speaker-Plot/Ring meist 0.
  • Reihenfolge fĂŒrs Bebildern: (1) GeminiPod fertig (Live-Session + Speaker dauernd aktiv), (2) wĂ€hrend echter Konversation Region-Screenshots ziehen, (3) Vorher/Nachher- Paar der Skalen-Sektion gezielt nachstellen (einmal ohne scale/autoMin, einmal mit).
  • Capture-Tool: aidex_screenshot({ mode: "region" }).

TODO — noch offen

Text und HTTP-Beispiele sind fertig (gegen den Code abgeglichen, Stand 2026-06-30). Offen sind nur noch die Bilder und das Verlinken:

  • Screenshots einfĂŒgen ([BILD: 
]-Platzhalter) — Quelle GeminiPod, ERST wenn feature-vollstĂ€ndig (Speaker-Plot + Spk-Ring zeigen dauerhaft Werte). Anleitung dazu → Sektion 10. Optional: kurzes Demo-GIF (separater Task #24).
  • slider/number-Control im Demo-Bild zeigen, sobald der GeminiPod-Build es nutzt.
  • Verlinken: in AiDex-README.md / CLAUDE.md („LogHub Developer Guide") auf diese Seite verweisen.