- User kann nach TOTP-Eingabe das Gerät für 1–30 Tage speichern - Cookie mfa_trust mit zufälligem Token, gespeichert in trusted_devices[] - Nächstes POST /login: Cookie prüfen → gültiges Token → MFA überspringen - Abgelaufene Einträge werden beim Login automatisch bereinigt - Logout + MFA-Deaktivierung löschen Cookie + trusted_devices - mfa_verify.html: checkbox + days-select (1/3/7/14/30 Tage)
19 KiB
Executable File
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: globale Inhalte wirken auf alle Player
- Willkommensseite: bis zu 3 Kundenlogos via OpenAI + Brandfetch, pro Standort konfigurierbares Hintergrundbild
- 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)
- 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/
│ │ ├── casino/
│ │ └── videosysteme/
│ └── priority/
├── 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).
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>/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/priority/<file> |
Priority-Medien (global) |
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
Willkommensseite
http://localhost:5005/willkommen?site=stuttgart
Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → welcome.html wird in der Lobby-Playlist vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf static/wallpaper.png).
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
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
}
}
},
"priority": {
"enabled": true,
"playlist": [...]
}
}
Screen-Konfiguration (pro Screen in config.json)
| Feld | Typ | Beschreibung |
|---|---|---|
playlist |
Array | Playlist-Einträge (Strings oder Dicts mit url/zoom) |
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) |
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 |
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 extrahierenplaylist_item_enabled(item)– Enabled-Status prüfenload_priority_files()– Priority-Playlist + Dateien ladenprio_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.