Compare commits

...

2 Commits

Author SHA1 Message Date
Erik Thiele
d2938f87f3 fix: Priority-Playlist je Standort statt global
Die Priority-Playlist lag unter config["priority"] und wirkte damit auf
*alle* Standorte. In der Praxis lief so ein Stuttgart-spezifischer
Eintrag (wbxroompresence...find=Stuttgart) auch auf den Karlsruher
Screens.

Sie liegt jetzt unter sites.<site>.priority, die Medien unter
media/<site>/priority/:
- get_priority_config(cfg, site) liefert die Konfiguration des
  Standorts. Ist dort nichts hinterlegt, wird ein globaler Altbestand
  einmalig uebernommen - per deepcopy, damit spaetere Aenderungen nicht
  zwischen Standorten durchschlagen.
- load_priority_files(site), priority_dir(site) sowie Player, Hash,
  Upload, Loeschen, URL-Anlegen, Umsortieren und Einzeleintrag-
  Bearbeitung arbeiten durchgaengig standortbezogen.
- Route /media/priority/<datei> -> /media/<site>/priority/<datei>,
  Templates entsprechend.
- Der Ordnername "priority" ist wie "logos" in der Screen-Erkennung
  ausgenommen. media/<site>/priority/ waere sonst als Screen in der
  Konfiguration gelandet; "priority" ist in den Routen ohnehin als
  Kennzeichen belegt, ein Screen dieses Namens war nie moeglich.

Migration der vorhandenen Konfiguration: der globale Block wurde in alle
drei Standorte uebernommen und der globale Schluessel entfernt. Der
leere globale Ordner media/priority/ ist entfallen.

README, AGENTS.md und Hilfeseite nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:32:27 +02:00
Erik Thiele
a9cbe42df9 docs: Luecken in README und AGENTS.md schliessen
Abgleich der Doku gegen den Code ergab:
- /admin/help und /customer fehlten in der Routen-Tabelle
- Der Verzeichnisbaum zeigte nur die Screen-Ordner, nicht die Dateien
  auf Standort-Ebene (background.*, logo_<slot>.png, welcome.html) -
  gerade die sind aber bewusst Dateien und keine Ordner, weil jeder
  Unterordner als Screen interpretiert wird
- Der Docker-Abschnitt erwaehnte weder das noetige --build noch die
  Log-Einstellungen (PYTHONUNBUFFERED, access-logfile), die fuer jede
  Fehlersuche die Grundlage sind

AGENTS.md bekommt dazu einen Abschnitt "Betrieb / Diagnose".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:25:13 +02:00
6 changed files with 125 additions and 45 deletions

View File

@@ -29,7 +29,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
| `GET /admin/<site>` | Admin-Dashboard für einen Standort (`@site_access_required`) |
| `GET /admin/<site>/priority` | Priority-Playlist als separate Seite |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) |
| `GET /media/<site>/priority/<file>` | Priority-Medien des Standorts |
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
| `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 |
@@ -109,10 +109,11 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- `GET /admin` requires login; redirects to first accessible site for the user.
- URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows. File items may also be dicts (`{"name": "bild.jpg", ...}`); `save_playlist` copies existing dicts, so extra keys like `schedule` survive reordering.
- `.html` items in `media/` are rendered inline as content, not in an iframe.
- `config.priority.enabled` makes the priority playlist show on every screen.
- `sites.<site>.priority.enabled` makes the priority playlist show on every screen **of that site**.
- `POST /api/customer` generates `welcome.html` and inserts it at the front of the playlist of every target screen for the specified site. Target screens are those with `welcome_enabled`; if no screen of the site has the key at all, the old behaviour applies (`lobby` only). Optional `valid_until` (`"YYYY-MM-DD"`) attaches a schedule so the page disappears on its own.
- New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`.
- Priority playlist (`config.priority`) is global and affects all sites/screens.
- **Die Priority-Playlist gilt je Standort** (`sites.<site>.priority`), nicht global. Frueher lag sie unter `config.priority` und wirkte auf alle Standorte - dadurch lief Stuttgarter Inhalt auch in Karlsruhe. `get_priority_config(cfg, site)` liefert die Konfiguration des Standorts und uebernimmt einen globalen Altbestand beim ersten Zugriff einmalig (mit `deepcopy`, damit Aenderungen nicht zwischen Standorten durchschlagen).
- Priority-Medien liegen unter `media/<site>/priority/`, ausgeliefert ueber `/media/<site>/priority/<datei>`. Die Ordnernamen `priority` und `logos` sind in der Screen-Erkennung ausgenommen - sonst wuerden sie als Screens in der Konfiguration landen.
- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column).
- `generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None)` takes lists for up to 3 customers; logos have equal width (280px) with fixed height 120px, text left-aligned below each logo.
- Contact info supports Markdown: `**bold**`, `*italic*`, `` `code` ``, `- list items`, and line breaks (preserved as `<br>`).
@@ -153,13 +154,20 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden.
- Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen
## Betrieb / Diagnose
- `docker compose up -d` startet nur neu und **baut nicht**. Nach Code-Aenderungen `--build` verwenden, sonst laeuft weiter das alte Image - das hat bei der Fehlersuche mehrfach zu falschen Schluessen gefuehrt.
- Das Dockerfile setzt `PYTHONUNBUFFERED=1`. Ohne das puffert Python `stdout` blockweise (kein TTY) und **keine** `print()`-Ausgabe der App erscheint in `docker compose logs`. Tracebacks waren sichtbar, weil Flask sie ueber stderr schreibt - das machte den blinden Fleck schwer erkennbar.
- gunicorn laeuft mit `--access-logfile -`, protokolliert also jeden Request. Ohne das laesst sich nicht feststellen, ob eine Anfrage den Container ueberhaupt erreicht.
- `add_customer` protokolliert je Kundenfeld `Upload = <dateiname>` bzw. `Upload = keiner`. Ein geleertes `<input type="file">` - etwa nach einem Reload der Seite - sieht serverseitig sonst genauso aus wie "kein Logo hochgeladen".
## Repo Quirks
- `media/` and media file extensions are gitignored.
- `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default).
- The app has no configured tests, lint, typecheck, formatter, or CI.
- Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `is_item_active()`, `_schedule_time()`, `_window_start_date()`, `_in_validity_range()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`, `get_client_info()`, `add_history_entry()`.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `is_item_active()`, `_schedule_time()`, `_window_start_date()`, `_in_validity_range()`, `load_priority_files(site)`, `get_priority_config(cfg, site)`, `priority_dir(site)`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`, `get_client_info()`, `add_history_entry()`.
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<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.

View File

@@ -9,7 +9,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
- Browserbasierter Player (Kiosk-Modus, Chromium, Firefox)
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
- **Multi-Standort**: Standorte (sites) gruppieren Screens
- **Priority-Playlist**: globale Inhalte wirken auf alle Player
- **Priority-Playlist**: pro Standort; die Inhalte laufen auf allen Screens **dieses** Standorts, nicht standortübergreifend
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
- Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
- Auto-Reload bei Playlist-Änderungen
@@ -53,7 +53,7 @@ Flask App (Server)
│ │ │ ├── bild.jpg
│ │ │ ├── video.mp4
│ │ │ └── welcome.html
│ └── priority/
│ │ └── priority/
└── generate_welcome_page.py
```
@@ -75,10 +75,13 @@ signage/
├── history.json # Aktivitätsverlauf (Dashboard)
├── media/
│ ├── <site>/
│ │ ├── lobby/
│ │ ├── lobby/ # Screen-Ordner (jeder Unterordner = ein Screen!)
│ │ ├── casino/
│ │ └── videosysteme/
│ └── priority/
│ │ ├── videosysteme/
│ │ ├── background.jpg # Hintergrundbild der Willkommensseite (optional)
│ │ ├── priority/ # Priority-Medien dieses Standorts
│ │ ├── logo_1.png # hochgeladene Kundenlogos (Slot 1-3)
│ │ └── welcome.html # Standort-Kopie der Willkommensseite
├── templates/
│ ├── admin.html # Admin-Dashboard (Übersicht)
│ ├── admin_dashboard.html # Admin Dashboard (Statistiken + Verlauf)
@@ -175,6 +178,25 @@ docker compose up -d
Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User).
**Wichtig:** `docker compose up -d` startet nur neu, es baut nicht. Nach Code-Änderungen:
```bash
docker compose up -d --build
```
### Logs
```bash
docker compose logs -f signage
```
Der Container ist so eingestellt, dass dort alles Nötige ankommt:
- `ENV PYTHONUNBUFFERED=1` im Dockerfile – ohne das puffert Python `stdout` blockweise, sobald es kein Terminal ist, und sämtliche `print()`-Ausgaben der App bleiben unsichtbar.
- `gunicorn --access-logfile -` – protokolliert jeden Request. Ohne das ist nicht einmal erkennbar, ob eine Anfrage den Container überhaupt erreicht.
Beides fehlte anfangs und machte die Fehlersuche unnötig schwer.
---
## Routen
@@ -220,8 +242,10 @@ Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User
| `POST /admin/users/reset-password/<email>` | Passwort-Reset mit temporärem Passwort (Admin) |
| `POST /admin/users/edit/<email>` | User bearbeiten speichern (Admin) |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) |
| `GET /media/<site>/priority/<file>` | Priority-Medien des Standorts |
| `GET /media/<site>/logos/<file>` | Manuell hochgeladenes Kundenlogo |
| `GET /admin/help` | Hilfeseite zur Bedienung |
| `GET /customer` | Alias für `/willkommen` |
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
### Heartbeat / Online-Status
@@ -403,10 +427,6 @@ OPENAI_API_KEY=sk-...
"background_url": null
}
}
},
"priority": {
"enabled": true,
"playlist": [...]
}
}
```
@@ -436,6 +456,30 @@ OPENAI_API_KEY=sk-...
| `voice_agent_target` | string | `"overlay"` (iframe) oder `"redirect"` |
| `voice_agent_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
## Priority-Playlist (pro Standort)
Liegt in `config.json` unter `sites.<site>.priority`, die Medien unter `media/<site>/priority/`:
```jsonc
"sites": {
"stuttgart": {
"priority": {
"enabled": true, // schaltet die Priority-Playlist dieses Standorts
"playlist": [ ... ] // gleiches Eintragsformat wie eine Screen-Playlist
},
"screens": { ... }
}
}
```
Die Einträge laufen auf allen Screens **dieses** Standorts, eingestreut zwischen den normalen Playlist-Einträgen. Ein anderer Standort ist davon nicht betroffen.
> Früher lag die Priority-Playlist global unter `config.priority` und wirkte auf *alle* Standorte. `get_priority_config()` übernimmt einen solchen Altbestand beim ersten Zugriff einmalig in den jeweiligen Standort.
Die Ordnernamen `priority` und `logos` sind unter `media/<site>/` **reserviert** und werden bei der Screen-Erkennung übersprungen – sonst würden sie als Screens in der Oberfläche auftauchen.
---
<a id="playlist-eintraege"></a>
## Playlist-Einträge
@@ -507,7 +551,9 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
- `_window_start_date(now, start, end)` – Kalendertag, an dem das laufende Zeitfenster begann (`None` = ausserhalb)
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
- `load_priority_files()` – Priority-Playlist + Dateien laden
- `load_priority_files(site)` – Priority-Playlist eines Standorts + Dateien laden
- `get_priority_config(cfg, site)` – Priority-Konfiguration des Standorts (übernimmt einmalig einen globalen Altbestand)
- `priority_dir(site)` – Pfad zu `media/<site>/priority/`
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
- `save_uploaded_logo(file, site, slot, freistellen=True)` – hochgeladenes Logo freistellen und als PNG ablegen
- `remove_logo_background(image)` – Hintergrund per Flood Fill entfernen, gibt `(bild, hinweis)` zurück

75
app.py
View File

@@ -10,6 +10,7 @@ zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird.
import os
import json
import copy
import hashlib
import re
import time
@@ -380,16 +381,38 @@ def is_item_active(item):
# -------------------------------------------------
# Priority-Playlist-Helfer
# -------------------------------------------------
def load_priority_files():
"""Lädt die Priority-Playlist aus config.json plus die dazugehörigen Dateien
aus media/priority/. Gibt eine Liste von Dicts mit name/type/size/enabled zurück.
PRIORITY_DIRNAME = "priority"
def priority_dir(site):
"""Verzeichnis der Priority-Medien eines Standorts: media/<site>/priority/."""
return os.path.join(MEDIA_DIR, site, PRIORITY_DIRNAME)
def get_priority_config(cfg, site):
"""Priority-Konfiguration eines Standorts (erzeugt fehlende Ebenen).
Die Priority-Playlist gilt ausschliesslich fuer den eigenen Standort.
Aeltere Konfigurationen hatten sie global unter cfg["priority"]; ist beim
Standort noch nichts hinterlegt, wird der Altbestand einmalig uebernommen."""
site_cfg = cfg.setdefault("sites", {}).setdefault(site, {})
if "priority" not in site_cfg:
alt = cfg.get("priority")
site_cfg["priority"] = copy.deepcopy(alt) if isinstance(alt, dict) else {}
return site_cfg["priority"]
def load_priority_files(site):
"""Lädt die Priority-Playlist eines Standorts aus config.json plus die
dazugehörigen Dateien aus media/<site>/priority/.
Gibt eine Liste von Dicts mit name/type/size/enabled zurück.
Neue Dateien im Ordner, die noch nicht in der Playlist sind, werden angehängt."""
cfg = load_config()
prio_dir = os.path.join(MEDIA_DIR, "priority")
prio_dir = priority_dir(site)
os.makedirs(prio_dir, exist_ok=True)
priority_files = []
prio_playlist = cfg.get("priority", {}).get("playlist", [])
prio_playlist = get_priority_config(cfg, site).get("playlist", [])
# 1. Einträge aus der config.json-Playlist
for item in prio_playlist:
@@ -1143,11 +1166,10 @@ def api_add_customer():
# -------------------------------------------------
# Medien ausliefern (statische Dateien)
# -------------------------------------------------
@app.route("/media/priority/<path:filename>")
def media_priority(filename):
"""Liefert globale Priority-Medien aus media/priority/ aus."""
path = os.path.join(MEDIA_DIR, "priority")
return send_from_directory(path, filename)
@app.route("/media/<site>/priority/<path:filename>")
def media_priority(site, filename):
"""Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus."""
return send_from_directory(priority_dir(site), filename)
@app.route("/media/<site>/<screen>/<path:filename>")
@@ -1229,7 +1251,7 @@ def player(site, screen):
normal_files.append({"kind": "file", "name": f})
# ─── Priority-Playlist (global) ───
priority_cfg = config.get("priority", {})
priority_cfg = get_priority_config(config, site)
if priority_cfg.get("enabled", False):
prio_files = []
@@ -1243,7 +1265,7 @@ def player(site, screen):
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": is_item_active(item), "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False, "schedule": item.get("schedule") if isinstance(item, dict) else None})
continue
file_path = os.path.join(MEDIA_DIR, "priority", item_name)
file_path = os.path.join(priority_dir(site), item_name)
if os.path.exists(file_path):
prio_files.append({"kind": "file", "name": item_name, "enabled": True})
else:
@@ -1296,7 +1318,7 @@ def playlist_hash(site, screen):
screen_playlist = (
config.get("sites", {}).get(site, {}).get("screens", {}).get(screen, {}).get("playlist", [])
)
prio_cfg = config.get("priority", {})
prio_cfg = get_priority_config(config, site)
prio_playlist = prio_cfg.get("playlist", []) if prio_cfg.get("enabled", False) else []
relevant = {
@@ -1409,15 +1431,17 @@ def admin(site):
config_screens = site_cfg.setdefault("screens", {})
# Ordner im Medienverzeichnis = Screens.
# "logos" ist reserviert: aeltere Versionen legten dort hochgeladene
# Kundenlogos ab, und ein solcher Ordner wuerde sonst dauerhaft als Screen
# in der Konfiguration landen.
# Reservierte Namen: "logos" (hochgeladene Kundenlogos) und "priority"
# (Priority-Medien des Standorts). Solche Ordner wuerden sonst dauerhaft als
# Screen in der Konfiguration landen; "priority" ist ausserdem in den Routen
# bereits als Kennzeichen fuer die Priority-Playlist belegt.
reserviert = {generate_welcome_page.LOGO_URL_PREFIX, PRIORITY_DIRNAME}
if os.path.isdir(site_media_base):
for entry in os.listdir(site_media_base):
entry_path = os.path.join(site_media_base, entry)
if (os.path.isdir(entry_path)
and not entry.startswith("._")
and entry != generate_welcome_page.LOGO_URL_PREFIX):
and entry not in reserviert):
known_screens.add(entry)
# Screens aus Config ergänzen (auch ohne Ordner)
@@ -1523,7 +1547,7 @@ def admin_priority(site):
"""Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien)."""
cfg = load_config()
site_list = get_accessible_sites(cfg, current_user)
priority_files = load_priority_files()
priority_files = load_priority_files(site)
return render_template(
"priority.html",
@@ -1719,7 +1743,8 @@ def upload(site, screen):
filename = secure_filename(file.filename)
if screen == "priority":
file.save(os.path.join(MEDIA_DIR, "priority", filename))
os.makedirs(priority_dir(site), exist_ok=True)
file.save(os.path.join(priority_dir(site), filename))
else:
file.save(os.path.join(MEDIA_DIR, site, screen, filename))
@@ -1790,7 +1815,7 @@ def add_url(site, screen):
config = load_config()
if screen == "priority":
config.setdefault("priority", {}).setdefault("playlist", []).append(url_data)
get_priority_config(config, site).setdefault("playlist", []).append(url_data)
else:
cfg = get_screen_config(config, site, screen)
cfg.setdefault("playlist", []).append(url_data)
@@ -1809,8 +1834,8 @@ def delete_file(site, screen, filename):
cfg = load_config()
if screen == "priority":
path = os.path.join(MEDIA_DIR, "priority", filename)
playlist = cfg.get("priority", {}).get("playlist", [])
path = os.path.join(priority_dir(site), filename)
playlist = get_priority_config(cfg, site).get("playlist", [])
playlist[:] = [item for item in playlist if not (
(isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("url") == filename)
@@ -1847,7 +1872,7 @@ def save_playlist(site, screen):
enabled_map = data.get("enabled", {})
if screen == "priority":
old_playlist = cfg.get("priority", {}).get("playlist", [])
old_playlist = get_priority_config(cfg, site).get("playlist", [])
else:
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
old_playlist = screens_cfg.get(screen, {}).get("playlist", [])
@@ -1868,7 +1893,7 @@ def save_playlist(site, screen):
new_playlist.append({"url": key, "zoom": 1.0, "enabled": enabled_map.get(key, True)} if is_url(key) else {"name": key, "enabled": enabled_map.get(key, True)})
if screen == "priority":
cfg.setdefault("priority", {})["playlist"] = new_playlist
get_priority_config(cfg, site)["playlist"] = new_playlist
else:
cfg.setdefault("sites", {}).setdefault(site, {}).setdefault("screens", {}).setdefault(screen, {})["playlist"] = new_playlist
@@ -1907,7 +1932,7 @@ def update_playlist_item(site, screen):
config = load_config()
if screen == "priority":
playlist = config.setdefault("priority", {}).setdefault("playlist", [])
playlist = get_priority_config(config, site).setdefault("playlist", [])
else:
playlist = config["sites"][site]["screens"][screen].setdefault("playlist", [])

View File

@@ -170,12 +170,13 @@
<div class="card mb-3">
<div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
<div class="card-body">
<p>Die Priority-Playlist ist <strong>global</strong> und wird auf <em>allen</em> Playern eines Standorts eingeblendet – zwischen den normalen Playlist-Einträgen.</p>
<p>Die Priority-Playlist gilt <strong>pro Standort</strong>. Ihre Inhalte werden auf <em>allen</em> Screens dieses Standorts eingeblendet – zwischen den normalen Playlist-Einträgen.</p>
<ul>
<li>Aufruf über <code>/admin/&lt;standort&gt;/priority</code></li>
<li>Globale Medien und URLs verwalten, ebenfalls per Drag & Drop</li>
<li>Priority muss in der Konfiguration aktiviert sein (<code>config.json → priority.enabled</code>)</li>
<li>Medien und URLs dieses Standorts verwalten, ebenfalls per Drag & Drop</li>
<li>Priority muss für den Standort aktiviert sein (<code>config.json → sites.&lt;standort&gt;.priority.enabled</code>)</li>
<li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li>
<li>Jeder Standort hat seine <strong>eigene</strong> Priority-Playlist – Inhalte eines Standorts erscheinen nie auf den Screens eines anderen.</li>
</ul>
</div>
</div>

View File

@@ -472,7 +472,7 @@ function playNext() {
}
// Datei aus dem Medienverzeichnis
const src = entry.isPrio ? `/media/priority/${item.name}` : `/media/${site}/${basePath}/${item.name}`;
const src = `/media/${site}/${basePath}/${item.name}`;
if (isVideo(item)) {
vid.style.display = "block";
vid.src = src;

View File

@@ -147,7 +147,7 @@
{% if file.type == "image" %}
data-bs-toggle="tooltip"
data-bs-html="true"
title="<img src='/media/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
title="<img src='/media/{{ current_site }}/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
{% elif file.type == "url" %}
data-bs-toggle="tooltip"
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"