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 insrc/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 dieselbeiderneut, 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,toggleundbuttonâ 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
curlreicht.

â¶ïž 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)
-
LogHub starten â der HTTP-EmpfĂ€nger lĂ€uft auf Port 3335:
aidex_log({ action: "init" }) -
Viewer öffnen und auf den Logs-Tab â Dashboard umschalten:
aidex_viewer({ path: "." }) -
Erstes Widget senden. Ein
POST /panellegt 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
idnoch einmal mit einem anderenvalue, Ă€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.typebraucht nur das erste Senden zum Anlegen; danach genĂŒgenid+valuefĂŒ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 & Pfad | Zweck |
|---|---|
POST /panel | Ein Widget anlegen oder aktualisieren (beides per Upsert ĂŒber die id). |
POST /panels | Array von Widgets in einem Rutsch (Batch â ein POST pro Dashboard-Tick). |
POST /panel/clear | Body {} = alle Widgets löschen, {"id":"x"} = nur eines. RÀumt zugehörige Controls mit weg (s.u.). |
GET /health | Status-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. WeilPOST /panelein Upsert ist, braucht das Update keintypemehr. - 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,togglesind Zustand. Der Wert steht einfach da; wer ihn spĂ€ter liest, bekommt trotzdem die richtige Antwort.buttonist 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 einesbuttonein 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 & Pfad | Zweck |
|---|---|
POST /control | Setzt 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/press | Meldet 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 /control | Liefert 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 schicktPOST /controlâ (4) die Quelle holt sich den neuen Wert perGET /controlin 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/clearentfernt 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

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. Mitstateals 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 perPOST /controlzurĂŒck.toggleâ Schalter (Zustand), Wert0oder1. Optionale Beschriftung beider Stellungen ĂŒberunitim Format"AN|AUS"(defaultON/OFF). Schreibt perPOST /controlzurĂŒck.buttonâ Taster (Ereignis). Der Wert ist ein Druck-ZĂ€hler, kein Flag â siehe Abschnitt 3b.valuebeim Anlegen wird ignoriert, der ZĂ€hler startet immer bei0. Beschriftung kommt auslabel. Schreibt perPOST /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.
| Feld | Typ | Gilt fĂŒr | Bedeutung |
|---|---|---|---|
id | string | alle (Pflicht) | Eindeutiger SchlĂŒssel. Gleiche id = Update in place. |
type | string | erstellen (Pflicht) | label/progress/gauge/plot/slider/number/toggle/button. Nur beim Anlegen nötig. |
value | number|string|number[] | alle | Aktueller Wert. Bei plot: Zahl = anhĂ€ngen, Array = ganzer Frame. Bei toggle: 0/1. Bei button ignoriert â der ZĂ€hler startet immer bei 0. |
group | string | alle | Gruppen-Box. Viewer sortiert Gruppen alphabetisch â Zahlen-PrĂ€fix ("1 Boot", "2 Audio") erzwingt Reihenfolge. |
label | string | alle | Anzeigename der Kachel. |
unit | string | alle | Einheit (z.B. dB, %, ms). Bei toggle: Beschriftung beider Stellungen als `"AN |
min | number | progress/gauge/plot/slider/number | Skala/Range-Untergrenze. |
max | number | progress/gauge/plot/slider/number | Skala/Range-Obergrenze. |
step | number | slider/number | Schrittweite pro Tick (default 1). |
warn | number | gauge/progress | Schwelle â gelbe Zone. |
crit | number | gauge/progress | Schwelle â rote Zone. |
color | string | alle | Accent-Name (cyan/green/orange/purple/âŠ) oder Hex. |
order | number | alle | Sortierung innerhalb der Gruppe. |
state | string | gauge/label | LED-Farbe getrennt vom value-Text (ok/warn/error/âŠ). Farbige LED + lesbarer Text gleichzeitig. |
scale | string | plot | Y-Achse: "linear" (default) | "log". â Sektion 6. |
decimals | number | plot | Nachkommastellen im Footer (0 = ganzzahlig). |
autoMin | boolean | plot | Untergrenze folgt dem Daten-Minimum (Decke bleibt max). â Sektion 6. |
Hinweis:
step,scale,decimals,autoMinsind 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/maxweg, 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
minundmax, 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 aufmax). 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)]
7. Footer (cur/min/max/avg)
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
/panelsmit 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.
statevsvalue: fĂŒr farbige Status-LEDsstate(Farbe) undvalue(Text) trennen.- Controls entkoppeln:
GET /controlim 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.