Warum ein eigenes Dashboard
Der Power-BI-Bericht braucht Power BI Desktop zum Öffnen oder ein Konto zum Teilen. Ein Dashboard im Browser braucht nur eine Adresse: hotel.butscher.cloud (dieselbe Anwendung läuft auch auf Render unter hotel-dashboard-cuoi.onrender.com). Dafür muss jemand programmieren – und genau das macht es zum Lehrstück: Jede Kennzahl ist eine SQL-Abfrage, die man sich im Dashboard anzeigen lassen kann („SQL anzeigen“ unter jedem Diagramm), jedes Diagramm eine Funktion, jede Gestaltungsentscheidung nachlesbar.
Power-BI-Bericht
- klicken statt programmieren
- Daten importiert, Stand vom Laden
- öffnen mit Desktop, teilen mit Konto
- Measures in DAX
Dashboard
- programmiert in Python und JavaScript
- liest bei jedem Filterwechsel live aus der Datenbank
- öffentliche Adresse, kein Konto, kein Programm
- Kennzahlen in SQL, sichtbar unter jedem Diagramm
Beide zeigen dieselben Zahlen aus derselben Datenbank. Wer beides gebaut hat, weiß, was ein BI-Werkzeug abnimmt – und was es verbirgt.
Aufbau: Backend und Frontend
Das Dashboard folgt einer Arbeitsteilung, die für Webanwendungen üblich ist: Python liefert Zahlen als JSON, JavaScript zeichnet. Das Backend kennt die Datenbank und die Kennzahlen; das Frontend kennt den Filterzustand und die Diagramme. Beide sprechen über Routen miteinander.
| Datei | Aufgabe |
|---|---|
backend/app/main.py | FastAPI-Anwendung: eine Route je Diagramm, alle mit denselben Filterparametern; liefert das Frontend als statische Dateien aus |
backend/app/abfragen.py | eine SQL-Abfrage je Route, aus einem gemeinsamen Sternschema-Block und Kennzahlen-Block zusammengesetzt |
backend/app/filter.py | macht aus den Filterparametern eine parametrisierte WHERE-Klausel – nie Text in SQL einsetzen |
backend/app/datenbank.py | Verbindung über DATABASE_URL; fehlt sie, gilt die Leserolle der Vorlesung |
frontend/app.js | Filterzustand in der Adresse, lädt die Routen parallel, bereitet die Daten auf, zeichnet alle Abschnitte |
frontend/diagramme.js | ein Diagramm je Funktion mit Observable Plot: Balken, Balkenpaar, Säulen mit Vorjahresversatz, Mini-Säulen |
frontend/texte.js | Aussagen, Interpretationen und Handlungsempfehlungen, aus den Daten formuliert |
Was bei einem Filterwechsel passiert
- Klick auf „City Hotel“ – das Frontend schreibt
?hotel=City%20Hotelin die Adresse. - Es ruft alle Routen mit diesem Parameter auf, parallel:
/api/kennzahlen?hotel=…,/api/monate?…… - Das Backend baut je Route eine SQL-Abfrage mit WHERE-Klausel und schickt sie an die Datenbank.
- PostgreSQL rechnet über die gefilterten Buchungen; das Backend liefert das Ergebnis als JSON.
- Das Frontend zeichnet alle Abschnitte neu – Kacheln, Tabellen, Diagramme, Texte.
Weil der Filterzustand in der Adresse steht, lässt sich jede Ansicht als Link teilen: „Link mit Filterzustand kopieren“.
Die Routen
Eine Route ist eine Adresse, unter der das Backend antwortet. Jede Route des Dashboards
liefert ein JSON-Objekt mit drei Teilen: daten (die Zeilen), sql (die Abfrage, die
gelaufen ist) und parameter (die Filter). Genau das zeigt „SQL anzeigen“ unter jedem Diagramm.
Die Zahlen aus /api/hotels sind Rohwerte: Summen und Zähler. Quoten und Mittelwerte rechnet das
Frontend aus Zähler und Nenner – deshalb kann es „beide Hotels“ korrekt bilden (Lab 06: Mittelwerte lassen
sich nicht addieren, Zähler und Nenner schon).
Gestaltungsregeln
Das Dashboard folgt Regeln, die vier Autoren geprägt haben: Edward Tufte (wenig Tinte ohne Daten), Stephen Few (Dashboards, die man auf einen Blick liest), Rolf Hichert (IBCS: einheitliche Notation, Aussage im Titel) und Nicolas Bissantz (Kennzahlen untereinander, Farbe als Werturteil). Jede Regel hat im Dashboard eine sichtbare Umsetzung – und jede wurde in einer Überarbeitungsrunde erkämpft.
| Regel | Quelle | Umsetzung im Dashboard |
|---|---|---|
| Aussage zuerst | IBCS, Datawrapper | Jeder Titel nennt den Befund mit Zeitbezug („Mai 2017 war der stärkste Anreisemonat: 6.313 Buchungen“); darunter eine Zeile mit Messgröße, Einheit, Zeitraum, Filter. Nur zwei Textebenen. |
| Gleiche Skalen, wo verglichen wird | Bissantz | Die vier Stornoquoten-Diagramme teilen eine Skala von 0 bis 100 %; Kennzahlen mit verschiedenen Einheiten stehen untereinander auf einer Zeitachse, nie nebeneinander. |
| Wenig Tinte ohne Daten | Tufte | Keine Rahmen, keine Legenden, Werte direkt am Balken, Gitterlinien nur bei Zeitreihen. |
| Fluchten | Few, Bissantz | Alle Balkendiagramme haben dieselbe Beschriftungsbreite, sodass Balken und Werte über die Seite hinweg in einer Linie stehen. |
| Vorjahresvergleich als Versatz | IBCS | Der Vorjahresmonat steht als graue Säule versetzt hinter der Ist-Säule; die Differenz trägt den Signalton. Hohle Säulen bleiben Plan- und Prognosewerten vorbehalten. |
| Farbe als Werturteil | Bissantz (DeltaMaster) | Blau = gut für das Ergebnis, Rot = zu seinen Lasten, Grau = ohne Wertung; keine Farbe für Vergleiche ohne Ergebniswirkung. |
| Klarnamen statt Kürzel | Few | „Reisebüro online“ statt „Online TA“, Ländernamen statt ISO-Codes; die Datenbank behält die Originalwerte. |
| Grafische Tabellen | Bissantz | Zahlen rechtsbündig, Datenbalken relativ zum Spaltenmaximum, Mini-Säulen mit gleicher Zeitachse, sortierbare Spaltenköpfe. |
Was das Dashboard bewusst nicht hat: Tortendiagramme, eine Landkarte, Liniendiagramme mit verschiedenen Skalen nebeneinander, Prozentwerte mit Nachkommastellen in Diagrammen, eine Farbe je Kategorie. Jedes davon war einmal drin und wurde aus einem benennbaren Grund wieder entfernt.
Container und Render
Damit das Dashboard öffentlich läuft, braucht es einen Server, auf dem Python, die Pakete und der Code liegen
und der auf Anfragen antwortet. Docker packt das alles in ein Abbild (Image), das überall gleich
läuft; Render baut dieses Abbild aus dem Repository und betreibt es als Web Service. Zwei Dateien steuern das: das Dockerfile und der Blueprint render.yaml.
Drei Dinge, die ein Web Service bei Render braucht
- Etwas, das läuft: ein Server, der auf Anfragen wartet – hier uvicorn mit der FastAPI-Anwendung. Ein Skript, das durchläuft und endet, ist kein Web Service.
- Einen Port, den Render sieht: Der Server muss an
0.0.0.0lauschen, nicht an127.0.0.1, und den Port aus der UmgebungsvariablenPORTnehmen. Sonst: „No open ports detected“. - Die Konfiguration im Repository:
render.yamlals Blueprint nennt Laufzeit (Docker), Root Directory (dashboard), Plan (Free) und Health-Check-Pfad (/health). Jeder Push aufmainlöst einen neuen Deploy aus.
Free-Plan heißt: Der Dienst schläft nach 15 Minuten ohne Zugriff ein; der erste Aufruf danach dauert bis zu einer Minute. Für ein Lehrprojekt ist das in Ordnung, für einen Bericht, den die Geschäftsführung morgens um acht öffnet, nicht. Deshalb läuft dasselbe Docker-Abbild zusätzlich auf einem eigenen Server unter hotel.butscher.cloud – ohne Schlafpause, dafür mit eigenem Betrieb. Stand 09/2026.
Stolpersteine beim Deploy
Der Server lauscht auf 127.0.0.1 – erreichbar nur aus dem Container selbst. Render findet
keinen Port und bricht ab. Abhilfe: --host 0.0.0.0.
--port 8000 statt --port $PORT: Render findet den Port trotzdem, warnt aber – der Weg
ist zerbrechlich, weil Render den Port vorgibt. Im Dockerfile des Projekts: ${PORT:-8000}, also
Render-Port, sonst 8000.
Der Start Command ruft ein Skript auf, das rechnet und endet. Kein Fehler, aber kein Server. Web Services müssen laufen bleiben; für Skripte gibt es Background Worker und Cron Jobs.
Der Code liest eine Umgebungsvariable, die niemand gesetzt hat. Das Dashboard hat dafür einen Rückfall auf die Leserolle; ein Projekt ohne Rückfall braucht die Variable im Render-Formular.
Laufzeit Docker, aber das Dockerfile liegt in dashboard/, nicht im Wurzelverzeichnis. Abhilfe:
Root Directory auf dashboard setzen – so steht es in render.yaml.
Nach einem Deploy zeigt der Browser das alte JavaScript aus seinem Zwischenspeicher. Das Dashboard schickt
deshalb Cache-Control: no-cache und hängt an jede Skriptadresse eine Versionsnummer, die bei
jeder Änderung hochgezählt wird.
Spielplatz
Das Formular „New Web Service“ von Render, nachgebildet, daneben das Repository swrobuts/hotel mit
den Dateien, auf die es ankommt. Probieren Sie aus, was passiert, wenn das Root Directory fehlt, der Host falsch
ist oder ein Skript statt eines Servers startet – das Protokoll ist dem echten nachempfunden.
Übungen
Fünf Übungen: die Gestaltungsregeln ihren Umsetzungen zuordnen, Verständnisfragen zum Aufbau, zwei Deploys im Simulator – einmal als Docker-Dienst wie im Projekt, einmal als Python-Dienst mit Start Command – und eine Routenantwort als JSON.
Zusammenfassung
- Das Dashboard zeigt dieselben Zahlen wie Power BI, aber programmiert, öffentlich und mit sichtbarem SQL.
- Arbeitsteilung: Python liefert JSON über Routen, JavaScript zeichnet; der Filterzustand steht in der Adresse.
- Jede Route antwortet mit daten, sql und parameter – das Backend rechnet Rohwerte, das Frontend Quoten.
- Gestaltung nach Tufte, Few, IBCS und Bissantz: Aussage im Titel, gleiche Skalen, wenig Tinte, Fluchten, Vorjahresversatz, Farbe als Werturteil.
- Docker packt, Render betreibt; render.yaml nennt Laufzeit, Root Directory, Plan und Health-Check.
- Ein Web Service muss laufen bleiben, an 0.0.0.0 lauschen und den Port aus $PORT nehmen.