Wer von Stuttgart aus das Admin-Dashboard oeffnete, sah im Header ploetzlich Karlsruhe - und landete beim Zurueckgehen ueber die Standortauswahl dort. Grund: Dashboard, Hilfe, User-Verwaltung und die MFA-Seiten setzten current_site fest auf get_site_list(cfg)[0], also den global ersten Standort. Der Context-Processor tat dasselbe, fuer angemeldete User sogar unabhaengig davon, ob sie auf diesen Standort ueberhaupt zugreifen duerfen. - remember_site() legt den zuletzt besuchten Standort in der Session ab; aufgerufen aus admin(), admin_priority() und add_customer(). - current_site_for() liefert diesen Standort, sonst den ersten *zugaenglichen*. Verwendet vom Context-Processor und den genannten Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CANCOM Simple Signage
Browserbasiertes Digital-Signage-System für interne Info-Screens.
Features
- Browserbasierter Player (Kiosk-Modus, Chromium, Firefox)
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
- Multi-Standort: Standorte (sites) gruppieren Screens
- Priority-Playlist: pro Standort; die Inhalte laufen auf allen Screens dieses Standorts, nicht standortübergreifend
- Willkommensseite: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder manueller Upload mit automatischem Freistellen (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit
welcome_enabled(mehrere möglich), optional mit Ablaufdatum - Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
- Auto-Reload bei Playlist-Änderungen
- Newsticker pro Screen
- Custom-URL-Button: pro Screen konfigurierbarer Aktions-Button mit frei wählbarer Position (9 Positionen) im Player (öffnet URL in iframe-Overlay mit Zurück-Button oder per Direkt-Weiterleitung)
- Digital Voice Agent: global konfigurierbarer Voice-Agent-Button pro Screen (9 Positionen, iframe-Overlay mit positionsgetreuem Zurück-Button, Typewriter-Tagline mit mehrsprachigen Wechseltexten, optionales Bild über dem Button)
- Stay-on-First: Screen kann auf erstem Playlist-Element stehen bleiben (kein automatischer Refresh)
- Online/Offline-Status: Player-Status wird via dateibasiertem Heartbeat ermittelt – grün (online) bei aktivem Player, rot (offline) nach 60s ohne Poll
- Client-Info: Tooltip beim Hovern über den Online-Button zeigt Browser, Bildschirmauflösung, IP-Adresse und letzten Heartbeat-Zeitpunkt; Info-Tab pro Screen zeigt detaillierte Client-Info-Tabelle
- User-Verwaltung: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort
- Passwort-Workflow: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash
- MFA (Multi-Faktor): Optionale TOTP-Authentifizierung (Google Authenticator, Authy) per User aktivierbar; QR-Code-Scan + Code-Verifikation, 8 Recovery-Codes, Deaktivierung mit Passwort-Bestätigung, Trusted Device (Gerät für 1–30 Tage speichern, überspringt MFA beim nächsten Login)
- Admin Dashboard:
/admin/dashboard– Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf - Aktivitätsverlauf: Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden in
history.jsonprotokolliert und im Dashboard angezeigt - Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info (Tabler Tabs)
- Zeitsteuerung: Pro Playlist-Eintrag (URL und Datei, auch in der Priority-Playlist) ein Zeitfenster festlegen – einmalig (Datum + Uhrzeit), täglich oder wöchentlich (Uhrzeit + optionaler Gültigkeitszeitraum, wöchentlich zusätzlich mit Wochentagen). Fenster über Mitternacht werden unterstützt. Ein "Zeit"-Badge in der Playlist-Zeile zeigt die Einstellung im Tooltip
- Query-Parameter-Passthrough: Per Player-URL übergebene Query-Parameter (z. B.
/player/stuttgart/lobby?customer=acme) können pro Playlist-URL im Edit-Modal aktiviert werden – standardmäßig aus. Link-Icon in der Playlist-Zeile zeigt Status (an/aus) an. - Priority-Seite ebenfalls mit Tabs: Playlist und Medien
- Dark Mode (localStorage-persistiert)
- CI-konformes Admin-UI (CANCOM-Design:
brand-surface,nav-surfacerot)
Architektur
Browser (Player)
│
▼
Flask App (Server)
├── Admin UI /admin/<site>
├── Player UI /player/<site>/<screen>
├── Priority-Seite /admin/<site>/priority
├── config.json
├── users.json
├── history.json
├── /tmp/signage-heartbeat/ (Heartbeat-JSONs mit Client-Info für Online-Status)
├── media/
│ ├── <site>/
│ │ ├── <screen>/
│ │ │ ├── bild.jpg
│ │ │ ├── video.mp4
│ │ │ └── welcome.html
│ │ └── priority/
└── generate_welcome_page.py
- Server: Python 3 + Flask
- Player: Jeder moderne Browser (Chrome Kiosk, Edge, Firefox)
- State:
config.json+ Dateisystem - Frontend: Tabler Core + Tabler Icons + SortableJS (CDN)
Projektstruktur
signage/
├── app.py # Flask-App (alle Routen)
├── generate_welcome_page.py # Logo-Suche + Willkommensseite-Generierung
├── config.json # Persistente Konfiguration
├── users.json # User-Datenbank (gehashte Passwörter)
├── history.json # Aktivitätsverlauf (Dashboard)
├── media/
│ ├── <site>/
│ │ ├── lobby/ # Screen-Ordner (jeder Unterordner = ein Screen!)
│ │ ├── casino/
│ │ ├── videosysteme/
│ │ ├── background.jpg # Hintergrundbild der Willkommensseite (optional)
│ │ ├── priority/ # Priority-Medien dieses Standorts
│ │ ├── logo_1.png # hochgeladene Kundenlogos (Slot 1-3)
│ │ └── welcome.html # Standort-Kopie der Willkommensseite
├── templates/
│ ├── admin.html # Admin-Dashboard (Übersicht)
│ ├── admin_dashboard.html # Admin Dashboard (Statistiken + Verlauf)
│ ├── priority.html # Priority-Playlist (eigene Seite)
│ ├── customer.html # Willkommensseite-Formular
│ ├── player.html # Player-Ansicht
│ ├── login.html
│ ├── change_password.html # Passwort ändern (First-Login / Reset)
│ ├── user_list.html # User-Liste (Admin)
│ ├── user_create.html # User anlegen (Admin)
│ ├── user_edit.html # User bearbeiten (Admin)
│ ├── mfa_setup.html # MFA einrichten (QR-Code + Deaktivierung)
│ ├── mfa_verify.html # TOTP-Code nach Login eingeben
│ ├── mfa_recovery.html # Einmalige Recovery-Codes-Anzeige
│ ├── _header.html # Gemeinsamer Header (MFA-Link + Aktiv-Badge)
│ ├── _footer.html # Gemeinsamer Footer
│ └── _styles.html # Zentrale CSS (Variablen, Dark Mode)
├── static/
│ ├── cancom.svg
│ ├── dva.png
│ └── wallpaper.png
└── AGENTS.md
Installation
Voraussetzungen
- Python ≥ 3.9
- pip
- ffmpeg (optional, für Videokonvertierung)
Setup
pip install -r requirements.txt
Starten
python app.py
Server läuft auf http://localhost:5005.
Docker
Empfohlenes docker-compose.yml mit Bind-Mounts für persistente Daten:
services:
signage:
container_name: signage
build:
context: .
dockerfile: Dockerfile
platforms:
- linux/amd64
image: your-registry/signage:latest
ports:
- "5005:5005"
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- ./media:/app/media
- ./config.json:/app/config.json
- ./users.json:/app/users.json
- ./history.json:/app/history.json
environment:
- TZ=Europe/Berlin
Der Container läuft als User mit UID 1000 ohne Shell, auf read-only Root-FS ohne Capabilities. Daher vor dem ersten Start die Berechtigungen setzen:
chown 1000:1000 config.json users.json history.json
chown -R 1000:1000 media/
Start:
docker compose up -d
Server läuft auf http://localhost:5005 (gunicorn mit 4 Workern, non-root User).
Wichtig: docker compose up -d startet nur neu, es baut nicht. Nach Code-Änderungen:
docker compose up -d --build
Logs
docker compose logs -f signage
Der Container ist so eingestellt, dass dort alles Nötige ankommt:
ENV PYTHONUNBUFFERED=1im Dockerfile – ohne das puffert Pythonstdoutblockweise, sobald es kein Terminal ist, und sämtlicheprint()-Ausgaben der App bleiben unsichtbar.gunicorn --access-logfile -– protokolliert jeden Request. Ohne das ist nicht einmal erkennbar, ob eine Anfrage den Container überhaupt erreicht.
Beides fehlte anfangs und machte die Fehlersuche unnötig schwer.
Routen
| Route | Beschreibung |
|---|---|
GET / |
Weiterleitung zum Login |
GET /login |
Admin-Login |
GET /logout |
Ausloggen |
GET /admin |
Redirect zum ersten konfigurierten Standort |
GET /admin/dashboard |
Admin Dashboard (Statistiken + Verlauf) |
GET /admin/<site> |
Admin-Dashboard für einen Standort |
GET /admin/<site>/priority |
Priority-Playlist (separate Seite) |
POST /admin/<site>/update/<screen> |
Allgemeine Screen-Einstellungen speichern (Intervall, Newsticker, Bilder/Videos, stay_on_first) |
POST /admin/<site>/update-actions/<screen> |
Aktionen-Einstellungen speichern (Custom-URL) |
POST /admin/<site>/update-voice/<screen> |
Voice-Agent-Einstellungen speichern (Enabled, Label, Target, Position, Bild) |
POST /admin/<site>/upload/<screen> |
Medien hochladen |
POST /admin/<site>/add-url/<screen> |
URL zur Playlist hinzufügen |
POST /admin/<site>/delete/<screen>/<filename> |
Datei löschen |
POST /admin/<site>/playlist/<screen> |
Playlist-Reihenfolge speichern (JSON) |
POST /admin/<site>/update-playlist-item/<screen> |
Einzelnen Playlist-Eintrag ändern: URL, Zoom, Passthrough, Zeitsteuerung (JSON) |
POST /admin/<site>/delete-screen/<screen> |
Screen + Medien löschen |
GET /admin/<site>/add-screen?name=<name> |
Neuen Screen anlegen |
GET /add-site?name=<name> |
Neuen Standort anlegen |
POST /admin/<site>/delete-site |
Standort + alle Screens/Medien löschen |
POST /admin/<site>/upload-background |
Hintergrundbild für Willkommensseite hochladen |
POST /admin/<site>/delete-background |
Hintergrundbild zurücksetzen |
GET /player/<site>/<screen> |
Player-Ansicht |
GET /playlist/<site>/<screen>/hash |
Playlist-Checksumme (für Auto-Reload) + Heartbeat (Online-Status) |
GET /willkommen?site=<site> |
Willkommensseite-Formular (GET + POST) |
POST /api/customer |
API-Endpunkt für Willkommensseite (JSON) |
GET /change-password |
Passwort ändern (First-Login / nach Admin-Reset) |
GET /admin/users |
User-Liste (Admin) |
GET /admin/users/create |
User anlegen (Admin) |
GET /admin/users/edit/<email> |
User bearbeiten (Admin) |
GET /mfa/setup |
MFA einrichten / deaktivieren (QR-Code + Verifikation) |
POST /mfa/setup |
TOTP-Code verifizieren und MFA aktivieren |
GET /mfa/verify |
TOTP-Code nach Login eingeben |
POST /mfa/verify |
TOTP-Code prüfen oder Recovery-Code verwenden |
POST /mfa/disable |
MFA deaktivieren (mit Passwort-Bestätigung) |
POST /admin/users/create |
User anlegen (Admin) |
POST /admin/users/delete/<email> |
User löschen (Admin, letzter Admin geschützt) |
POST /admin/users/reset-password/<email> |
Passwort-Reset mit temporärem Passwort (Admin) |
POST /admin/users/edit/<email> |
User bearbeiten speichern (Admin) |
GET /media/<site>/<screen>/<file> |
Medien-Datei ausliefern |
GET /media/<site>/priority/<file> |
Priority-Medien des Standorts |
GET /media/<site>/logos/<file> |
Manuell hochgeladenes Kundenlogo |
GET /admin/help |
Hilfeseite zur Bedienung |
GET /customer |
Alias für /willkommen |
GET /media/<site>/background/<filename> |
Hintergrundbild der Willkommensseite |
Heartbeat / Online-Status
Der Player ruft alle 5s /playlist/<site>/<screen>/hash?w=<breite>&h=<höhe> auf. Der Server speichert dabei ein JSON mit Client-Info (IP, User-Agent, Auflösung) nach /tmp/signage-heartbeat/<site>/<screen>.json. Im Admin-Dashboard wird geprüft, ob der Timestamp < 60s alt ist → grüner "Online"-Button mit Tooltip (Browser, Auflösung, IP, letztes Update) oder roter "Offline"-Button pro Screen.
# Heartbeat-Dateien anzeigen
ls -la /tmp/signage-heartbeat/*/
cat /tmp/signage-heartbeat/stuttgart/lobby.json
Nach einem Server-Neustart werden alle Screens kurz rot, bis die Player wieder pollen (max. 60s).
Admin Portal
http://localhost:5005/admin/<standort>
- Screens konfigurieren (Tab-basiert: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info)
- Info-Tab pro Screen zeigt Client-Info-Tabelle: IP-Adresse, Browser (User-Agent), Bildschirmauflösung und letzter Heartbeat-Zeitpunkt
- Medien hochladen / löschen
- Playlist per Drag & Drop sortieren
- Priority-Playlist verwalten (ebenfalls mit Tabs)
- Willkommensseite generieren (bis zu 3 Kundenlogos, Hintergrundbild pro Standort)
- Custom-URL-Aktionsbutton pro Screen konfigurieren (Position, iframe-Overlay oder Weiterleitung)
- Digital Voice Agent pro Screen konfigurieren (Position, Bild ein/aus, Typewriter-Tagline)
- Stay-on-First-Modus pro Screen (kein automatischer Refresh)
- Standorte anlegen & löschen
- Userverwaltung: User anlegen, bearbeiten, löschen, Passwort-Reset (temporäres Passwort wird angezeigt), MFA-Status pro User in der Liste (grünes "Aktiv"-Badge oder "–")
Admin Dashboard
http://localhost:5005/admin/dashboard
- 5 Statistik-Cards (Standorte, Screens, User, Admins, Superuser) mit ApexCharts-Sparkline + Trendanzeige
- Aktivitätsverlauf (History) als Liste mit Zeitstempel, Aktion und User
- Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden protokolliert
Player-URL
http://localhost:5005/player/<standort>/<screen>
Beispiel:
http://localhost:5005/player/stuttgart/lobby
Query-Parameter-Passthrough
Die Player-URL akzeptiert beliebige Query-Parameter, z. B. ?customer=acme&token=xyz. Ob diese Parameter an eine Playlist-URL weitergegeben werden, wird pro URL im Admin-Playlist-Edit-Modal gesteuert (Checkbox "Query-Parameter an diese URL übergeben", standardmäßig aus). In der Playlist-Zeile zeigt ein Link-Icon (ti-link, grau) den Status an: sichtbar = an, durchgestrichen (ti-link-off, hellgrau) = aus.
Beispiel:
/player/stuttgart/lobby?customer=acme&token=xyz
- URL in Playlist mit aktiviertem Passthrough:
https://dashboard.acme.com/status?customer=acme&token=xyz - URL in Playlist mit deaktiviertem Passthrough:
https://dashboard.acme.com/status
Willkommensseite
http://localhost:5005/willkommen?site=stuttgart
Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → welcome.html wird in den Playlists aller Zielscreens vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf static/wallpaper.png).
Zielscreens: Jeder Screen mit welcome_enabled (Schalter „Willkommensseite hier anzeigen" im Tab Einstellungen). Besitzt kein Screen des Standorts den Schlüssel, greift das alte Verhalten und die Seite geht nur in lobby.
Der Schalter wirkt sofort: Beim Einschalten wird eine bereits vorhandene Willkommensseite unmittelbar in die Playlist des Screens gesetzt, beim Ausschalten wieder entfernt (samt Datei) – ohne erneutes Generieren. Die Seite ist je Standort identisch, ein zweiter Handgriff wäre also nur Aufwand ohne anderes Ergebnis. Beim Generieren wird zusätzlich eine Standort-Kopie unter media/<site>/welcome.html abgelegt, die nie gelöscht wird; dadurch findet der Schalter auch dann noch eine Quelle, wenn die Seite zwischenzeitlich auf allen Screens abgeschaltet war.
Logo-Upload: Findet die automatische Suche kein Logo (oder passt es nicht), kann pro Kunde eine Bilddatei hochgeladen werden. Ein Upload hat Vorrang vor der Suche. Die Datei wird als media/<site>/logo_<slot>.png abgelegt und über /media/<site>/logos/<datei> ausgeliefert. Bewusst als Datei im Standort-Verzeichnis, nicht in einem Unterordner: admin() behandelt jeden Unterordner von media/<site>/ als Screen und schreibt ihn dauerhaft in die Konfiguration – ein Ordner logos würde also als Phantom-Screen in der Oberfläche auftauchen. Das Hintergrundbild liegt aus demselben Grund als media/<site>/background.*. Der Ordnername logos ist zusätzlich reserviert und wird bei der Screen-Erkennung übersprungen.
Freistellen und Zuschneiden: Beim Upload wird der Hintergrund automatisch entfernt, damit das Logo nicht in einem weissen Kasten auf dem Hintergrundbild steht. Anschliessend werden die durchsichtigen Ränder weggeschnitten (trim_transparent_border). Ohne diesen Schnitt erscheint ein hochgeladenes Logo kleiner als ein automatisch gefundenes: die Willkommensseite skaliert mit object-fit: contain auf eine feste Höhe von 120px, und ein breiter Rand zählt dabei mit – Brandfetch-Logos sind bereits eng beschnitten. Verfahren: Flood Fill von den Bildrändern her (bewusst kein globaler Farbvergleich – so bleibt Weiss innerhalb des Logos erhalten), weiche Kante gegen Treppeneffekte, Speicherung immer als PNG. Bereits transparente Bilder werden unverändert übernommen; ist der Bildrand nicht einfarbig, bleibt das Bild unangetastet und die Oberfläche weist darauf hin. Dasselbe gilt, wenn beim Freistellen fast nichts übrig bliebe – ein helles Logo auf hellem Grund liegt innerhalb der Farbtoleranz und würde sonst vollständig verschwinden, was auf der Willkommensseite als „kein Logo" ankäme. Mit der Checkbox „Hintergrund behalten" lässt sich das Freistellen pro Kunde abschalten. Umgesetzt mit Pillow – bewusst ohne ML-Modell, da der Container mit read-only Root-FS läuft und kein Modell-Cache zur Verfügung steht.
Anzeigen bis: Optionales Datum im Formular. Ist es gesetzt, bekommt der Eintrag eine Zeitsteuerung (daily 00:00–23:59 mit valid_until) und verschwindet danach auf allen Screens von selbst.
Ansprechpartner: Pro Kunde können Kontaktdaten eingegeben werden (Markdown-Format).
- Fett:
**Text** - Kursiv:
*Text* Code:`Code`- Aufzählungen:
- Punktoder* Punkt - Zeilenumbrüche werden als
<br>übernommen
Layout:
- Landscape: Logos nebeneinander (280px breit, 120px hoch), Texte linksbündig darunter
- Portrait: Texte links vom Logo, per
order-CSS gesteuert
OpenAI API-Key
Die Logo-Suche via OpenAI benötigt einen API-Key. Diesen als Umgebungsvariable setzen:
export OPENAI_API_KEY=sk-...
Oder via .env-Datei (wird von Docker Compose automatisch geladen):
OPENAI_API_KEY=sk-...
Globale Konfiguration (config.json)
| Feld | Typ | Beschreibung |
|---|---|---|
server_url |
string | Öffentliche Server-URL für Player-Links |
voice_agent_url |
string | URL des Digital Voice Agents (global, read-only im Admin) |
User-Rollen (users.json)
| Rolle | Zugriff | Userverwaltung |
|---|---|---|
| Admin | Alle Standorte | Ja (anlegen/bearbeiten/löschen/Reset) |
| Superuser | Alle Standorte | Nein |
| User | Nur freigegebene Standorte | Nein |
- Usernamen sind E-Mail-Adressen
- Passwörter werden als scrypt-Hash gespeichert
users.jsonwird beim ersten Start ausconfig.json.adminbefüllt- Notfall:
users.jsonlöschen → Server-Neustart erzeugt neuen Admin ausconfig.json
User-Felder (users.json)
| Feld | Typ | Beschreibung |
|---|---|---|
password_hash |
string | scrypt-Hash des Passworts |
role |
string | admin, superuser oder user |
sites |
array | Freigegebene Standorte (nur für User-Rolle) |
must_change_password |
bool | Erzwingt Passwort-Änderung beim nächsten Login |
first_name / last_name |
string | Vor- und Nachname |
department |
string | Abteilung |
notes |
string | Interne Notizen |
mfa_enabled |
bool | MFA aktiv (TOTP) |
mfa_secret |
string | TOTP-Secret (sofort beim ersten GET /mfa/setup persistiert) |
mfa_recovery_codes |
array | 8 Recovery-Codes (hex, 7 Stellen, einmalig verwendbar) |
{
"server_url": "http://signage.ccmake.de",
"voice_agent_url": "https://voice-agent.example.com",
"admin": { "username": "...", "password": "..." },
"sites": {
"stuttgart": {
"screens": {
"lobby": {
"playlist": [...],
"interval": 10,
"show_images": true,
"show_videos": true,
"newsticker_text": "...",
"newsticker_enabled": false,
"custom_url_enabled": false,
"custom_url": "https://...",
"custom_url_label": "Infos",
"custom_url_target": "overlay",
"custom_url_position": "top-left",
"stay_on_first": false,
"voice_agent_enabled": true,
"voice_agent_show_image": false,
"voice_agent_label": "Digitaler Assistent",
"voice_agent_target": "overlay",
"voice_agent_position": "top-left"
},
"casino": { "playlist": [...], "interval": 15 }
},
"welcome_data": {
"names": ["Firma A"],
"logo_urls": ["https://..."],
"background_url": null
}
}
}
}
Screen-Konfiguration (pro Screen in config.json)
| Feld | Typ | Beschreibung |
|---|---|---|
playlist |
Array | Playlist-Einträge (Strings oder Dicts, siehe Playlist-Einträge) |
interval |
int | Anzeige-Intervall in Sekunden |
show_images |
bool | Bilder anzeigen |
show_videos |
bool | Videos anzeigen |
newsticker_text |
string | Text für Newsticker (max. 200 Zeichen) |
newsticker_enabled |
bool | Newsticker anzeigen |
custom_url_enabled |
bool | Custom-URL-Button im Player anzeigen |
custom_url |
string | URL des Aktions-Buttons |
custom_url_label |
string | Button-Beschriftung |
custom_url_target |
string | "overlay" (iframe) oder "redirect" |
custom_url_position |
string | Position des Buttons: top-left, top-center, top-right, middle-left, middle-center, middle-right, bottom-left, bottom-center, bottom-right |
stay_on_first |
bool | Player bleibt auf erstem Element stehen (kein Refresh) |
welcome_enabled |
bool | Willkommensseite wird auf diesem Screen vorne in die Playlist gesetzt |
voice_agent_enabled |
bool | Voice-Agent-Button im Player anzeigen |
voice_agent_show_image |
bool | Bild (dva.png) über dem Button anzeigen |
voice_agent_label |
string | Button-Beschriftung (Default: "Digitaler Assistent") |
voice_agent_target |
string | "overlay" (iframe) oder "redirect" |
voice_agent_position |
string | Position des Buttons: top-left, top-center, top-right, middle-left, middle-center, middle-right, bottom-left, bottom-center, bottom-right |
Priority-Playlist (pro Standort)
Liegt in config.json unter sites.<site>.priority, die Medien unter media/<site>/priority/:
"sites": {
"stuttgart": {
"priority": {
"enabled": true, // schaltet die Priority-Playlist dieses Standorts
"playlist": [ ... ] // gleiches Eintragsformat wie eine Screen-Playlist
},
"screens": { ... }
}
}
Die Einträge laufen auf allen Screens dieses Standorts, eingestreut zwischen den normalen Playlist-Einträgen. Ein anderer Standort ist davon nicht betroffen.
Früher lag die Priority-Playlist global unter
config.priorityund wirkte auf alle Standorte.get_priority_config()übernimmt einen solchen Altbestand beim ersten Zugriff einmalig in den jeweiligen Standort.
Die Ordnernamen priority und logos sind unter media/<site>/ reserviert und werden bei der Screen-Erkennung übersprungen – sonst würden sie als Screens in der Oberfläche auftauchen.
Playlist-Einträge
Ein Eintrag ist entweder ein String (Dateiname oder URL, Altbestand) oder ein Dict:
// URL-Eintrag
{
"url": "https://example.com",
"zoom": 1.0, // Zoom-Faktor im Player
"enabled": true, // Checkbox in der Playlist
"passthrough_params": false, // Query-Parameter der Player-URL durchreichen
"schedule": { ... } // optional, siehe unten
}
// Datei-Eintrag
{
"name": "bild.jpg",
"enabled": true,
"schedule": { ... } // optional
}
Zeitsteuerung (schedule)
Optional pro Eintrag. Ohne schedule läuft der Eintrag immer (sofern enabled).
| Feld | Typ | Beschreibung |
|---|---|---|
mode |
string | "once", "daily" oder "weekly" |
start / end |
string | Bei once voller Zeitstempel ("2026-09-10T08:00"), bei daily/weekly nur die Uhrzeit ("08:00") |
days |
Array | Nur bei weekly: Wochentage als Strings, "0" = Montag … "6" = Sonntag |
valid_from |
string | Optional, nur bei daily/weekly: Datum ("2026-09-10"), ab dem das Fenster gilt – inklusiv |
valid_until |
string | Optional, nur bei daily/weekly: Datum, bis zu dem das Fenster gilt – inklusiv |
// Täglich 08:00–12:00, nur vom 10.09. bis 20.09.2026
{ "mode": "daily", "start": "08:00", "end": "12:00",
"valid_from": "2026-09-10", "valid_until": "2026-09-20" }
// Mo/Mi/Fr von 09:00 bis 17:00, unbefristet
{ "mode": "weekly", "start": "09:00", "end": "17:00", "days": ["0", "2", "4"] }
// Einmalig, durchgehend über mehrere Tage
{ "mode": "once", "start": "2026-09-10T08:00", "end": "2026-09-20T12:00" }
Verhaltensregeln:
- Bei
dailyundweeklyzählt nur die Uhrzeit vonstart/end. Ein Gültigkeitszeitraum wird übervalid_from/valid_untilgesetzt, nicht über den Datumsanteil. - Bei
dailysind alle Wochentage gemeint;dayswird in diesem Modus nicht ausgewertet. - Ist
start>end, läuft das Fenster über Mitternacht (z. B.22:00–02:00). Ein solches Fenster gehört zum Wochentag bzw. Gültigkeitstag seines Starts: „Mi 22:00–02:00" läuft Mittwochabend bis Donnerstagfrüh. - Unlesbare oder unvollständige Zeitangaben blenden den Eintrag nicht aus – ein Konfigurationsfehler soll nicht dazu führen, dass stillschweigend nichts mehr läuft.
_schedule_time()liest neben"08:00"auch volle ISO-Zeitstempel, damit Einträge aus älteren Versionen unverändert weiterlaufen.- Der Aktiv-Zustand fliesst in
/playlist/<site>/<screen>/hashein, damit der Player beim Öffnen und Schliessen eines Zeitfensters von selbst neu lädt.
Helper-Funktionen (app.py)
load_config()/save_config()– JSON lesen/schreibenget_site_list(cfg)– alle Standorte sortiertget_screen_config(cfg, site, screen)– Screen-Konfiguration mit Defaultsis_url(item)– prüft ob Item eine URL istnormalize_url(item)– normalisiert URL-Item zu{"url", "zoom"}playlist_item_name(item)– Name aus String oder Dict extrahierenis_item_active(item)– prüftenabledund die Zeitsteuerung_schedule_time(value)– Uhrzeit aus"HH:MM"oder vollem ISO-Zeitstempel lesen_window_start_date(now, start, end)– Kalendertag, an dem das laufende Zeitfenster begann (None= ausserhalb)_in_validity_range(schedule, day)– optionalen Gültigkeitszeitraum prüfenschedule_text(schedule)– Jinja-Filter: Klartext einer Zeitsteuerung für Tooltipsload_priority_files(site)– Priority-Playlist eines Standorts + Dateien ladenget_priority_config(cfg, site)– Priority-Konfiguration des Standorts (übernimmt einmalig einen globalen Altbestand)priority_dir(site)– Pfad zumedia/<site>/priority/welcome_target_screens(config, site)– Zielscreens der Willkommensseite (Fallbacklobby)save_uploaded_logo(file, site, slot, freistellen=True)– hochgeladenes Logo freistellen und als PNG ablegenremove_logo_background(image)– Hintergrund per Flood Fill entfernen, gibt(bild, hinweis)zurücktrim_transparent_border(img)– durchsichtige Ränder wegschneiden, damit das Logo den Anzeigebereich ausfülltsync_welcome_for_screen(config, site, screen, enabled)– Willkommensseite beim Umlegen des Schalters platzieren bzw. entfernenfind_welcome_source(site, config)– vorhandenewelcome.htmlals Kopiervorlage findenprio_redirect(site)– Redirect-Pfad zur Priority-Seiteget_background_url(site)– URL zum benutzerdefinierten Hintergrundbild oderNoneload_users()/save_users()/get_user()– User aususers.jsoninit_user_db()– initialen Admin ausconfig.jsonanlegenget_accessible_sites(cfg, user)– Standorte nach User-Berechtigung filternrecord_heartbeat(site, screen, ip, ua, resolution)– Heartbeat-JSON mit Client-Info schreibenscreen_is_active(site, screen)– Prüft ob Heartbeat < 60s alt istget_client_info(site, screen)– Gibt gespeicherte Client-Info zurück (Dict mit ip, ua, resolution, last_seen)admin_required/site_access_required– Zugriffs-Dekoratoren
Dark Mode
Wird über localStorage("signage-theme") persistiert. Umschalt-Button im Header. CSS-Variablen --ccm-* in _styles.html.
Sicherheit
- Admin-Bereich per Flask-Login geschützt (E-Mail + Passwort)
- Optionale MFA (TOTP) per User aktivierbar (Google Authenticator, Authy)
- 8 Recovery-Codes pro User, einmalig bei Aktivierung angezeigt
- Passwörter gehasht (scrypt) in
users.json - Drei Rollen: Admin (alle Standorte + Userverwaltung), Superuser (alle Standorte), User (nur zugewiesene Standorte)
- Zugriffskontrolle per Decorator (
@admin_required,@site_access_required) - Letzter Admin kann nicht gelöscht werden
- Player-Seiten Read-Only
- Externe Nutzung via Reverse Proxy + TLS empfohlen
Git & Medien
Das Repository ist für Code gedacht, nicht für Medien.
media/ und Medien-Dateiendungen sind in .gitignore ausgeschlossen.
Video-Empfehlungen
- Format: MP4 (H.264)
- Auflösung: max. 1920×1080
ffmpeg -i input.mov -c:v libx264 -pix_fmt yuv420p -movflags +faststart output.mp4
Maintainer
CANCOM Simple Signage – Interne Lösung, nicht für externe Weitergabe bestimmt.