Ergänzung Active Button
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ media/
|
|||||||
*.mp4
|
*.mp4
|
||||||
*.mov
|
*.mov
|
||||||
agents.md
|
agents.md
|
||||||
|
opencode.md
|
||||||
54
AGENTS.md
54
AGENTS.md
@@ -31,7 +31,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
|
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
|
||||||
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
||||||
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
|
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
|
||||||
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload |
|
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload + zeichnet Heartbeat auf (Online-Status) |
|
||||||
| `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren |
|
| `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren |
|
||||||
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
|
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
|
||||||
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen auf Standard |
|
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen auf Standard |
|
||||||
@@ -120,7 +120,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).
|
- `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.
|
- 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.
|
- 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()`.
|
- 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()`.
|
||||||
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
|
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
|
||||||
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert.
|
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert.
|
||||||
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.
|
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.
|
||||||
@@ -129,3 +129,53 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN.
|
- `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN.
|
||||||
- `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.
|
- `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.
|
||||||
- `static/dva.png`: Bild für den Voice-Agent-Button (optional, per Admin-Toggle ein-/ausblendbar).
|
- `static/dva.png`: Bild für den Voice-Agent-Button (optional, per Admin-Toggle ein-/ausblendbar).
|
||||||
|
|
||||||
|
## Heartbeat / Online-Status (seit v6.1.0)
|
||||||
|
|
||||||
|
Der Player-Status (Online/Offline) wird mittels dateibasiertem Heartbeat ermittelt.
|
||||||
|
|
||||||
|
### Funktionsweise
|
||||||
|
|
||||||
|
| Schritt | Beschreibung |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Player-pollt** alle 5s `/playlist/<site>/<screen>/hash` (`checkForUpdates`) | Bestehender Mechanismus, kein Extra-Request |
|
||||||
|
| **Server zeichnet Heartbeat auf** | `record_heartbeat(site, screen)` erstellt/updated `/tmp/signage-heartbeat/<site>/<screen>` 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" |
|
||||||
|
|
||||||
|
### Helper-Funktionen in `app.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
HEARTBEAT_DIR = Path("/tmp/signage-heartbeat")
|
||||||
|
HEARTBEAT_TIMEOUT = 60 # Sekunden
|
||||||
|
|
||||||
|
def record_heartbeat(site, screen):
|
||||||
|
path = HEARTBEAT_DIR / site / screen
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
|
||||||
|
def screen_is_active(site, screen):
|
||||||
|
path = HEARTBEAT_DIR / site / screen
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
return (time.time() - path.stat().st_mtime) < HEARTBEAT_TIMEOUT
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 `<button class="btn btn-sm btn-success/btn-danger" onclick="return false;">`
|
||||||
|
- Die Lösch-Buttons (`btn-outline-danger btn-sm`) haben gleiche Größe durch gleiche btn-sm-Klasse, Form-Wrapper hat `m-0 p-0`
|
||||||
|
|
||||||
|
### Docker-Hinweis
|
||||||
|
|
||||||
|
Im Docker-Container liegt der Heartbeat-Pfad unter `/tmp/signage-heartbeat/` im Container. Da die Docker-Umgebung keine Volumes mounted, sind Heartbeats container-lokal. Nach `docker compose restart` werden alle Screens kurz rot, bis die Player innerhalb von max. 60s wieder pollen.
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Heartbeat-Dateien anzeigen
|
||||||
|
ls -la /tmp/signage-heartbeat/*/
|
||||||
|
find /tmp/signage-heartbeat -type f -exec ls -la {} \;
|
||||||
|
```
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -17,6 +17,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
|||||||
- **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)
|
- **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)
|
- **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)
|
- **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
|
||||||
- **User-Verwaltung**: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort
|
- **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
|
- **Passwort-Workflow**: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash
|
||||||
- **Admin Dashboard**: `/admin/dashboard` – Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf
|
- **Admin Dashboard**: `/admin/dashboard` – Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf
|
||||||
@@ -41,6 +42,7 @@ Flask App (Server)
|
|||||||
├── config.json
|
├── config.json
|
||||||
├── users.json
|
├── users.json
|
||||||
├── history.json
|
├── history.json
|
||||||
|
├── /tmp/signage-heartbeat/ (Heartbeat-Dateien für Online-Status)
|
||||||
├── media/
|
├── media/
|
||||||
│ ├── <site>/
|
│ ├── <site>/
|
||||||
│ │ ├── <screen>/
|
│ │ ├── <screen>/
|
||||||
@@ -185,7 +187,7 @@ Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User
|
|||||||
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
|
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
|
||||||
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen |
|
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen |
|
||||||
| `GET /player/<site>/<screen>` | Player-Ansicht |
|
| `GET /player/<site>/<screen>` | Player-Ansicht |
|
||||||
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme (für Auto-Reload) |
|
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme (für Auto-Reload) + Heartbeat (Online-Status) |
|
||||||
| `GET /willkommen?site=<site>` | Willkommensseite-Formular (GET + POST) |
|
| `GET /willkommen?site=<site>` | Willkommensseite-Formular (GET + POST) |
|
||||||
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
||||||
| `GET /change-password` | Passwort ändern (First-Login / nach Admin-Reset) |
|
| `GET /change-password` | Passwort ändern (First-Login / nach Admin-Reset) |
|
||||||
@@ -200,6 +202,17 @@ Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User
|
|||||||
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
||||||
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
|
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
|
||||||
|
|
||||||
|
### Heartbeat / Online-Status
|
||||||
|
|
||||||
|
Der Player ruft alle 5s `/playlist/<site>/<screen>/hash` auf. Der Server schreibt dabei einen Timestamp nach `/tmp/signage-heartbeat/<site>/<screen>`. Im Admin-Dashboard wird geprüft, ob dieser Timestamp < 60s alt ist → grüner "Online"- oder roter "Offline"-Button pro Screen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Heartbeat-Dateien anzeigen
|
||||||
|
ls -la /tmp/signage-heartbeat/*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach einem Server-Neustart werden alle Screens kurz rot, bis die Player wieder pollen (max. 60s).
|
||||||
|
|
||||||
### Admin Portal
|
### Admin Portal
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -357,6 +370,8 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
|
|||||||
- `load_users()` / `save_users()` / `get_user()` – User aus `users.json`
|
- `load_users()` / `save_users()` / `get_user()` – User aus `users.json`
|
||||||
- `init_user_db()` – initialen Admin aus `config.json` anlegen
|
- `init_user_db()` – initialen Admin aus `config.json` anlegen
|
||||||
- `get_accessible_sites(cfg, user)` – Standorte nach User-Berechtigung filtern
|
- `get_accessible_sites(cfg, user)` – Standorte nach User-Berechtigung filtern
|
||||||
|
- `record_heartbeat(site, screen)` – Heartbeat-Datei schreiben (wird vom Player-Poll getriggert)
|
||||||
|
- `screen_is_active(site, screen)` – Prüft ob Heartbeat < 60s alt ist
|
||||||
- `admin_required` / `site_access_required` – Zugriffs-Dekoratoren
|
- `admin_required` / `site_access_required` – Zugriffs-Dekoratoren
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
30
app.py
30
app.py
@@ -12,10 +12,12 @@ import os
|
|||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung
|
import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, jsonify, render_template,
|
Flask, jsonify, render_template,
|
||||||
@@ -40,7 +42,7 @@ CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration
|
|||||||
USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank
|
USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank
|
||||||
HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf
|
HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf
|
||||||
|
|
||||||
APP_VERSION = "6.0.2"
|
APP_VERSION = "6.0.3"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -68,7 +70,25 @@ def save_config(cfg):
|
|||||||
"""Schreibt das Config-Dict zurück in die config.json."""
|
"""Schreibt das Config-Dict zurück in die config.json."""
|
||||||
with open(CONFIG_FILE, "w") as f:
|
with open(CONFIG_FILE, "w") as f:
|
||||||
json.dump(cfg, f, indent=2)
|
json.dump(cfg, f, indent=2)
|
||||||
f.flush()
|
|
||||||
|
|
||||||
|
HEARTBEAT_DIR = Path("/tmp/signage-heartbeat")
|
||||||
|
HEARTBEAT_TIMEOUT = 60 # Sekunden
|
||||||
|
|
||||||
|
|
||||||
|
def record_heartbeat(site, screen):
|
||||||
|
"""Aktualisiert den Heartbeat-Timestamp für einen Screen."""
|
||||||
|
path = HEARTBEAT_DIR / site / screen
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
|
||||||
|
|
||||||
|
def screen_is_active(site, screen):
|
||||||
|
"""Prüft, ob ein Screen zuletzt innerhalb des Timeouts registriert wurde."""
|
||||||
|
path = HEARTBEAT_DIR / site / screen
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
return (time.time() - path.stat().st_mtime) < HEARTBEAT_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
def get_site_list(cfg):
|
def get_site_list(cfg):
|
||||||
@@ -737,7 +757,9 @@ def player(site, screen):
|
|||||||
@app.route("/playlist/<site>/<screen>/hash")
|
@app.route("/playlist/<site>/<screen>/hash")
|
||||||
def playlist_hash(site, screen):
|
def playlist_hash(site, screen):
|
||||||
"""Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).
|
"""Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).
|
||||||
Gibt JSON mit Hash und aktuellem stay_on_first-Wert zurück."""
|
Gibt JSON mit Hash und aktuellem stay_on_first-Wert zurück.
|
||||||
|
Zeichnet gleichzeitig den Heartbeat auf (Player aktiv)."""
|
||||||
|
record_heartbeat(site, screen)
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
relevant = {
|
relevant = {
|
||||||
@@ -921,7 +943,7 @@ def admin(site):
|
|||||||
|
|
||||||
screens[screen_name] = screen_cfg
|
screens[screen_name] = screen_cfg
|
||||||
media_files[screen_name] = files
|
media_files[screen_name] = files
|
||||||
screen_status[screen_name] = "active" if files else "empty"
|
screen_status[screen_name] = "online" if screen_is_active(current_site, screen_name) else "offline"
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"admin.html",
|
"admin.html",
|
||||||
|
|||||||
32
history.json
32
history.json
@@ -1,4 +1,10 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-26 14:49:09",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:38:54",
|
"timestamp": "2026-06-20 16:38:54",
|
||||||
"action": "superuser_deleted",
|
"action": "superuser_deleted",
|
||||||
@@ -8,13 +14,13 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:38:54",
|
"timestamp": "2026-06-20 16:38:54",
|
||||||
"action": "user_updated",
|
"action": "user_updated",
|
||||||
"detail": "User 'test1@test.de' bearbeitet (Rolle: superuser → user)",
|
"detail": "User 'test1@test.de' bearbeitet (Rolle: superuser \u2192 user)",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:38:19",
|
"timestamp": "2026-06-20 16:38:19",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'wdqwsqws' gelöscht",
|
"detail": "Standort 'wdqwsqws' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -38,7 +44,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:29:59",
|
"timestamp": "2026-06-20 16:29:59",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'qwsqwsqs' gelöscht",
|
"detail": "Standort 'qwsqwsqs' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,7 +62,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:29:33",
|
"timestamp": "2026-06-20 16:29:33",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'wsqwdsqws' gelöscht",
|
"detail": "Standort 'wsqwdsqws' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,7 +74,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:27:58",
|
"timestamp": "2026-06-20 16:27:58",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'sdwqdwddd' gelöscht",
|
"detail": "Standort 'sdwqdwddd' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,31 +86,31 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:26:50",
|
"timestamp": "2026-06-20 16:26:50",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'test1' gelöscht",
|
"detail": "Standort 'test1' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:26:43",
|
"timestamp": "2026-06-20 16:26:43",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'test2' gelöscht",
|
"detail": "Standort 'test2' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:26:40",
|
"timestamp": "2026-06-20 16:26:40",
|
||||||
"action": "screen_deleted",
|
"action": "screen_deleted",
|
||||||
"detail": "Screen 'dasdsfs' an Standort 'test2' gelöscht",
|
"detail": "Screen 'dasdsfs' an Standort 'test2' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:26:36",
|
"timestamp": "2026-06-20 16:26:36",
|
||||||
"action": "screen_deleted",
|
"action": "screen_deleted",
|
||||||
"detail": "Screen 'asdwedwed' an Standort 'test2' gelöscht",
|
"detail": "Screen 'asdwedwed' an Standort 'test2' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:22:12",
|
"timestamp": "2026-06-20 16:22:12",
|
||||||
"action": "site_deleted",
|
"action": "site_deleted",
|
||||||
"detail": "Standort 'dwedwed' gelöscht",
|
"detail": "Standort 'dwedwed' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -134,7 +140,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:19:27",
|
"timestamp": "2026-06-20 16:19:27",
|
||||||
"action": "screen_deleted",
|
"action": "screen_deleted",
|
||||||
"detail": "Screen 'test2' an Standort 'test2' gelöscht",
|
"detail": "Screen 'test2' an Standort 'test2' gel\u00f6scht",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,7 +164,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:16:32",
|
"timestamp": "2026-06-20 16:16:32",
|
||||||
"action": "password_changed",
|
"action": "password_changed",
|
||||||
"detail": "Passwort für User 'erik.thiele@cancom.de' geändert",
|
"detail": "Passwort f\u00fcr User 'erik.thiele@cancom.de' ge\u00e4ndert",
|
||||||
"user": "erik.thiele@cancom.de"
|
"user": "erik.thiele@cancom.de"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -170,7 +176,7 @@
|
|||||||
{
|
{
|
||||||
"timestamp": "2026-06-20 16:14:03",
|
"timestamp": "2026-06-20 16:14:03",
|
||||||
"action": "user_updated",
|
"action": "user_updated",
|
||||||
"detail": "User 'test1@test.de' bearbeitet (Rolle: user → superuser)",
|
"detail": "User 'test1@test.de' bearbeitet (Rolle: user \u2192 superuser)",
|
||||||
"user": "admin"
|
"user": "admin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
8704
session-ses_1e96.md
8704
session-ses_1e96.md
File diff suppressed because one or more lines are too long
@@ -175,13 +175,13 @@
|
|||||||
<div class="page-subtitle"><a href="{{ server_url }}/player/{{ current_site }}/{{ screen }}">{{ server_url }}/player/{{ current_site }}/{{ screen }}</a></div>
|
<div class="page-subtitle"><a href="{{ server_url }}/player/{{ current_site }}/{{ screen }}">{{ server_url }}/player/{{ current_site }}/{{ screen }}</a></div>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
{% if screen_status[screen] == "active" %}
|
{% if screen_status[screen] == "online" %}
|
||||||
<span class="badge bg-green text-green-fg">Aktiv</span>
|
<button type="button" class="btn btn-sm btn-success" onclick="return false;">Online</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning text-dark">Leer</span>
|
<button type="button" class="btn btn-sm btn-danger" onclick="return false;">Offline</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="/admin/{{ current_site }}/delete-screen/{{ screen }}"
|
<form action="/admin/{{ current_site }}/delete-screen/{{ screen }}"
|
||||||
method="post" class="d-inline"
|
method="post" class="d-inline m-0 p-0"
|
||||||
onsubmit="return confirm('Screen »{{ screen }}« wirklich löschen? Alle Medien werden gelöscht.')">
|
onsubmit="return confirm('Screen »{{ screen }}« wirklich löschen? Alle Medien werden gelöscht.')">
|
||||||
<button type="submit" class="btn btn-outline-danger btn-sm">Löschen</button>
|
<button type="submit" class="btn btn-outline-danger btn-sm">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user