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:
@@ -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).
|
- 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**: 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**.
|
- 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.
|
||||||
|
|||||||
12
README.md
12
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
|
||||||
- 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).
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
21
app.py
21
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
|
||||||
|
|
||||||
@@ -980,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):
|
||||||
@@ -1010,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)
|
||||||
@@ -1200,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
|
||||||
|
|
||||||
@@ -1608,7 +1610,16 @@ def update_playlist_item(site, screen):
|
|||||||
for i, item in enumerate(playlist):
|
for i, item in enumerate(playlist):
|
||||||
name = playlist_item_name(item)
|
name = playlist_item_name(item)
|
||||||
if name == old_name:
|
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)
|
save_config(config)
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
|||||||
@@ -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 -->
|
||||||
@@ -290,7 +290,8 @@
|
|||||||
data-zoom="{{ file.zoom | default(1.0) }}"
|
data-zoom="{{ file.zoom | default(1.0) }}"
|
||||||
data-site="{{ current_site }}"
|
data-site="{{ current_site }}"
|
||||||
data-screen="{{ screen }}"
|
data-screen="{{ screen }}"
|
||||||
data-oldname="{{ file.name | e }}">
|
data-oldname="{{ file.name | e }}"
|
||||||
|
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -552,6 +553,7 @@ document.addEventListener("click", function (e) {
|
|||||||
editState.oldName = btn.dataset.oldname;
|
editState.oldName = btn.dataset.oldname;
|
||||||
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
||||||
document.getElementById("edit-zoom-input").value = btn.dataset.zoom;
|
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";
|
document.getElementById("edit-url-error").style.display = "none";
|
||||||
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
||||||
});
|
});
|
||||||
@@ -629,7 +631,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
old_name: editState.oldName,
|
old_name: editState.oldName,
|
||||||
new_url: newUrl,
|
new_url: newUrl,
|
||||||
zoom: zoom
|
zoom: zoom,
|
||||||
|
passthrough: document.getElementById("edit-passthrough-input").checked
|
||||||
})
|
})
|
||||||
}).then(function (r) {
|
}).then(function (r) {
|
||||||
if (r.ok) {
|
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>
|
<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">
|
<input type="number" class="form-control" id="edit-zoom-input" step="0.1" min="0.1" value="1.0">
|
||||||
</div>
|
</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 class="alert alert-danger py-2" id="edit-url-error" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -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.</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>
|
||||||
|
|||||||
@@ -456,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 = appendParams(item.url, playerParams);
|
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;
|
||||||
|
|||||||
@@ -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" %}
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
data-zoom="{{ file.zoom | default(1.0) }}"
|
data-zoom="{{ file.zoom | default(1.0) }}"
|
||||||
data-site="{{ current_site }}"
|
data-site="{{ current_site }}"
|
||||||
data-screen="priority"
|
data-screen="priority"
|
||||||
data-oldname="{{ file.name | e }}">
|
data-oldname="{{ file.name | e }}"
|
||||||
|
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -265,6 +266,7 @@ document.addEventListener("click", function (e) {
|
|||||||
editState.oldName = btn.dataset.oldname;
|
editState.oldName = btn.dataset.oldname;
|
||||||
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
document.getElementById("edit-url-input").value = btn.dataset.oldname;
|
||||||
document.getElementById("edit-zoom-input").value = btn.dataset.zoom;
|
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";
|
document.getElementById("edit-url-error").style.display = "none";
|
||||||
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
|
||||||
});
|
});
|
||||||
@@ -337,7 +339,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
old_name: editState.oldName,
|
old_name: editState.oldName,
|
||||||
new_url: newUrl,
|
new_url: newUrl,
|
||||||
zoom: zoom
|
zoom: zoom,
|
||||||
|
passthrough: document.getElementById("edit-passthrough-input").checked
|
||||||
})
|
})
|
||||||
}).then(function (r) {
|
}).then(function (r) {
|
||||||
if (r.ok) {
|
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>
|
<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">
|
<input type="number" class="form-control" id="edit-zoom-input" step="0.1" min="0.1" value="1.0">
|
||||||
</div>
|
</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 class="alert alert-danger py-2" id="edit-url-error" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
Reference in New Issue
Block a user