Compare commits
16 Commits
bfbd970f3b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7870c889f8 | ||
|
|
6f82bda70c | ||
|
|
bf95b3173b | ||
|
|
22104b54cd | ||
|
|
6efce5b2ee | ||
|
|
5ee9e0b373 | ||
|
|
fca129b367 | ||
|
|
1480960801 | ||
|
|
b8093521a0 | ||
|
|
be00342109 | ||
|
|
a8cbeb089f | ||
|
|
f1c36f3c99 | ||
|
|
f8b7a94845 | ||
|
|
ce4cd4486b | ||
|
|
106ec2d897 | ||
|
|
b9e7b2e074 |
@@ -132,6 +132,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<name>` angelegt werden (nur Admins).
|
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<name>` angelegt werden (nur Admins).
|
||||||
- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv (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.
|
- `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet.
|
||||||
|
- **Query-Parameter-Passthrough**: Query-Parameter an der Player-URL (`/player/<site>/<screen>`) können pro Playlist-URL im Edit-Modal (Checkbox) gesteuert werden. Standardmäßig **aus** – nur URLs mit aktivierter Checkbox erhalten die Parameter. Beispiel: `/player/stuttgart/lobby?customer=acme` hängt `?customer=acme` nur an URLs mit Passthrough=an. In der Playlist-Zeile zeigt ein Link-Icon (`ti-link`, grau) den Status an: sichtbar = an, durchgestrichen (`ti-link-off`, hellgrau) = aus. Funktioniert über `appendParams()` in `player.html` + `player_params` im Flask-Route.
|
||||||
- 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**.
|
- 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).
|
- `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 → Info.
|
- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Digital Voice Agent → Medien → Info.
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -25,6 +25,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
|||||||
- **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
|
||||||
- **Aktivitätsverlauf**: Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden in `history.json` protokolliert und im Dashboard angezeigt
|
- **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)
|
- Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien, Info (Tabler Tabs)
|
||||||
|
- **Query-Parameter-Passthrough**: Per Player-URL übergebene Query-Parameter (z. B. `/player/stuttgart/lobby?customer=acme`) können pro Playlist-URL im Edit-Modal aktiviert werden – standardmäßig aus. Link-Icon in der Playlist-Zeile zeigt Status (an/aus) an.
|
||||||
- Priority-Seite ebenfalls mit Tabs: Playlist und Medien
|
- Priority-Seite ebenfalls mit Tabs: Playlist und Medien
|
||||||
- Dark Mode (localStorage-persistiert)
|
- Dark Mode (localStorage-persistiert)
|
||||||
- CI-konformes Admin-UI (CANCOM-Design: `brand-surface`, `nav-surface` rot)
|
- CI-konformes Admin-UI (CANCOM-Design: `brand-surface`, `nav-surface` rot)
|
||||||
@@ -271,6 +272,17 @@ Beispiel:
|
|||||||
http://localhost:5005/player/stuttgart/lobby
|
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
|
### Willkommensseite
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -292,6 +304,22 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### OpenAI API-Key
|
||||||
|
|
||||||
|
Die Logo-Suche via OpenAI benötigt einen API-Key. Diesen als Umgebungsvariable setzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY=sk-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder via `.env`-Datei (wird von Docker Compose automatisch geladen):
|
||||||
|
|
||||||
|
```
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Globale Konfiguration (config.json)
|
### Globale Konfiguration (config.json)
|
||||||
|
|
||||||
| Feld | Typ | Beschreibung |
|
| Feld | Typ | Beschreibung |
|
||||||
|
|||||||
88
app.py
88
app.py
@@ -252,7 +252,8 @@ def load_priority_files():
|
|||||||
"type": "url",
|
"type": "url",
|
||||||
"size": "URL",
|
"size": "URL",
|
||||||
"zoom": url_data.get("zoom", 1.0),
|
"zoom": url_data.get("zoom", 1.0),
|
||||||
"enabled": playlist_item_enabled(item)
|
"enabled": playlist_item_enabled(item),
|
||||||
|
"passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -517,9 +518,6 @@ def login():
|
|||||||
# Trusted Device prüfen (Cookie überspringt MFA)
|
# Trusted Device prüfen (Cookie überspringt MFA)
|
||||||
trust_token = request.cookies.get("mfa_trust")
|
trust_token = request.cookies.get("mfa_trust")
|
||||||
if trust_token:
|
if trust_token:
|
||||||
users_data = load_users()
|
|
||||||
user_data = users_data.get(email, {})
|
|
||||||
trusted = user_data.get("trusted_devices", [])
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
# Abgelaufene Einträge bereinigen
|
# Abgelaufene Einträge bereinigen
|
||||||
active = [d for d in trusted
|
active = [d for d in trusted
|
||||||
@@ -537,6 +535,8 @@ def login():
|
|||||||
if user.must_change_password:
|
if user.must_change_password:
|
||||||
return redirect("/change-password")
|
return redirect("/change-password")
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
pass
|
pass
|
||||||
if changed:
|
if changed:
|
||||||
@@ -566,9 +566,7 @@ def logout():
|
|||||||
email = current_user.email if current_user.is_authenticated else "unknown"
|
email = current_user.email if current_user.is_authenticated else "unknown"
|
||||||
logout_user()
|
logout_user()
|
||||||
add_history_entry("user_logout", f"User '{email}' abgemeldet", email)
|
add_history_entry("user_logout", f"User '{email}' abgemeldet", email)
|
||||||
resp = redirect("/login")
|
return redirect("/login")
|
||||||
resp.delete_cookie("mfa_trust")
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@@ -597,6 +595,8 @@ def mfa_verify():
|
|||||||
totp = pyotp.TOTP(user.mfa_secret)
|
totp = pyotp.TOTP(user.mfa_secret)
|
||||||
|
|
||||||
if code and totp.verify(code):
|
if code and totp.verify(code):
|
||||||
|
login_user(user)
|
||||||
|
session.pop("mfa_user", None)
|
||||||
# Trusted Device speichern wenn gewünscht
|
# Trusted Device speichern wenn gewünscht
|
||||||
remember_days = request.form.get("remember")
|
remember_days = request.form.get("remember")
|
||||||
if remember_days:
|
if remember_days:
|
||||||
@@ -604,28 +604,22 @@ def mfa_verify():
|
|||||||
days = int(remember_days)
|
days = int(remember_days)
|
||||||
if 1 <= days <= 365:
|
if 1 <= days <= 365:
|
||||||
token = secrets.token_urlsafe(32)
|
token = secrets.token_urlsafe(32)
|
||||||
expires = datetime.now().isoformat()
|
exp_date = datetime.now() + timedelta(days=days)
|
||||||
exp_date = datetime.now()
|
|
||||||
exp_date = exp_date + timedelta(days=days)
|
|
||||||
expires = exp_date.isoformat()
|
expires = exp_date.isoformat()
|
||||||
users_data = load_users()
|
users_data = load_users()
|
||||||
trusted = users_data.setdefault(email, {}).setdefault("trusted_devices", [])
|
trusted = users_data.setdefault(email, {}).setdefault("trusted_devices", [])
|
||||||
trusted.append({"token": token, "expires": expires})
|
trusted.append({"token": token, "expires": expires})
|
||||||
save_users(users_data)
|
save_users(users_data)
|
||||||
resp = redirect("/admin")
|
add_history_entry("user_login", f"User '{email}' angemeldet (mit MFA, Gerät für {days} Tage gespeichert)", email)
|
||||||
|
target = "/change-password" if user.must_change_password else "/admin"
|
||||||
|
resp = redirect(target)
|
||||||
resp.set_cookie("mfa_trust", token, max_age=days * 86400,
|
resp.set_cookie("mfa_trust", token, max_age=days * 86400,
|
||||||
|
path="/",
|
||||||
httponly=True, samesite="Lax",
|
httponly=True, samesite="Lax",
|
||||||
secure=False)
|
secure=False)
|
||||||
login_user(user)
|
|
||||||
session.pop("mfa_user", None)
|
|
||||||
add_history_entry("user_login", f"User '{email}' angemeldet (mit MFA, Gerät für {days} Tage gespeichert)", email)
|
|
||||||
if user.must_change_password:
|
|
||||||
resp = redirect("/change-password")
|
|
||||||
return resp
|
return resp
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
login_user(user)
|
|
||||||
session.pop("mfa_user", None)
|
|
||||||
add_history_entry("user_login", f"User '{email}' angemeldet (mit MFA)", email)
|
add_history_entry("user_login", f"User '{email}' angemeldet (mit MFA)", email)
|
||||||
if user.must_change_password:
|
if user.must_change_password:
|
||||||
return redirect("/change-password")
|
return redirect("/change-password")
|
||||||
@@ -987,7 +981,7 @@ def player(site, screen):
|
|||||||
if is_url(item):
|
if is_url(item):
|
||||||
url_data = normalize_url(item)
|
url_data = normalize_url(item)
|
||||||
if url_data:
|
if url_data:
|
||||||
normal_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True})
|
normal_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True, "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
|
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
|
||||||
@@ -1017,7 +1011,7 @@ def player(site, screen):
|
|||||||
if is_url(item):
|
if is_url(item):
|
||||||
url_data = normalize_url(item)
|
url_data = normalize_url(item)
|
||||||
if url_data:
|
if url_data:
|
||||||
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True})
|
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True, "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(MEDIA_DIR, "priority", item_name)
|
file_path = os.path.join(MEDIA_DIR, "priority", item_name)
|
||||||
@@ -1046,7 +1040,8 @@ def player(site, screen):
|
|||||||
voice_agent_url=config.get("voice_agent_url", ""),
|
voice_agent_url=config.get("voice_agent_url", ""),
|
||||||
voice_agent_label=screen_cfg.get("voice_agent_label", "Digitaler Assistent"),
|
voice_agent_label=screen_cfg.get("voice_agent_label", "Digitaler Assistent"),
|
||||||
voice_agent_target=screen_cfg.get("voice_agent_target", "overlay"),
|
voice_agent_target=screen_cfg.get("voice_agent_target", "overlay"),
|
||||||
voice_agent_position=screen_cfg.get("voice_agent_position", "top-left")
|
voice_agent_position=screen_cfg.get("voice_agent_position", "top-left"),
|
||||||
|
player_params=dict(request.args)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1206,7 +1201,8 @@ def admin(site):
|
|||||||
"type": "url",
|
"type": "url",
|
||||||
"size": "URL",
|
"size": "URL",
|
||||||
"zoom": url_data.get("zoom", 1.0),
|
"zoom": url_data.get("zoom", 1.0),
|
||||||
"enabled": playlist_item_enabled(item)
|
"enabled": playlist_item_enabled(item),
|
||||||
|
"passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1582,6 +1578,54 @@ def save_playlist(site, screen):
|
|||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Admin: Playlist-Item bearbeiten (URL + Zoom)
|
||||||
|
# -------------------------------------------------
|
||||||
|
@app.route("/admin/<site>/update-playlist-item/<screen>", methods=["POST"])
|
||||||
|
@site_access_required
|
||||||
|
def update_playlist_item(site, screen):
|
||||||
|
"""Aktualisiert URL und Zoom eines einzelnen Playlist-Items."""
|
||||||
|
data = request.get_json()
|
||||||
|
old_name = data.get("old_name")
|
||||||
|
new_url = data.get("new_url", "").strip()
|
||||||
|
zoom = data.get("zoom", 1.0)
|
||||||
|
|
||||||
|
if not is_url(new_url):
|
||||||
|
return jsonify({"error": "Ungültige URL"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
zoom = float(zoom)
|
||||||
|
if zoom <= 0:
|
||||||
|
zoom = 1.0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
zoom = 1.0
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
if screen == "priority":
|
||||||
|
playlist = config.setdefault("priority", {}).setdefault("playlist", [])
|
||||||
|
else:
|
||||||
|
playlist = config["sites"][site]["screens"][screen].setdefault("playlist", [])
|
||||||
|
|
||||||
|
for i, item in enumerate(playlist):
|
||||||
|
name = playlist_item_name(item)
|
||||||
|
if name == old_name:
|
||||||
|
updated = {"url": new_url, "zoom": zoom}
|
||||||
|
passthrough = data.get("passthrough", None)
|
||||||
|
if passthrough is not None:
|
||||||
|
updated["passthrough_params"] = bool(passthrough)
|
||||||
|
else:
|
||||||
|
# Bestehenden Wert übernehmen falls vorhanden
|
||||||
|
existing = item.get("passthrough_params") if isinstance(item, dict) else None
|
||||||
|
if existing is not None:
|
||||||
|
updated["passthrough_params"] = existing
|
||||||
|
playlist[i] = updated
|
||||||
|
save_config(config)
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
return jsonify({"error": "Item nicht gefunden"}), 404
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Change-Password (für First-Login / Reset)
|
# Change-Password (für First-Login / Reset)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|||||||
10
config.json
10
config.json
@@ -19,7 +19,10 @@
|
|||||||
"show_images": true,
|
"show_images": true,
|
||||||
"show_videos": true,
|
"show_videos": true,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"welcome.html",
|
{
|
||||||
|
"name": "welcome.html",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://www.meteoblue.com/en/meteotv/d7b0fd",
|
"url": "https://www.meteoblue.com/en/meteotv/d7b0fd",
|
||||||
"zoom": 1.0,
|
"zoom": 1.0,
|
||||||
@@ -99,11 +102,6 @@
|
|||||||
{
|
{
|
||||||
"name": "welcome.html",
|
"name": "welcome.html",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://dva-stage.cancom-ds.de/",
|
|
||||||
"zoom": 1.0,
|
|
||||||
"enabled": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ services:
|
|||||||
- ALL
|
- ALL
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
environment:
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ WELCOME_FILENAME = "welcome.html"
|
|||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
def get_openai_client():
|
def get_openai_client():
|
||||||
"""Initialisiert den OpenAI-Client mit dem API-Key.
|
api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
Der Key ist aktuell direkt im Code hinterlegt – sollte in env var ausgelagert werden."""
|
|
||||||
api_key = "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"
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ValueError("OPENAI_API_KEY environment variable not set")
|
raise ValueError("OPENAI_API_KEY environment variable not set")
|
||||||
return OpenAI(api_key=api_key)
|
return OpenAI(api_key=api_key)
|
||||||
|
|||||||
174
history.json
174
history.json
@@ -1,4 +1,178 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:15:59",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (Trusted Device)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:15:56",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:15:51",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:13:50",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:13:46",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:11:43",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:11:39",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:10:18",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:10:14",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:07:35",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 16:07:27",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:45:29",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:45:20",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:44:47",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:41:11",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:40:59",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:40:56",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 7 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:40:37",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:40:28",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet (mit MFA, Ger\u00e4t f\u00fcr 14 Tage gespeichert)",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:40:03",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' abgemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:39:57",
|
||||||
|
"action": "mfa_enabled",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' hat MFA aktiviert",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:39:32",
|
||||||
|
"action": "password_changed",
|
||||||
|
"detail": "Passwort f\u00fcr User 'erik.thiele@cancom.de' ge\u00e4ndert",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:39:22",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'erik.thiele@cancom.de' angemeldet",
|
||||||
|
"user": "erik.thiele@cancom.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:39:15",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'admin' abgemeldet",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:38:55",
|
||||||
|
"action": "user_password_reset",
|
||||||
|
"detail": "Passwort f\u00fcr User 'erik.thiele@cancom.de' zur\u00fcckgesetzt (MFA deaktiviert)",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:38:49",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'admin' angemeldet",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:31:55",
|
||||||
|
"action": "user_logout",
|
||||||
|
"detail": "User 'admin' abgemeldet",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:31:42",
|
||||||
|
"action": "user_password_reset",
|
||||||
|
"detail": "Passwort f\u00fcr User 'erik.thiele@cancom.de' zur\u00fcckgesetzt",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-10 15:31:14",
|
||||||
|
"action": "user_login",
|
||||||
|
"detail": "User 'admin' angemeldet",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"timestamp": "2026-07-02 12:25:24",
|
"timestamp": "2026-07-02 12:25:24",
|
||||||
"action": "user_login",
|
"action": "user_login",
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Playlist-Zeile als Grid (Checkbox | Name | Typ | Größe | Löschen | Drag) ─── */
|
/* ─── Playlist-Zeile als Grid (Checkbox | Name | Typ | Größe | Löschen | Edit | Drag) ─── */
|
||||||
.playlist-row {
|
.playlist-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
28px 1fr 90px 80px 90px 32px;
|
28px 1fr 90px 80px 50px 40px 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}{% if file.type == "url" %}{% if file.get('passthrough', False) %} <i class="ti ti-link" style="color:#6c757d;" title="Query-Parameter werden übergeben"></i>{% else %} <i class="ti ti-link-off" style="color:#adb5bd;" title="Query-Parameter werden nicht übergeben"></i>{% endif %}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Typ-Badge -->
|
<!-- Typ-Badge -->
|
||||||
@@ -281,6 +281,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Bearbeiten (nur URLs) -->
|
||||||
|
{% if file.type == "url" %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm edit-url-btn"
|
||||||
|
title="URL bearbeiten"
|
||||||
|
data-name="{{ file.name | e }}"
|
||||||
|
data-zoom="{{ file.zoom | default(1.0) }}"
|
||||||
|
data-site="{{ current_site }}"
|
||||||
|
data-screen="{{ screen }}"
|
||||||
|
data-oldname="{{ file.name | e }}"
|
||||||
|
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Drag-Handle (SortableJS) -->
|
<!-- Drag-Handle (SortableJS) -->
|
||||||
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -523,9 +540,26 @@
|
|||||||
</div> <!-- /page -->
|
</div> <!-- /page -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
/* ─── Playlist-Item bearbeiten (URL + Zoom) ─── */
|
||||||
|
var editState = { site: "", screen: "", oldName: "" };
|
||||||
|
|
||||||
|
/* Event-Delegation für Edit-Buttons (global, sofort aktiv) */
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
var btn = e.target.closest(".edit-url-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
e.preventDefault();
|
||||||
|
editState.site = btn.dataset.site;
|
||||||
|
editState.screen = btn.dataset.screen;
|
||||||
|
editState.oldName = btn.dataset.oldname;
|
||||||
|
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
||||||
|
document.getElementById("edit-zoom-input").value = btn.dataset.zoom;
|
||||||
|
document.getElementById("edit-passthrough-input").checked = btn.dataset.passthrough === "true";
|
||||||
|
document.getElementById("edit-url-error").style.display = "none";
|
||||||
|
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
/* ─── SortableJS Drag & Drop initialisieren ─── */
|
|
||||||
function initSortable(name) {
|
function initSortable(name) {
|
||||||
const el = document.getElementById("playlist-" + name);
|
const el = document.getElementById("playlist-" + name);
|
||||||
if (!el || typeof Sortable === "undefined") return;
|
if (!el || typeof Sortable === "undefined") return;
|
||||||
@@ -577,8 +611,77 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ─── Edit-URL-Modal: Speichern-Button ─── */
|
||||||
|
document.getElementById("edit-url-save").addEventListener("click", function () {
|
||||||
|
const newUrl = document.getElementById("edit-url-input").value.trim();
|
||||||
|
const zoom = parseFloat(document.getElementById("edit-zoom-input").value) || 1.0;
|
||||||
|
const errorEl = document.getElementById("edit-url-error");
|
||||||
|
|
||||||
|
if (!newUrl.startsWith("http://") && !newUrl.startsWith("https://")) {
|
||||||
|
errorEl.textContent = "Ungültige URL (muss mit http:// oder https:// beginnen)";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEl.style.display = "none";
|
||||||
|
|
||||||
|
fetch("/admin/" + editState.site + "/update-playlist-item/" + editState.screen, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
old_name: editState.oldName,
|
||||||
|
new_url: newUrl,
|
||||||
|
zoom: zoom,
|
||||||
|
passthrough: document.getElementById("edit-passthrough-input").checked
|
||||||
|
})
|
||||||
|
}).then(function (r) {
|
||||||
|
if (r.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
return r.json().then(function (d) {
|
||||||
|
errorEl.textContent = d.error || "Fehler beim Speichern";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(function () {
|
||||||
|
errorEl.textContent = "Netzwerkfehler";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Modal: URL bearbeiten -->
|
||||||
|
<div class="modal fade" id="edit-url-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">URL bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">URL</label>
|
||||||
|
<input type="url" class="form-control" id="edit-url-input">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
|
||||||
|
<input type="number" class="form-control" id="edit-zoom-input" step="0.1" min="0.1" value="1.0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="edit-passthrough-input">
|
||||||
|
<label class="form-check-label" for="edit-passthrough-input">Query-Parameter an diese URL übergeben</label>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-danger py-2" id="edit-url-error" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="edit-url-save">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
<li><strong>Sortieren:</strong> Per Drag & Drop die Reihenfolge der Playlist-Einträge ändern (Ziehgriff links).</li>
|
<li><strong>Sortieren:</strong> Per Drag & Drop die Reihenfolge der Playlist-Einträge ändern (Ziehgriff links).</li>
|
||||||
<li><strong>Deaktivieren:</strong> Mit der Checkbox einzelne Einträge temporär ausblenden.</li>
|
<li><strong>Deaktivieren:</strong> Mit der Checkbox einzelne Einträge temporär ausblenden.</li>
|
||||||
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
|
<li><strong>Löschen:</strong> Über den roten Papierkorb-Button.</li>
|
||||||
|
<li><strong>Query-Parameter-Passthrough:</strong> Per URL-Edit-Button (Stift-Icon) im Playlist-Eintrag kann pro URL gesteuert werden, ob Query-Parameter von der Player-URL an die Playlist-URL übergeben werden. Standardmäßig <strong>aus</strong> – Checkbox "Query-Parameter an diese URL übergeben" aktivieren wenn gewünscht. Ein Link-Icon in der Playlist-Zeile zeigt den Status: <i class="ti ti-link"></i> = an, <i class="ti ti-link-off"></i> = aus.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Änderungen an der Playlist werden automatisch gespeichert. Der Player lädt die Seite automatisch neu (via Hash-Prüfung).</p>
|
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Änderungen an der Playlist werden automatisch gespeichert. Der Player lädt die Seite automatisch neu (via Hash-Prüfung).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -324,7 +324,7 @@
|
|||||||
// ─── Custom-URL/Voice-Agent-Overlay (open/close mit Zurück-Button-Position-Tracking) ───
|
// ─── Custom-URL/Voice-Agent-Overlay (open/close mit Zurück-Button-Position-Tracking) ───
|
||||||
function openOverlay(url, pos) {
|
function openOverlay(url, pos) {
|
||||||
document.getElementById("overlay").style.display = "block";
|
document.getElementById("overlay").style.display = "block";
|
||||||
document.getElementById("overlay-iframe").src = url;
|
document.getElementById("overlay-iframe").src = appendParams(url, playerParams);
|
||||||
var backBtn = document.getElementById("overlay-back");
|
var backBtn = document.getElementById("overlay-back");
|
||||||
backBtn.className = "overlay-back-" + (pos || "top-left");
|
backBtn.className = "overlay-back-" + (pos || "top-left");
|
||||||
}
|
}
|
||||||
@@ -352,6 +352,14 @@ const interval = {{ interval }} * 1000;
|
|||||||
let stayOnFirst = {{ "true" if stay_on_first else "false" }};
|
let stayOnFirst = {{ "true" if stay_on_first else "false" }};
|
||||||
const screen = "{{ screen }}";
|
const screen = "{{ screen }}";
|
||||||
const site = "{{ site }}";
|
const site = "{{ site }}";
|
||||||
|
const playerParams = {{ player_params | tojson }};
|
||||||
|
|
||||||
|
// Query-Parameter aus der Player-URL an URLs anhängen
|
||||||
|
function appendParams(url, params) {
|
||||||
|
if (!params || Object.keys(params).length === 0) return url;
|
||||||
|
const sep = url.includes('?') ? '&' : '?';
|
||||||
|
return url + sep + new URLSearchParams(params).toString();
|
||||||
|
}
|
||||||
// stayOnFirst: Player bleibt auf erstem Element (kein Timer/Advance)
|
// stayOnFirst: Player bleibt auf erstem Element (kein Timer/Advance)
|
||||||
|
|
||||||
// ─── Player-Status ───
|
// ─── Player-Status ───
|
||||||
@@ -448,7 +456,7 @@ function playNext() {
|
|||||||
// URL im iframe anzeigen (mit Zoom-Unterstützung)
|
// URL im iframe anzeigen (mit Zoom-Unterstützung)
|
||||||
if (item.kind === "url") {
|
if (item.kind === "url") {
|
||||||
iframe.style.display = "block";
|
iframe.style.display = "block";
|
||||||
iframe.src = item.url;
|
iframe.src = (item.passthrough === true) ? appendParams(item.url, playerParams) : item.url;
|
||||||
const zoom = item.zoom || 1.0;
|
const zoom = item.zoom || 1.0;
|
||||||
if (zoom !== 1.0) {
|
if (zoom !== 1.0) {
|
||||||
const scale = 1 / zoom;
|
const scale = 1 / zoom;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
.playlist-row {
|
.playlist-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
28px 1fr 90px 80px 90px 32px;
|
28px 1fr 90px 80px 50px 40px 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}{% if file.type == "url" %}{% if file.get('passthrough', False) %} <i class="ti ti-link" style="color:#6c757d;" title="Query-Parameter werden übergeben"></i>{% else %} <i class="ti ti-link-off" style="color:#adb5bd;" title="Query-Parameter werden nicht übergeben"></i>{% endif %}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
@@ -183,6 +183,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Bearbeiten (nur URLs) -->
|
||||||
|
{% if file.type == "url" %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm edit-url-btn"
|
||||||
|
title="URL bearbeiten"
|
||||||
|
data-name="{{ file.name | e }}"
|
||||||
|
data-zoom="{{ file.zoom | default(1.0) }}"
|
||||||
|
data-site="{{ current_site }}"
|
||||||
|
data-screen="priority"
|
||||||
|
data-oldname="{{ file.name | e }}"
|
||||||
|
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Drag -->
|
<!-- Drag -->
|
||||||
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -236,6 +253,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
/* ─── Global: editState ─── */
|
||||||
|
var editState = { site: "", screen: "", oldName: "" };
|
||||||
|
|
||||||
|
/* Event-Delegation für Edit-Buttons (global, sofort aktiv) */
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
var btn = e.target.closest(".edit-url-btn");
|
||||||
|
if (!btn) return;
|
||||||
|
e.preventDefault();
|
||||||
|
editState.site = btn.dataset.site;
|
||||||
|
editState.screen = btn.dataset.screen;
|
||||||
|
editState.oldName = btn.dataset.oldname;
|
||||||
|
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
||||||
|
document.getElementById("edit-zoom-input").value = btn.dataset.zoom;
|
||||||
|
document.getElementById("edit-passthrough-input").checked = btn.dataset.passthrough === "true";
|
||||||
|
document.getElementById("edit-url-error").style.display = "none";
|
||||||
|
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
function initSortable(name) {
|
function initSortable(name) {
|
||||||
@@ -278,14 +313,83 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
window.savePlaylist = savePlaylist;
|
window.savePlaylist = savePlaylist;
|
||||||
|
|
||||||
// Initialize tooltips
|
/* ─── Bootstrap Tooltips ─── */
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ─── Edit-URL-Modal: Speichern-Button ─── */
|
||||||
|
document.getElementById("edit-url-save").addEventListener("click", function () {
|
||||||
|
const newUrl = document.getElementById("edit-url-input").value.trim();
|
||||||
|
const zoom = parseFloat(document.getElementById("edit-zoom-input").value) || 1.0;
|
||||||
|
const errorEl = document.getElementById("edit-url-error");
|
||||||
|
|
||||||
|
if (!newUrl.startsWith("http://") && !newUrl.startsWith("https://")) {
|
||||||
|
errorEl.textContent = "Ungültige URL (muss mit http:// oder https:// beginnen)";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEl.style.display = "none";
|
||||||
|
|
||||||
|
fetch("/admin/" + editState.site + "/update-playlist-item/" + editState.screen, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
old_name: editState.oldName,
|
||||||
|
new_url: newUrl,
|
||||||
|
zoom: zoom,
|
||||||
|
passthrough: document.getElementById("edit-passthrough-input").checked
|
||||||
|
})
|
||||||
|
}).then(function (r) {
|
||||||
|
if (r.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
return r.json().then(function (d) {
|
||||||
|
errorEl.textContent = d.error || "Fehler beim Speichern";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(function () {
|
||||||
|
errorEl.textContent = "Netzwerkfehler";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Modal: URL bearbeiten -->
|
||||||
|
<div class="modal fade" id="edit-url-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">URL bearbeiten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">URL</label>
|
||||||
|
<input type="url" class="form-control" id="edit-url-input">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
|
||||||
|
<input type="number" class="form-control" id="edit-zoom-input" step="0.1" min="0.1" value="1.0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="edit-passthrough-input">
|
||||||
|
<label class="form-check-label" for="edit-passthrough-input">Query-Parameter an diese URL übergeben</label>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-danger py-2" id="edit-url-error" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="edit-url-save">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
34
users.json
34
users.json
@@ -15,10 +15,11 @@
|
|||||||
"FC204A",
|
"FC204A",
|
||||||
"1D09D7"
|
"1D09D7"
|
||||||
],
|
],
|
||||||
"mfa_secret": "VOLBXT7HYK6BXBD32EGW7CMFLVD53QON"
|
"mfa_secret": "VOLBXT7HYK6BXBD32EGW7CMFLVD53QON",
|
||||||
|
"trusted_devices": []
|
||||||
},
|
},
|
||||||
"erik.thiele@cancom.de": {
|
"erik.thiele@cancom.de": {
|
||||||
"password_hash": "scrypt:32768:8:1$gyBs4dA7DhH9wA0u$c80f25262388095545733c95dd4e569eee610fa849a11dd28d4f504d09e23d3768adad26d4ed671975bd3dcbae5dc43bd982f4d475e6bc9d2ede088cb0b6c025",
|
"password_hash": "scrypt:32768:8:1$D8XYprsEYMO5EPrG$2829b18f77bbd7d9f0cbb945b0bdbc32e16d7889f8711881f82dd35accd26ddb7c31d3fa85e3b89d19d6d1f0b92d8d18765e1c82044188cab8fd0db2d35a0d15",
|
||||||
"role": "admin",
|
"role": "admin",
|
||||||
"sites": [],
|
"sites": [],
|
||||||
"must_change_password": false,
|
"must_change_password": false,
|
||||||
@@ -26,17 +27,23 @@
|
|||||||
"last_name": "Thiele",
|
"last_name": "Thiele",
|
||||||
"department": "CSO",
|
"department": "CSO",
|
||||||
"notes": "Standort Stuttgart",
|
"notes": "Standort Stuttgart",
|
||||||
"mfa_secret": "CHKS2QZOJ45TSCLUN7FWO3MLTMRSS6Q3",
|
"mfa_secret": "LR5DYI3SQPMW5XWKO33HPETHY5TPOWRG",
|
||||||
"mfa_enabled": true,
|
"mfa_enabled": true,
|
||||||
"mfa_recovery_codes": [
|
"mfa_recovery_codes": [
|
||||||
"1FB378",
|
"7BBD93",
|
||||||
"AAD593",
|
"EB1D3B",
|
||||||
"B4BCB9",
|
"BEE291",
|
||||||
"9CB5C3",
|
"D471D5",
|
||||||
"22D614",
|
"D43C00",
|
||||||
"26F3B7",
|
"75DBAC",
|
||||||
"47FB5C",
|
"8B00CB",
|
||||||
"50BB59"
|
"7AF72D"
|
||||||
|
],
|
||||||
|
"trusted_devices": [
|
||||||
|
{
|
||||||
|
"token": "5kMgj4JPxd2IHuzV5MagAAcYyi_DBImyGimBSvZUNVg",
|
||||||
|
"expires": "2026-07-24T15:40:28.955130"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"test1@test.de": {
|
"test1@test.de": {
|
||||||
@@ -51,6 +58,7 @@
|
|||||||
"first_name": "test",
|
"first_name": "test",
|
||||||
"last_name": "tester",
|
"last_name": "tester",
|
||||||
"department": "addff",
|
"department": "addff",
|
||||||
"notes": "asdasdasd"
|
"notes": "asdasdasd",
|
||||||
|
"trusted_devices": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user