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.

BrowserFrontend: app.js, diagramme.js, texte.js
→ /api/… →
BackendFastAPI: main.py, abfragen.py, filter.py
→ SQL →
Datenbankhotel_bi als studi_hotel
DateiAufgabe
backend/app/main.pyFastAPI-Anwendung: eine Route je Diagramm, alle mit denselben Filterparametern; liefert das Frontend als statische Dateien aus
backend/app/abfragen.pyeine SQL-Abfrage je Route, aus einem gemeinsamen Sternschema-Block und Kennzahlen-Block zusammengesetzt
backend/app/filter.pymacht aus den Filterparametern eine parametrisierte WHERE-Klausel – nie Text in SQL einsetzen
backend/app/datenbank.pyVerbindung über DATABASE_URL; fehlt sie, gilt die Leserolle der Vorlesung
frontend/app.jsFilterzustand in der Adresse, lädt die Routen parallel, bereitet die Daten auf, zeichnet alle Abschnitte
frontend/diagramme.jsein Diagramm je Funktion mit Observable Plot: Balken, Balkenpaar, Säulen mit Vorjahresversatz, Mini-Säulen
frontend/texte.jsAussagen, Interpretationen und Handlungsempfehlungen, aus den Daten formuliert

Was bei einem Filterwechsel passiert

  1. Klick auf „City Hotel“ – das Frontend schreibt ?hotel=City%20Hotel in die Adresse.
  2. Es ruft alle Routen mit diesem Parameter auf, parallel: /api/kennzahlen?hotel=…, /api/monate?…
  3. Das Backend baut je Route eine SQL-Abfrage mit WHERE-Klausel und schickt sie an die Datenbank.
  4. PostgreSQL rechnet über die gefilterten Buchungen; das Backend liefert das Ergebnis als JSON.
  5. 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.

RegelQuelleUmsetzung im Dashboard
Aussage zuerstIBCS, DatawrapperJeder 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 wirdBissantzDie vier Stornoquoten-Diagramme teilen eine Skala von 0 bis 100 %; Kennzahlen mit verschiedenen Einheiten stehen untereinander auf einer Zeitachse, nie nebeneinander.
Wenig Tinte ohne DatenTufteKeine Rahmen, keine Legenden, Werte direkt am Balken, Gitterlinien nur bei Zeitreihen.
FluchtenFew, BissantzAlle Balkendiagramme haben dieselbe Beschriftungsbreite, sodass Balken und Werte über die Seite hinweg in einer Linie stehen.
Vorjahresvergleich als VersatzIBCSDer 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 WerturteilBissantz (DeltaMaster)Blau = gut für das Ergebnis, Rot = zu seinen Lasten, Grau = ohne Wertung; keine Farbe für Vergleiche ohne Ergebniswirkung.
Klarnamen statt KürzelFew„Reisebüro online“ statt „Online TA“, Ländernamen statt ISO-Codes; die Datenbank behält die Originalwerte.
Grafische TabellenBissantzZahlen 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.0 lauschen, nicht an 127.0.0.1, und den Port aus der Umgebungsvariablen PORT nehmen. Sonst: „No open ports detected“.
  • Die Konfiguration im Repository: render.yaml als Blueprint nennt Laufzeit (Docker), Root Directory (dashboard), Plan (Free) und Health-Check-Pfad (/health). Jeder Push auf main lö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

No open ports detected

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.

Fester Port

--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.

Exited with status 0

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.

KeyError: 'DATABASE_URL'

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.

Dockerfile nicht gefunden

Laufzeit Docker, aber das Dockerfile liegt in dashboard/, nicht im Wurzelverzeichnis. Abhilfe: Root Directory auf dashboard setzen – so steht es in render.yaml.

Alte Dateien im Browser

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