From 297b52e50cd020adbef8631379ec087fd41959ac Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Sun, 28 Jun 2026 15:14:06 +0200 Subject: [PATCH] Info Tab und MFA --- AGENTS.md | 60 +- README.md | 45 +- app.py | 244 +- history.json | 264 + session-ses_1e96.md | 10461 ++++++++++++++-------------------- templates/_header.html | 3 + templates/admin.html | 39 +- templates/help.html | 89 +- templates/login.html | 86 +- templates/mfa_recovery.html | 64 + templates/mfa_setup.html | 101 + templates/mfa_verify.html | 70 + templates/player.html | 4 +- templates/user_list.html | 8 + users.json | 28 +- 15 files changed, 5203 insertions(+), 6363 deletions(-) create mode 100644 templates/mfa_recovery.html create mode 100644 templates/mfa_setup.html create mode 100644 templates/mfa_verify.html diff --git a/AGENTS.md b/AGENTS.md index 2a36c0e..c97c5af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,10 +65,33 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. | Route | Beschreibung | |-------|-------------| +| `GET /admin/users` | User-Liste (Admin, mit MFA-Spalte) | +| `GET /admin/users/create` | User anlegen (Formular) | | `POST /admin/users/create` | User anlegen | +| `GET /admin/users/edit/` | User bearbeiten (Formular) | +| `POST /admin/users/edit/` | User bearbeiten speichern | | `POST /admin/users/delete/` | User löschen (letzter Admin geschützt) | | `POST /admin/users/reset-password/` | Passwort-Reset (temporäres Passwort) | -| `POST /admin/users/edit/` | User bearbeiten speichern | +| `GET /mfa/setup` | MFA einrichten (QR-Code + Verifikation) | +| `POST /mfa/setup` | TOTP-Code verifizieren → MFA aktivieren | +| `GET /mfa/verify` | TOTP-Code nach Login eingeben | +| `POST /mfa/verify` | Code prüfen oder Recovery-Code verwenden | +| `POST /mfa/disable` | MFA deaktivieren (mit Passwort) | + +### MFA (Multi-Faktor-Authentifizierung) + +- TOTP via `pyotp`, QR-Code via `qrcode[pil]` (nur in `.venv` installiert) +- Der TOTP-Secret wird sofort beim ersten Aufruf von `/mfa/setup` (GET) in `users.json` persistiert, sodass die nachfolgende POST-Verifikation denselben Secret verwendet +- Nach erfolgreicher Aktivierung: 8 Recovery-Codes (hex, 7 Stellen), einmalig im UI angezeigt, jeder nur einmal verwendbar +- Recovery-Code-Einsatz erzwingt erneute MFA-Einrichtung +- Deaktivierung via `/mfa/disable` mit Passwort-Bestätigung +- MFA-Status in der Userliste (`/admin/users`) als grünes "Aktiv"-Badge oder "–" + +### MFA-Login-Flow + +1. POST `/login` mit Passwort → bei aktivem MFA: Session `mfa_pending` setzen → redirect `/mfa/verify` +2. GET/POST `/mfa/verify` → TOTP-Code prüfen → `login_user()` aufrufen → redirect `/admin/` +3. Recovery-Code: POST `/mfa/verify` mit `recovery=true` → Code prüfen → `mfa_enabled=False` → Login erlauben, aber User muss MFA neu einrichten ### Zugriffs-Dekoratoren @@ -106,13 +129,14 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. - Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=` angelegt werden (nur Admins). - `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv (nur Admins). - `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet. -- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**. +- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Digital Voice Agent** (4), **Medien** (5), **Info** (6); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**. - `stay_on_first`: Wenn aktiviert bleibt der Player auf dem ersten Playlist-Element stehen (kein Durchlauf). -- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien. +- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien → Info. - Player-URL im Screen-Header ist ein klickbarer Link in grauer Farbe. - `add_customer` und `/customer` erfordern jetzt Login (`@login_required`) mit Site-Zugriffsprüfung. - `/admin` redirectet zum ersten für den User zugänglichen Standort (nicht mehr global ersten). -- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, Userverwaltung (Admin), Abmelden. +- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden. +- Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen ## Repo Quirks @@ -120,7 +144,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. - `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default). - The app has no configured tests, lint, typecheck, formatter, or CI. - Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually. -- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`. +- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`, `get_client_info()`, `add_history_entry()`. - Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`. - Hintergrundbild der Willkommensseite wird pro Standort unter `media//background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert. - `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort. @@ -142,6 +166,7 @@ Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermitte | **Server zeichnet Heartbeat auf** | `record_heartbeat(site, screen)` erstellt/updated `/tmp/signage-heartbeat//` via `path.touch()` | | **Admin-UI fragt Status ab** | `screen_is_active(site, screen)` prüft ob `mtime < 60s` alt | | **Anzeige** | Grüner "Online"-Button (filled success) oder roter "Offline"-Button (filled danger) im Card-Header neben "Löschen" | +| **Client-Info** | Beim Hovern über den Online-Button: Tooltip mit Browser, Auflösung, IP, letztem Seen-Zeitpunkt | ### Helper-Funktionen in `app.py` @@ -149,23 +174,32 @@ Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermitte HEARTBEAT_DIR = Path("/tmp/signage-heartbeat") HEARTBEAT_TIMEOUT = 60 # Sekunden -def record_heartbeat(site, screen): - path = HEARTBEAT_DIR / site / screen +def record_heartbeat(site, screen, ip="", ua="", resolution=""): + """Schreibt JSON mit Client-Info (ip, ua, resolution, last_seen).""" + path = HEARTBEAT_DIR / site / f"{screen}.json" path.parent.mkdir(parents=True, exist_ok=True) - path.touch() + data = {"ip": ip, "ua": ua[:200], "resolution": resolution, + "last_seen": datetime.now().strftime("%d.%m.%Y %H:%M:%S")} + path.write_text(json.dumps(data)) def screen_is_active(site, screen): - path = HEARTBEAT_DIR / site / screen + """Prüft, ob Heartbeat < 60s alt ist (anhand .json-mtime).""" + path = HEARTBEAT_DIR / site / f"{screen}.json" if not path.exists(): return False - return (time.time() - path.stat().st_mtime) < HEARTBEAT_TIMEOUT + return (time.time() - path.stat().st_mtime) < 60 + +def get_client_info(site, screen): + """Gibt das gespeicherte Client-Info-JSON zurück.""" + ... ``` ### Integration -- `playlist_hash()`-Route ruft `record_heartbeat()` direkt am Anfang auf -- `admin()`-Route setzt `screen_status["online"/"offline"]` via `screen_is_active()` für jeden Screen -- Admin-Template (`admin.html`) zeigt beide Buttons als ` +187: +188: +189: + +(Showing lines 170-189 of 549. Use offset=190 to continue.) + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 6.7s) + +_Thinking:_ + +OK so `screen_status` is used to show a "Aktiv" or "Leer" badge. I should replace this with heartbeat-based status. Since `screen_status` is already used in the template, I can just change its values: + +- `"active"` → `"active"` if the heartbeat is fresh (green) +- `"empty"` → `"offline"` if no heartbeat or timed out (red) + +Or better yet, I'll change the values to: +- `"online"` if player has sent a heartbeat within 60s +- `"offline"` if no heartbeat +- I should also keep the "empty" concept... hmm, this is tricky. + +Actually, let me think about this differently. The screen_status can have 3 states: +1. Screen has files AND player is active → "active" (green) +2. Screen has files but player is NOT active → "offline" (maybe yellow/red?) +3. Screen has no files AND player is not active → "empty" (yellow warning) + +Or maybe just 2 states is enough: +- Player is active → green +- Player is not active → gray/red (regardless of files) + +Let me keep it simple: replace the current `screen_status` logic with the heartbeat logic. The `"empty"` label was not very useful anyway. + +Actually wait, I should be more careful. Maybe the original `screen_status` was put there for a reason. Let me replace it with heartbeat data and also keep the file check. + +Let me just do: +- `"active"` when heartbeat is fresh (green) +- `"offline"` when no recent heartbeat (red) + +The "empty" concept is less useful because a screen can have files but the player might not be showing them if no one visits the URL. + +Let me now implement: + +1. Add `from pathlib import Path` at the top +2. Add helper functions after `save_config` +3. Modify `playlist_hash` to call `record_heartbeat` +4. Modify `admin` route to use heartbeat status +5. Modify `admin.html` to use the new status values + +Let me read the top of app.py for the imports: + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "limit": 55 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/app.py +file + +1: #!/usr/bin/env python3 +2: """ +3: CANCOM Simple Signage – Flask-App für Digital-Signage-Anzeigen. +4: ────────────────────────────────────────────────────────────── +5: Diese App verwaltet mehrere Standorte (sites) mit jeweils mehreren +6: Screens. Jeder Screen hat eine eigene Playlist aus Bildern, Videos, +7: HTML-Seiten und URLs. Optional kann eine globale Priority-Playlist +8: zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird. +9: """ +10: +11: import os +12: import json +13: import hashlib +14: import re +15: import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung +16: +17: from functools import wraps +18: from datetime import datetime +19: +20: from flask import ( +21: Flask, jsonify, render_template, +22: send_from_directory, redirect, +23: request, abort +24: ) +25: from flask_login import ( +26: LoginManager, login_user, current_user, +27: login_required, logout_user, +28: UserMixin +29: ) +30: from werkzeug.utils import secure_filename +31: from werkzeug.security import generate_password_hash, check_password_hash +32: +33: +34: # ------------------------------------------------- +35: # Grundkonfiguration +36: # ------------------------------------------------- +37: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +38: MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/Screen +39: CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration +40: USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank +41: HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf +42: +43: APP_VERSION = "6.0.2" +44: UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} +45: +46: app = Flask(__name__) +47: app.secret_key = "CHANGE_THIS_SECRET!!!" +48: +49: login_manager = LoginManager(app) +50: login_manager.login_view = "login" # Nicht eingeloggte User werden zum Login umgeleitet +51: +52: +53: # ------------------------------------------------- +54: # Config-Helfer +55: # ------------------------------------------------- + +(Showing lines 1-55 of 1608. Use offset=56 to continue.) + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.8s) + +_Thinking:_ + +Now I have all the info I need. Let me implement the changes: + +1. Add `import time` and `from pathlib import Path` after line 14 +2. Add heartbeat helper functions after line 70 (after `save_config`) +3. Modify `playlist_hash` route (line 738) to call `record_heartbeat` +4. Modify `admin` route (line 924) to use heartbeat status +5. Modify `admin.html` template + +Let me do all edits. + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "oldString": "import os\nimport json\nimport hashlib\nimport re\nimport generate_welcome_page # Logo-Suche und Willkommensseite-Generierung\n\nfrom functools import wraps\nfrom datetime import datetime", + "newString": "import os\nimport json\nimport hashlib\nimport re\nimport time\nimport generate_welcome_page # Logo-Suche und Willkommensseite-Generierung\n\nfrom functools import wraps\nfrom datetime import datetime\nfrom pathlib import Path" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 4.2s) + +_Thinking:_ + +Now add the heartbeat helper functions after `save_config`: + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "oldString": "def save_config(cfg):\n \"\"\"Schreibt das Config-Dict zurück in die config.json.\"\"\"\n with open(CONFIG_FILE, \"w\") as f:\n json.dump(cfg, f, indent=2)\n\n\ndef get_site_list(cfg):", + "newString": "def save_config(cfg):\n \"\"\"Schreibt das Config-Dict zurück in die config.json.\"\"\"\n with open(CONFIG_FILE, \"w\") as f:\n json.dump(cfg, f, indent=2)\n\n\nHEARTBEAT_DIR = Path(\"/tmp/signage-heartbeat\")\nHEARTBEAT_TIMEOUT = 60 # Sekunden\n\n\ndef record_heartbeat(site, screen):\n \"\"\"Aktualisiert den Heartbeat-Timestamp für einen Screen.\"\"\"\n path = HEARTBEAT_DIR / site / screen\n path.parent.mkdir(parents=True, exist_ok=True)\n path.touch()\n\n\ndef screen_is_active(site, screen):\n \"\"\"Prüft, ob ein Screen zuletzt innerhalb des Timeouts registriert wurde.\"\"\"\n path = HEARTBEAT_DIR / site / screen\n if not path.exists():\n return False\n return (time.time() - path.stat().st_mtime) < HEARTBEAT_TIMEOUT\n\n\ndef get_site_list(cfg):" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.4s) + +_Thinking:_ + +Now add `record_heartbeat` call to `playlist_hash`: + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "oldString": "def playlist_hash(site, screen):\n \"\"\"Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).\n Gibt JSON mit Hash und aktuellem stay_on_first-Wert zurück.\"\"\"\n config = load_config()", + "newString": "def playlist_hash(site, screen):\n \"\"\"Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).\n Gibt JSON mit Hash und aktuellem stay_on_first-Wert zurück.\n Zeichnet gleichzeitig den Heartbeat auf (Player aktiv).\"\"\"\n record_heartbeat(site, screen)\n config = load_config()" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 2.9s) + +**Tool: todowrite** + +**Input:** +```json +{ + "todos": [ + { + "priority": "high", + "content": "Helper-Funktionen record_heartbeat + screen_is_active in app.py einfügen", + "status": "completed" + }, + { + "priority": "high", + "content": "playlist_hash-Route um record_heartbeat erweitern", + "status": "completed" + }, + { + "priority": "high", + "content": "admin-Route: screen_status durch Heartbeat ersetzen", + "status": "in_progress" + }, + { + "priority": "high", + "content": "admin.html: grünen/roten Indikator pro Screen einbauen", + "status": "pending" + } + ] +} +``` + +**Output:** +``` +[ + { + "content": "Helper-Funktionen record_heartbeat + screen_is_active in app.py einfügen", + "status": "completed", + "priority": "high" + }, + { + "content": "playlist_hash-Route um record_heartbeat erweitern", + "status": "completed", + "priority": "high" + }, + { + "content": "admin-Route: screen_status durch Heartbeat ersetzen", + "status": "in_progress", + "priority": "high" + }, + { + "content": "admin.html: grünen/roten Indikator pro Screen einbauen", + "status": "pending", + "priority": "high" + } +] +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.4s) + +_Thinking:_ + +Now I need to modify the `admin` route. The line `screen_status[screen_name] = "active" if files else "empty"` should be changed to use heartbeat data. Let me replace that line. + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "oldString": " screens[screen_name] = screen_cfg\n media_files[screen_name] = files\n screen_status[screen_name] = \"active\" if files else \"empty\"", + "newString": " screens[screen_name] = screen_cfg\n media_files[screen_name] = files\n screen_status[screen_name] = \"online\" if screen_is_active(current_site, screen_name) else \"offline\"" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.8s) + +_Thinking:_ + +Now update the admin.html template. The current code shows: +- `"active"` → green badge "Aktiv" +- `"empty"` → yellow badge "Leer" + +I need to change to: +- `"online"` → green badge "Online" +- `"offline"` → red badge "Offline" + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/templates/admin.html", + "oldString": " {% if screen_status[screen] == \"active\" %}\n Aktiv\n {% else %}\n Leer\n {% endif %}", + "newString": " {% if screen_status[screen] == \"online\" %}\n Online\n {% else %}\n Offline\n {% endif %}" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.2s) + +**Tool: todowrite** + +**Input:** +```json +{ + "todos": [ + { + "priority": "high", + "content": "Helper-Funktionen record_heartbeat + screen_is_active in app.py einfügen", + "status": "completed" + }, + { + "priority": "high", + "content": "playlist_hash-Route um record_heartbeat erweitern", + "status": "completed" + }, + { + "priority": "high", + "content": "admin-Route: screen_status durch Heartbeat ersetzen", + "status": "completed" + }, + { + "priority": "high", + "content": "admin.html: grünen/roten Indikator pro Screen einbauen", + "status": "completed" + } + ] +} +``` + +**Output:** +``` +[ + { + "content": "Helper-Funktionen record_heartbeat + screen_is_active in app.py einfügen", + "status": "completed", + "priority": "high" + }, + { + "content": "playlist_hash-Route um record_heartbeat erweitern", + "status": "completed", + "priority": "high" + }, + { + "content": "admin-Route: screen_status durch Heartbeat ersetzen", + "status": "completed", + "priority": "high" + }, + { + "content": "admin.html: grünen/roten Indikator pro Screen einbauen", + "status": "completed", + "priority": "high" + } +] +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.6s) + +_Thinking:_ + +Let me verify the changes look correct by checking the final state: + +**Tool: read** + +**Input:** +```json +{ + "offset": 11, + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "limit": 10 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/app.py +file + +11: import os +12: import json +13: import hashlib +14: import re +15: import time +16: import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung +17: +18: from functools import wraps +19: from datetime import datetime +20: from pathlib import Path + +(Showing lines 11-20 of 1630. Use offset=21 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "offset": 69, + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "limit": 20 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/app.py +file + +69: def save_config(cfg): +70: """Schreibt das Config-Dict zurück in die config.json.""" +71: with open(CONFIG_FILE, "w") as f: +72: json.dump(cfg, f, indent=2) +73: +74: +75: HEARTBEAT_DIR = Path("/tmp/signage-heartbeat") +76: HEARTBEAT_TIMEOUT = 60 # Sekunden +77: +78: +79: def record_heartbeat(site, screen): +80: """Aktualisiert den Heartbeat-Timestamp für einen Screen.""" +81: path = HEARTBEAT_DIR / site / screen +82: path.parent.mkdir(parents=True, exist_ok=True) +83: path.touch() +84: +85: +86: def screen_is_active(site, screen): +87: """Prüft, ob ein Screen zuletzt innerhalb des Timeouts registriert wurde.""" +88: path = HEARTBEAT_DIR / site / screen + +(Showing lines 69-88 of 1630. Use offset=89 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "offset": 752, + "filePath": "/Users/erik/Documents/DEV/signage/app.py", + "limit": 10 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/app.py +file + +752: # Hash für automatisches Player-Reload +753: # ───────────────────────────────────────────────── +754: # Der Player pollt alle 5s diese Route. +755: # Wenn sich der Hash ändert, wird neu geladen. +756: # ------------------------------------------------- +757: @app.route("/playlist///hash") +758: def playlist_hash(site, screen): +759: """Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority). +760: Gibt JSON mit Hash und aktuellem stay_on_first-Wert zurück. +761: Zeichnet gleichzeitig den Heartbeat auf (Player aktiv).""" + +(Showing lines 752-761 of 1630. Use offset=762 to continue.) + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 2.7s) + +_Thinking:_ + +Let me also verify the admin route and the template changes: + +**Tool: bash** + +**Input:** +```json +{ + "description": "Verify the screen_status assignment in admin route", + "command": "grep -n \"screen_status\" app.py" +} +``` + +**Output:** +``` +860: screen_status = {} +946: screen_status[screen_name] = "online" if screen_is_active(current_site, screen_name) else "offline" +954: screen_status=screen_status, + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 2.9s) + +_Thinking:_ + +Everything looks correct. Let me restart the server and verify the Flask app starts without errors. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Check syntax of app.py", + "command": "python3 -c \"import py_compile; py_compile.compile('app.py', doraise=True)\" 2>&1" +} +``` + +**Output:** +``` +(no output) +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.7s) + +_Thinking:_ + +Syntax is fine. Let me also check if the server was still running from before - I should restart it to pick up the changes. + +Actually, since `debug=True` is set, Flask should auto-reload when I save the file. Let me check if the server is running and if it auto-reloaded by testing the endpoint. + +Let me also check if there are existing heartbeats in /tmp that might confuse things. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Check if server is running and test heartbeat", + "command": "curl -s http://localhost:5005/playlist/stuttgart/lobby/hash && echo \"\" && ls -la /tmp/signage-heartbeat/stuttgart/lobby 2>&1 || echo \"Server not responding or no heartbeat yet\"" +} +``` + +**Output:** +``` +Server not responding or no heartbeat yet + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 4.8s) + +_Thinking:_ + +Server might have been killed. Let me start it. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Start Flask server in background", + "command": "cd /Users/erik/Documents/DEV/signage && nohup python3 app.py > /tmp/flask.log 2>&1 &\nsleep 2\ncurl -s http://localhost:5005/playlist/stuttgart/lobby/hash" +} +``` + +**Output:** +``` +(no output) +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 279.5s) + +_Thinking:_ + +No output – maybe the server is still starting. Let me check the log. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Check flask log and try curl again", + "command": "cat /tmp/flask.log && echo \"---\" && sleep 2 && curl -sv http://localhost:5005/playlist/stuttgart/lobby/hash 2>&1 | head -20" +} +``` + +**Output:** +``` +--- +* Host localhost:5005 was resolved. +* IPv6: ::1 +* IPv4: 127.0.0.1 +* Trying [::1]:5005... +* connect to ::1 port 5005 from ::1 port 56828 failed: Connection refused +* Trying 127.0.0.1:5005... +* connect to 127.0.0.1 port 5005 from 127.0.0.1 port 56829 failed: Connection refused +* Failed to connect to localhost port 5005 after 0 ms: Couldn't connect to server +* Closing connection + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 123.7s) + +_Thinking:_ + +The server didn't start. Let me try to run it directly to see what error occurs. Maybe port 5005 is still in use from the old process. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Start server in foreground to see errors", + "command": "pkill -f \"python3 app.py\" 2>/dev/null; sleep 1; python3 app.py 2>&1 &\nsleep 3\ncurl -s http://localhost:5005/playlist/stuttgart/lobby/hash" +} +``` + +**Output:** +``` + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5005 + * Running on http://192.168.10.191:5005 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 629-939-237 +/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/resource_tracker.py:324: UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown: {'/mp-v440fjyz'} + warnings.warn( + + + +shell tool terminated command after exceeding timeout 120000 ms. If this command is expected to take longer and is not waiting for interactive input, retry with a larger timeout value in milliseconds. + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.0s) + +_Thinking:_ + +It seems like the command timed out but the server did start (it was running). Let me check if the heartbeat file was created. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Test heartbeat file was created", + "command": "ls -la /tmp/signage-heartbeat/stuttgart/lobby 2>&1" +} +``` + +**Output:** +``` +ls: /tmp/signage-heartbeat/stuttgart/lobby: No such file or directory + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.1s) + +_Thinking:_ + +The heartbeat file wasn't created. The server might have been killed by the timeout or there's still an issue. Let me check if curl works now. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Test if server is running and heartbeat works", + "command": "curl -s http://localhost:5005/playlist/stuttgart/lobby/hash 2>&1 && echo \"\" && ls -la /tmp/signage-heartbeat/stuttgart/lobby 2>&1" +} +``` + +**Output:** +``` +(no output) +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 13.7s) + +_Thinking:_ + +No output at all - the server seems to have been killed when the previous command timed out. Let me start it fresh. + +**Tool: bash** + +**Input:** +```json +{ + "description": "Clean start Flask server", + "command": "pkill -f \"python3 app.py\" 2>/dev/null; sleep 1\npython3 app.py &>/tmp/flask.log &\nsleep 3\necho \"--- Server log ---\"\ncat /tmp/flask.log\necho \"--- Curl test ---\"\ncurl -s http://localhost:5005/playlist/stuttgart/lobby/hash\necho \"\"\necho \"--- Heartbeat file ---\"\nls -la /tmp/signage-heartbeat/stuttgart/lobby 2>&1", + "timeout": 15000 +} +``` + +**Output:** +``` +--- Server log --- + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5005 + * Running on http://192.168.10.191:5005 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 629-939-237 +--- Curl test --- +{ + "hash": "fa32da88f619d449692ee16f480378a9", + "stay_on_first": false +} + +--- Heartbeat file --- +-rw-r--r-- 1 erik wheel 0 26 Juni 14:48 /tmp/signage-heartbeat/stuttgart/lobby + +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.7s) + +_Thinking:_ + +Der Server läuft und der Heartbeat funktioniert. Die Datei `/tmp/signage-heartbeat/stuttgart/lobby` wurde erstellt. + +Jetzt im Admin sollte der Screen "stuttgart/lobby" grün (online) angezeigt werden, während andere Screans ohne Heartbeat rot (offline) sind. + +Alles läuft. Der Server antwortet und der Heartbeat wird aufgezeichnet. Prüf es im Admin unter `http://localhost:5005/admin/stuttgart` – der `lobby`-Screen sollte **grün (Online)** sein, alle anderen rot (Offline), da nur der Player in `lobby` regelmäßig den Hash pollt. + +--- + +## User + +ja stimmt + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.7s) + +_Thinking:_ + +The user confirmed it works. The heartbeat system is functional - lobby shows green/online since its player polls the hash endpoint every 5 seconds, while other screens that aren't being played show red/offline. Nothing more to do here. + +Läuft. Sobald ein Player für einen anderen Screen die Seite öffnet, wechselt auch der automatisch auf grün. 60s nach dem letzten Poll wird er wieder rot. + +--- + +## User + +der Online / Offline Button sollte von der größe dem löschen Button rechts daneben angeglichen werden + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 2.9s) + +_Thinking:_ + +The user wants the Online/Offline badge to match the size of the "Löschen" button next to it. Let me look at the current admin.html to see both elements. + +**Tool: read** + +**Input:** +```json +{ + "offset": 175, + "filePath": "/Users/erik/Documents/DEV/signage/templates/admin.html", + "limit": 20 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/templates/admin.html +file + +175: +176: +177:
+178: {% if screen_status[screen] == "online" %} +179: Online +180: {% else %} +181: Offline +182: {% endif %} +183:
+186: +187:
+188:
+189: +190: +191: +192:
+193: +194:
+``` + +The Löschen button is inside a `
` element. The `` element might have some styling (margin, padding, or line-height) that adds extra space. The Online/Offline button is directly in the flex container. + +Let me check if the `` has any default margin/padding that could affect the height. + +Actually, I think the simplest fix is to add `class="d-inline"` to the form (which it already has) and maybe add `m-0 p-0` to the form to eliminate any form-level spacing. + +Wait, the form already has `class="d-inline"`. Let me look at what else could cause the height difference. + +The button inside `` has `btn-outline-danger`. The Online button has `btn-success`. These have different background colors. Could the background make it look visually different? + +Let me just try a different approach - make the Online/Offline badge use `btn-outline-success`/`btn-outline-danger` to match the Löschen button's outline style: + +**Tool: read** + +**Input:** +```json +{ + "offset": 177, + "filePath": "/Users/erik/Documents/DEV/signage/templates/admin.html", + "limit": 12 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/signage/templates/admin.html +file + +177:
+178: {% if screen_status[screen] == "online" %} +179: +180: {% else %} +181: +182: {% endif %} +183: +186: +187: +188:
+ +(Showing lines 177-188 of 549. Use offset=189 to continue.) +
+``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 9.0s) + +_Thinking:_ + +Looking at this more carefully, I think the issue is that: +1. Löschen: `
\n {% else %}\n \n {% endif %}", + "newString": " {% if screen_status[screen] == \"online\" %}\n \n {% else %}\n \n {% endif %}" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 4.5s) + +_Thinking:_ + +Let me also check if the form wrapper adds extra height. I could add `m-0 p-0` to the form to ensure no extra spacing. + +Und zur Sicherheit auch das ``-Wrapper-styling (falls das margin/padding addiert): + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/signage/templates/admin.html", + "oldString": " \n \n
", + "newString": "
\n \n
" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 3.4s) + +_Thinking:_ + +Jetzt haben beide Buttons denselben Outline-Stil und das Form hat kein extra margin/padding. Sollte jetzt identische Höhe haben. + +Jetzt beide mit `btn-outline-*` + `m-0 p-0` auf dem Formular → gleicher Border, gleiches Padding, gleiche Höhe. + +--- + +## User + +jetzt sollte nur die Schrift weiß und der Hintergrund immer rot oder grün sein + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 17.0s) + +_Thinking:_ + +The user wants the Online/Offline buttons to have a filled background (green/red) with white text, not the outline style. So I should go back to `btn-success`/`btn-danger` but also make the Löschen button match by removing its border somehow, or changing the approach. + +Actually, the user is saying: they want Online = green background, white text; Offline = red background, white text. But they also want the height to match the Löschen button. + +The problem was that `btn-success` and `btn-outline-danger` have different heights because of the border. So I need to either: +1. Add `border: 0` to the Löschen button too +2. Or give the Online/Offline buttons a border to match + +Actually, the simplest: all three buttons should be the same style. Let me: +- Online: `btn btn-sm btn-success` with white text +- Offline: `btn btn-sm btn-danger` with white text +- Löschen: `btn btn-sm btn-danger` (filled, not outline) - but that changes the visual meaning + +Hmm, but the Löschen is a delete action and should look different. Let me think... + +Actually, the simplest approach: keep `btn-success`/`btn-danger` for Online/Offline, and add a `border: 1px solid transparent` to make the box-sizing match the `btn-outline-danger` button. That way the total height will be the same. + +Or even simpler: just change the Löschen button to not have an outline. Make it `btn-danger` instead of `btn-outline-danger`. But that changes the design. + +Wait, let me re-read what the user said: "jetzt sollte nur die Schrift weiß und der Hintergrund immer rot oder grün sein" - they want white text with colored background. So they want filled buttons, not outline. The issue is just the height difference. + +The simplest fix is to keep `btn-outline-*` for all (same height) but fill the Online/Offline ones. Or add a transparent border to the filled ones. + +Actually, the real fix: use `btn-success`/`btn-danger` but add `border: 1px solid transparent` so the box model matches the outline button. + +Or even better: use `btn-success`/`btn-danger` and change the Löschen button to also be `btn-danger` without outline. That way all three have no border and same height. + +Let me ask... no, the user just told me what they want. Let me just do it: filled background with white text. For height matching, I'll add `border: 0` to the Löschen button since it already has `btn-outline-danger`. Wait no, `btn-outline-danger` with `border:0` is just a text with no background. + +OK, let me just go with: Online = `btn-success`, Offline = `btn-danger`, Löschen = `btn-outline-danger` but I'll make the Online/Offline match by giving them `border: var(--tblr-border-width) solid transparent` or something. + +Actually the easiest: just wrap both in a flex container and let CSS handle it. But that's already the case. + +Let me try a different approach - just make all three use `btn` with the same border-box: + +```html +Online: \n {% else %}\n \n {% endif %}", + "newString": " {% if screen_status[screen] == \"online\" %}\n \n {% else %}\n \n {% endif %}" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 9.7s) + +_Thinking:_ + +Now if the height is still different, the issue is the `` wrapper around the Löschen button. Let me try another approach - move the Löschen button out of the form or make the form not affect the height. + +Actually, looking at the structure, both `\n {% else %}\n \n {% endif %}\n \n \n ", + "newString": " {% if screen_status[screen] == \"online\" %}\n \n {% else %}\n \n {% endif %}\n
\n \n
" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · DeepSeek V4 Flash Free · 11.9s) + +_Thinking:_ + +Now both buttons are without `disabled` attribute. The Online/Offline uses `onclick="return false"` to prevent clicking. The Löschen button is inside a form but has no `disabled`. They should have the same height now. + +If the height is STILL different, the issue must be the `
` wrapper or the `btn-outline-danger` vs `btn-danger` difference. Let me check if there's still a height diff. + +Actually, let me compare: +- Online: ` + {% else %} {% endif %} @@ -188,7 +189,7 @@ - +
@@ -473,6 +479,35 @@
+ +
+

Client-Informationen

+ {% if client_info[screen] %} + + + + + + + + + + + + + + + + + + + +
IP-Adresse{{ client_info[screen].get('ip', '') }}
Browser{{ client_info[screen].get('ua', '') }}
Auflösung{{ client_info[screen].get('resolution', '') }}
Zuletzt gesehen{{ client_info[screen].get('last_seen', '') }}
+ {% else %} +

Noch keine Client-Daten vorhanden. Der Player muss mindestens einmal die Playlist aufgerufen haben.

+ {% endif %} +
+
diff --git a/templates/help.html b/templates/help.html index 676589b..6b4be26 100644 --- a/templates/help.html +++ b/templates/help.html @@ -41,9 +41,36 @@
+
+ + + +
-

Standort anlegen

+

Standort anlegen

Ein Standort (Site) gruppiert mehrere Screens, z. B. stuttgart oder karlsruhe.

    @@ -56,7 +83,7 @@
-

Screen hinzufügen

+

Screen hinzufügen

Ein Screen ist ein einzelner Player (z. B. lobby, casino), der eine Playlist abspielt.

    @@ -69,7 +96,7 @@
-

Playlist verwalten

+

Playlist verwalten

Jeder Screen hat eine eigene Playlist mit Medien und URLs.

    @@ -84,7 +111,7 @@
-

Priority-Playlist

+

Priority-Playlist

Die Priority-Playlist ist global und wird auf allen Playern eines Standorts eingeblendet – zwischen den normalen Playlist-Einträgen.

    @@ -96,7 +123,7 @@
-

Action Button (Custom-URL)

+

Action Button (Custom-URL)

Der Action Button ist ein konfigurierbarer Button im Player, der eine frei wählbare URL öffnet.

    @@ -116,7 +143,7 @@
-

Digital Voice Agent

+

Digital Voice Agent

Der Digital Voice Agent zeigt einen Button im Player, der eine Typewriter-Animation mit wechselnden mehrsprachigen Texten anzeigt.

    @@ -132,7 +159,7 @@
-

Screen-Einstellungen

+

Screen-Einstellungen

Im Tab Einstellungen können pro Screen konfiguriert werden:

    @@ -145,7 +172,21 @@
-

Willkommensseite

+

Info-Tab im Screen

+
+

Jeder Screen hat einen Info-Tab (Tab 6), der detaillierte Client-Informationen zum aktuell verbundenen Player anzeigt:

+
    +
  • IP-Adresse des Players
  • +
  • Browser (User-Agent) des Players
  • +
  • Bildschirmauflösung (Breite × Höhe)
  • +
  • Zuletzt gesehen – Zeitstempel des letzten Heartbeats
  • +
+

Die Daten werden alle 5 Sekunden vom Player gesendet und im Heartbeat-JSON gespeichert. Der Online-Status (grüner/roter Button im Screen-Header) basiert auf dem gleichen Mechanismus.

+
+
+ +
+

Willkommensseite

Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.

    @@ -159,7 +200,7 @@ {% if current_user.is_admin %}
    -

    User-Verwaltung (Admin)

    +

    User-Verwaltung (Admin)

    Admins können über /admin/users die User-Verwaltung aufrufen.

      @@ -179,7 +220,7 @@
    -

    Admin Dashboard

    +

    Admin Dashboard

    Das Admin Dashboard (/admin/dashboard) bietet eine Übersicht über das gesamte System:

      @@ -193,24 +234,40 @@ {% endif %}
      -

      Dark Mode

      +

      Dark Mode

      Der Dark Mode kann über den Mond-Button im Header umgeschaltet werden. Die Einstellung wird im Browser gespeichert (localStorage) und bleibt auch nach Seitenwechsel erhalten.

      -

      Passwort ändern

      +

      Passwort ändern

      Das Passwort kann über das User-Menü (oben rechts) → Passwort ändern geändert werden. Nach einem Admin-Reset muss der User beim ersten Login ein neues Passwort vergeben.

      -
    -
    +
    +

    MFA (Multi-Faktor-Authentifizierung)

    +
    +

    MFA bietet eine optionale zweite Sicherheitsstufe per TOTP (Google Authenticator, Authy).

    +
      +
    • Aktivierung: Über das User-Menü → MFA einrichten → QR-Code mit einer Authenticator-App scannen → 6-stelligen Code eingeben → Bestätigen
    • +
    • Recovery-Codes: Nach erfolgreicher Aktivierung werden 8 Recovery-Codes (einmalig) angezeigt. Jeder Code kann nur einmal verwendet werden. Nach Einsatz eines Recovery-Codes muss MFA neu eingerichtet werden.
    • +
    • Login mit MFA: Nach Eingabe von E-Mail + Passwort erscheint ein zweites Formular für den TOTP-Code. Erst nach erfolgreichem Code wird der Zugriff gewährt.
    • +
    • Deaktivierung: Über /mfa/setup → Button "MFA deaktivieren" → aktuelles Passwort eingeben
    • +
    • MFA-Status: In der Userliste (/admin/users) wird der MFA-Status als grünes "Aktiv"-Badge oder "–" angezeigt.
    • +
    +
    +
    + +
    +
+ +
+
{% include "_footer.html" %} -
- +
\ No newline at end of file diff --git a/templates/login.html b/templates/login.html index 32c745e..28ccf1c 100755 --- a/templates/login.html +++ b/templates/login.html @@ -2,50 +2,60 @@ + CANCOM Simple Signage Admin - - - + + +{% include "_styles.html" %} + + +
- - -
-
-

Signage Admin

+{% include "_header.html" %} - {% if error %} - - + \ No newline at end of file diff --git a/templates/mfa_recovery.html b/templates/mfa_recovery.html new file mode 100644 index 0000000..02c4a9b --- /dev/null +++ b/templates/mfa_recovery.html @@ -0,0 +1,64 @@ + + + + + +MFA eingerichtet – CANCOM Signage + + + + + + + + +{% include '_styles.html' %} + + + +{% include '_header.html' %} + +
+
+
+
+
+ +
+
+

✅ MFA aktiviert

+

+ Speichere diese Recovery-Codes an einem sicheren Ort.
+ Jeder Code kann einmal verwendet werden. +

+ +
+ ⚠️ Wichtig: Diese Codes werden nur einmal angezeigt. +
+ +
+ {% for code in codes %} + {{ code }} + {% endfor %} +
+ +

Klicke auf einen Code, um ihn zu kopieren.

+ ← Zum Admin +
+
+ +
+
+
+
+ {% include '_footer.html' %} +
+ + + diff --git a/templates/mfa_setup.html b/templates/mfa_setup.html new file mode 100644 index 0000000..ecd13fb --- /dev/null +++ b/templates/mfa_setup.html @@ -0,0 +1,101 @@ + + + + + +MFA einrichten – CANCOM Signage + + + + + + + + +{% include '_styles.html' %} + + + +{% include '_header.html' %} + +
+
+
+
+
+ + {% if recovered %} +
+ Hinweis: Du hast einen Recovery-Code verwendet. Bitte richte MFA neu ein. +
+ {% endif %} + {% if disabled %} +
MFA wurde deaktiviert.
+ {% endif %} + +
+
+ + {% if current_user.mfa_enabled %} +

✅ MFA ist aktiv

+

Dein Konto ist durch Zwei-Faktor-Authentifizierung geschützt.

+

Möchtest du MFA deaktivieren? Gib dein Passwort ein:

+
+
+
+ + +
+
+
+ {% else %} +

MFA einrichten

+

Scanne den QR-Code mit deiner Authenticator-App (z. B. Google Authenticator, Authy).

+ + {% if error %} + + {% endif %} + + {% if qr_data %} +
+ QR-Code +
+

Alternativ: Secret manuell eingeben

+
+ {{ secret }} +
+
+

Code verifizieren

+

Gib den 6-stelligen Code aus der App ein, um MFA zu aktivieren.

+
+
+
+ + +
+
+
+ {% endif %} + {% endif %} + +
+
+ +
+
+
+
+ {% include '_footer.html' %} +
+ + + diff --git a/templates/mfa_verify.html b/templates/mfa_verify.html new file mode 100644 index 0000000..a53451a --- /dev/null +++ b/templates/mfa_verify.html @@ -0,0 +1,70 @@ + + + + + + Sicherheitscode eingeben - CANCOM Signage + + + {% include "_styles.html" %} + + + +
+ +{% include "_header.html" %} + +
+
+
+
+
+
+

Sicherheitscode

+

Gib den 6-stelligen Code aus deiner Authenticator-App ein.

+ + {% if error %} + + {% endif %} + +
+
+ +
+ +
+ +

+ + Code verloren? + +

+ +
+
+
+
+
+
+ +{% include "_footer.html" %} +
+ + \ No newline at end of file diff --git a/templates/player.html b/templates/player.html index 096867e..8acce55 100755 --- a/templates/player.html +++ b/templates/player.html @@ -488,7 +488,9 @@ let lastHash = null; async function checkForUpdates() { try { - const res = await fetch(`/playlist/${site}/${screen}/hash`, { cache: "no-store" }); + const w = window.screen ? window.screen.width : '?'; + const h = window.screen ? window.screen.height : '?'; + const res = await fetch(`/playlist/${site}/${screen}/hash?w=${w}&h=${h}`, { cache: "no-store" }); const data = await res.json(); const hash = data.hash; const serverStayOnFirst = data.stay_on_first; diff --git a/templates/user_list.html b/templates/user_list.html index 099a907..8be91c1 100644 --- a/templates/user_list.html +++ b/templates/user_list.html @@ -76,6 +76,7 @@ Rolle Standorte Abteilung + MFA Status Aktionen @@ -106,6 +107,13 @@ {% endif %} {{ u.department }} + + {% if u.mfa_enabled %} + Aktiv + {% else %} + + {% endif %} + {% if u.must_change_password %} Passwort-Änderung erforderlich diff --git a/users.json b/users.json index 491bfc1..2d56989 100644 --- a/users.json +++ b/users.json @@ -3,7 +3,19 @@ "password_hash": "scrypt:32768:8:1$JNEBAJBSSigcWCxK$4809dcc38b2abcaa3f6397d052560f9f59dedf7c95cf99b61bac0df08a65b64287907522841cbb23e06b36760814007d5757542db1c968a8536be7fd6604a382", "role": "admin", "sites": [], - "must_change_password": false + "must_change_password": false, + "mfa_enabled": false, + "mfa_recovery_codes": [ + "923EFA", + "AED258", + "E8EAF6", + "A659E6", + "946F07", + "880C07", + "FC204A", + "1D09D7" + ], + "mfa_secret": "VOLBXT7HYK6BXBD32EGW7CMFLVD53QON" }, "erik.thiele@cancom.de": { "password_hash": "scrypt:32768:8:1$gyBs4dA7DhH9wA0u$c80f25262388095545733c95dd4e569eee610fa849a11dd28d4f504d09e23d3768adad26d4ed671975bd3dcbae5dc43bd982f4d475e6bc9d2ede088cb0b6c025", @@ -13,7 +25,19 @@ "first_name": "Erik", "last_name": "Thiele", "department": "CSO", - "notes": "Standort Stuttgart" + "notes": "Standort Stuttgart", + "mfa_secret": "CHKS2QZOJ45TSCLUN7FWO3MLTMRSS6Q3", + "mfa_enabled": true, + "mfa_recovery_codes": [ + "1FB378", + "AAD593", + "B4BCB9", + "9CB5C3", + "22D614", + "26F3B7", + "47FB5C", + "50BB59" + ] }, "test1@test.de": { "password_hash": "scrypt:32768:8:1$35kBhL79TJyVA3Rz$98b2e269db5ae96c9285323a274a0cc5b199fb45078e7530b5e72cdb9b96d5b522aa18fd06ce0f801bd19ec68ed323ffa04ce484cfb849eba73b9260ca6a50e1",