Erik Thiele 3844ceacf2 chore: env.sh aus der Versionierung nehmen und ignorieren
Die Datei enthielt OPENAI_API_KEY und BRANDFETCH_API_KEY im Klartext und
war versioniert. Sie wurde lokal entfernt; der Loeschvorgang wird hier
nachgezogen und env.sh in .gitignore aufgenommen, damit sie nicht erneut
hineinrutscht.

Hinweis: Die Schluessel stehen weiterhin in der Repo-Historie und sollten
rotiert werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 13:59:15 +02:00
2026-06-19 14:17:42 +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 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.json protokolliert 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-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>/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/priority/<file> Priority-Medien (global)
GET /media/<site>/logos/<file> Manuell hochgeladenes Kundenlogo
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. Wird ein Screen später abgewählt, verschwindet die Willkommensseite dort beim nächsten Generieren.

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>/logos/logo_<slot>.png abgelegt und über /media/<site>/logos/<datei> ausgeliefert.

Freistellen: Beim Upload wird der Hintergrund automatisch entfernt, damit das Logo nicht in einem weissen Kasten auf dem Hintergrundbild steht. 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. 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: - 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

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

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 daily und weekly zählt nur die Uhrzeit von start/end. Ein Gültigkeitszeitraum wird über valid_from/valid_until gesetzt, nicht über den Datumsanteil.
  • Bei daily sind alle Wochentage gemeint; days wird 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>/hash ein, damit der Player beim Öffnen und Schliessen eines Zeitfensters von selbst neu lädt.

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
  • is_item_active(item) – prüft enabled und 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üfen
  • schedule_text(schedule) – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
  • load_priority_files() – Priority-Playlist + Dateien laden
  • welcome_target_screens(config, site) – Zielscreens der Willkommensseite (Fallback lobby)
  • save_uploaded_logo(file, site, slot, freistellen=True) – hochgeladenes Logo freistellen und als PNG ablegen
  • remove_logo_background(image) – Hintergrund per Flood Fill entfernen, gibt (bild, hinweis) zurück
  • 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 4.9 MiB
Languages
HTML 62.1%
Python 37.6%
Dockerfile 0.3%