# 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.json` protokolliert und im Dashboard angezeigt - Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info (Tabler Tabs) - **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-surface` rot) --- ## Architektur ``` Browser (Player) │ ▼ Flask App (Server) ├── Admin UI /admin/ ├── Player UI /player// ├── Priority-Seite /admin//priority ├── config.json ├── users.json ├── history.json ├── /tmp/signage-heartbeat/ (Heartbeat-JSONs mit Client-Info für Online-Status) ├── media/ │ ├── / │ │ ├── / │ │ │ ├── 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/ │ ├── / │ │ ├── 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 ```bash pip install -r requirements.txt ``` ### Starten ```bash python app.py ``` Server läuft auf `http://localhost:5005`. ### Docker Empfohlenes `docker-compose.yml` mit Bind-Mounts für persistente Daten: ```yaml 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: ```bash chown 1000:1000 config.json users.json history.json chown -R 1000:1000 media/ ``` Start: ```bash 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/` | Admin-Dashboard für einen Standort | | `GET /admin//priority` | Priority-Playlist (separate Seite) | | `POST /admin//update/` | Allgemeine Screen-Einstellungen speichern (Intervall, Newsticker, Bilder/Videos, stay_on_first) | | `POST /admin//update-actions/` | Aktionen-Einstellungen speichern (Custom-URL) | | `POST /admin//update-voice/` | Voice-Agent-Einstellungen speichern (Enabled, Label, Target, Position, Bild) | | `POST /admin//upload/` | Medien hochladen | | `POST /admin//add-url/` | URL zur Playlist hinzufügen | | `POST /admin//delete//` | Datei löschen | | `POST /admin//playlist/` | Playlist-Reihenfolge speichern (JSON) | | `POST /admin//delete-screen/` | Screen + Medien löschen | | `GET /admin//add-screen?name=` | Neuen Screen anlegen | | `GET /add-site?name=` | Neuen Standort anlegen | | `POST /admin//delete-site` | Standort + alle Screens/Medien löschen | | `POST /admin//upload-background` | Hintergrundbild für Willkommensseite hochladen | | `POST /admin//delete-background` | Hintergrundbild zurücksetzen | | `GET /player//` | Player-Ansicht | | `GET /playlist///hash` | Playlist-Checksumme (für Auto-Reload) + Heartbeat (Online-Status) | | `GET /willkommen?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/` | 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/` | User löschen (Admin, letzter Admin geschützt) | | `POST /admin/users/reset-password/` | Passwort-Reset mit temporärem Passwort (Admin) | | `POST /admin/users/edit/` | User bearbeiten speichern (Admin) | | `GET /media///` | Medien-Datei ausliefern | | `GET /media/priority/` | Priority-Medien (global) | | `GET /media//background/` | Hintergrundbild der Willkommensseite | ### Heartbeat / Online-Status Der Player ruft alle 5s `/playlist///hash?w=&h=` auf. Der Server speichert dabei ein JSON mit Client-Info (IP, User-Agent, Auflösung) nach `/tmp/signage-heartbeat//.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. ```bash # 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/ ``` - 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// ``` 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 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: `- Punkt` oder `* Punkt` - Zeilenumbrüche werden als `
` ü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.json` wird beim ersten Start aus `config.json.admin` befüllt - **Notfall**: `users.json` löschen → Server-Neustart erzeugt neuen Admin aus `config.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) | ```json { "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/schreiben - `get_site_list(cfg)` – alle Standorte sortiert - `get_screen_config(cfg, site, screen)` – Screen-Konfiguration mit Defaults - `is_url(item)` – prüft ob Item eine URL ist - `normalize_url(item)` – normalisiert URL-Item zu `{"url", "zoom"}` - `playlist_item_name(item)` – Name aus String oder Dict extrahieren - `playlist_item_enabled(item)` – Enabled-Status prüfen - `load_priority_files()` – Priority-Playlist + Dateien laden - `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite - `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None` - `load_users()` / `save_users()` / `get_user()` – User aus `users.json` - `init_user_db()` – initialen Admin aus `config.json` anlegen - `get_accessible_sites(cfg, user)` – Standorte nach User-Berechtigung filtern - `record_heartbeat(site, screen, ip, ua, resolution)` – Heartbeat-JSON mit Client-Info schreiben - `screen_is_active(site, screen)` – Prüft ob Heartbeat < 60s alt ist - `get_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 ```bash 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.