Erik Thiele b8093521a0 Fix: Edit-Button-Klick-Handler vor DOMContentLoaded ziehen
Die globale Event-Delegation lag versehentlich innerhalb
des DOMContentLoaded-Callbacks. Wenn initSortable fehlschlug,
wurde der Click-Handler nie registriert.
- editState + document click handler jetzt global (vor DOMContentLoaded)
- Duplicate Code entfernt
- Debug-alerts entfernt
2026-07-10 16:40:59 +02:00
2026-06-19 14:17:42 +02:00
2026-06-30 19:01:15 +02:00
2026-06-21 17:56:58 +02:00
2026-06-30 19:01:15 +02:00
2026-06-22 21:39:20 +02:00
2026-06-30 19:01:15 +02:00
2026-06-30 19:01:15 +02:00
2026-06-30 19:01:15 +02:00
2026-06-28 15:15:58 +02:00

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 130 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)
  • 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/<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: - Punkt oder * 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.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)
{
  "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
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.

Description
No description provided
Readme 3.6 MiB
Languages
HTML 63.3%
Python 36.4%
Dockerfile 0.2%
Shell 0.1%