Query-Parameter-Passthrough per URL steuerbar (Checkbox im Edit-Modal), Standard aus, sichtbar via Link-Icon in Playlist

- Neue Config-Felder: passthrough_params (true/false) pro URL-Item
- Edit-Modal in admin.html + priority.html mit Checkbox
- Player checkt item.passthrough === true vor appendParams()
- Default: false (keine Parameter-Übergabe ohne Checkbox)
- Link-Icon (ti-link / ti-link-off) in Playlist-Zeile als Status-Indikator
- Doku: help.html, README.md, AGENTS.md
This commit is contained in:
Erik Thiele
2026-07-10 17:44:57 +02:00
parent 6efce5b2ee
commit 22104b54cd
7 changed files with 51 additions and 13 deletions

View File

@@ -132,7 +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).
- `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.
- **Query-Parameter-Passthrough**: Alle Query-Parameter, die an die Player-URL (`/player/<site>/<screen>`) angehängt werden, werden automatisch an alle Playlist-URLs (iframe + Overlay) weitergereicht. Beispiel: `/player/stuttgart/lobby?customer=acme` hängt `?customer=acme` an jede URL in der Playlist an. Funktioniert über `appendParams()` in `player.html` + `player_params` im Flask-Route.
- **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. 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**.
- `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.

View File

@@ -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
- **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)
- **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
- Priority-Seite ebenfalls mit Tabs: Playlist und Medien
- Dark Mode (localStorage-persistiert)
- CI-konformes Admin-UI (CANCOM-Design: `brand-surface`, `nav-surface` rot)
@@ -271,6 +272,17 @@ Beispiel:
http://localhost:5005/player/stuttgart/lobby
```
### Query-Parameter-Passthrough
Die Player-URL akzeptiert beliebige Query-Parameter, z.&thinsp;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).
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
```

21
app.py
View File

@@ -252,7 +252,8 @@ def load_priority_files():
"type": "url",
"size": "URL",
"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
@@ -980,7 +981,7 @@ def player(site, screen):
if is_url(item):
url_data = normalize_url(item)
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
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
@@ -1010,7 +1011,7 @@ def player(site, screen):
if is_url(item):
url_data = normalize_url(item)
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
file_path = os.path.join(MEDIA_DIR, "priority", item_name)
@@ -1200,7 +1201,8 @@ def admin(site):
"type": "url",
"size": "URL",
"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
@@ -1608,7 +1610,16 @@ def update_playlist_item(site, screen):
for i, item in enumerate(playlist):
name = playlist_item_name(item)
if name == old_name:
playlist[i] = {"url": new_url, "zoom": zoom}
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})

View File

@@ -250,7 +250,7 @@
data-bs-toggle="tooltip"
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% 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>
<!-- Typ-Badge -->
@@ -290,7 +290,8 @@
data-zoom="{{ file.zoom | default(1.0) }}"
data-site="{{ current_site }}"
data-screen="{{ screen }}"
data-oldname="{{ file.name | e }}">
data-oldname="{{ file.name | e }}"
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
Edit
</button>
{% else %}
@@ -552,6 +553,7 @@ document.addEventListener("click", function (e) {
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();
});
@@ -629,7 +631,8 @@ document.addEventListener("DOMContentLoaded", function () {
body: JSON.stringify({
old_name: editState.oldName,
new_url: newUrl,
zoom: zoom
zoom: zoom,
passthrough: document.getElementById("edit-passthrough-input").checked
})
}).then(function (r) {
if (r.ok) {
@@ -666,6 +669,10 @@ document.addEventListener("DOMContentLoaded", function () {
<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">

View File

@@ -105,6 +105,7 @@
<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>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.</li>
</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>
</div>

View File

@@ -456,7 +456,7 @@ function playNext() {
// URL im iframe anzeigen (mit Zoom-Unterstützung)
if (item.kind === "url") {
iframe.style.display = "block";
iframe.src = appendParams(item.url, playerParams);
iframe.src = (item.passthrough === true) ? appendParams(item.url, playerParams) : item.url;
const zoom = item.zoom || 1.0;
if (zoom !== 1.0) {
const scale = 1 / zoom;

View File

@@ -152,7 +152,7 @@
data-bs-toggle="tooltip"
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% 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>
{% if file.type == "image" %}
@@ -192,7 +192,8 @@
data-zoom="{{ file.zoom | default(1.0) }}"
data-site="{{ current_site }}"
data-screen="priority"
data-oldname="{{ file.name | e }}">
data-oldname="{{ file.name | e }}"
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
Edit
</button>
{% else %}
@@ -265,6 +266,7 @@ document.addEventListener("click", function (e) {
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();
});
@@ -337,7 +339,8 @@ document.addEventListener("DOMContentLoaded", function () {
body: JSON.stringify({
old_name: editState.oldName,
new_url: newUrl,
zoom: zoom
zoom: zoom,
passthrough: document.getElementById("edit-passthrough-input").checked
})
}).then(function (r) {
if (r.ok) {
@@ -374,6 +377,10 @@ document.addEventListener("DOMContentLoaded", function () {
<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">