feat: Willkommensseite auf mehreren Screens anzeigen

Die Willkommensseite ging bisher fest in die Lobby-Playlist. Welche
Screens Begruessungsschirme sind, ist aber eine Eigenschaft der
Installation und nicht des einzelnen Kundenbesuchs - deshalb ein Flag
pro Screen statt einer Auswahl bei jedem Vorgang.

- Neue Screen-Einstellung "welcome_enabled" (Checkbox im Tab
  Einstellungen). Die generierte welcome.html wird in jeden markierten
  Screen des Standorts geschrieben und dort an Position 0 gesetzt.
- welcome_target_screens() faellt nur auf "lobby" zurueck, wenn kein
  Screen des Standorts den Schluessel besitzt. Bestehende Standorte
  verhalten sich damit ohne Konfigurationsaufwand unveraendert.
- add_customer_to_playlists() (frueher add_customer_to_lobby_playlist)
  raeumt welcome.html aus allen Screens des Standorts und fuegt sie nur
  in den Zielscreens wieder ein. Ein abgewaehlter Screen bleibt so nicht
  mit einer alten Begruessung stehen.
- Eingefuegt wird ein Dict statt eines Strings, damit der Eintrag
  enabled und schedule tragen kann.
- Optionales "Anzeigen bis" im Formular: setzt eine Zeitsteuerung, die
  Seite verschwindet danach auf allen Screens von selbst.
- Das Formular zeigt die aktuellen Zielscreens an und meldet
  verstaendlich, wenn keiner markiert ist.
- screens.setdefault("lobby", {}) entfernt - das legte an Standorten
  ohne Lobby stillschweigend einen Screen an. Es werden nur noch
  vorhandene Screens bespielt.

Die Seite referenziert alle Bilder absolut (/static/..., /media/...),
ist also ortsunabhaengig; je Screen liegt eine identische Kopie.

README, AGENTS.md und die Hilfeseite nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-09-02 12:26:22 +02:00
parent 3761497421
commit 848bcf93ab
7 changed files with 177 additions and 49 deletions

View File

@@ -110,7 +110,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- 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. - 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. - `.html` items in `media/` are rendered inline as content, not in an iframe.
- `config.priority.enabled` makes the priority playlist show on every screen. - `config.priority.enabled` makes the priority playlist show on every screen.
- `POST /api/customer` generates `welcome.html` and inserts it at the front of the lobby playlist for the specified 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>]`. - 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. - Priority playlist (`config.priority`) is global and affects all sites/screens.
- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column). - Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column).
@@ -119,7 +119,9 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- `customer_names` are preserved in form fields after POST (via `value`-Attribute). - `customer_names` are preserved in form fields after POST (via `value`-Attribute).
- Admin-UI nutzt keyadmin-Design: `brand-surface` (#2b2f36), `nav-surface` (rot #DA002D), Dark Mode per `localStorage("signage-theme")`. - Admin-UI nutzt keyadmin-Design: `brand-surface` (#2b2f36), `nav-surface` (rot #DA002D), Dark Mode per `localStorage("signage-theme")`.
- Gemeinsame HTML-Bausteine: `_header.html`, `_footer.html`, `_styles.html` (CSS-Variablen `--ccm-*`, Dark Mode, Card-Border-Radius 1rem). - Gemeinsame HTML-Bausteine: `_header.html`, `_footer.html`, `_styles.html` (CSS-Variablen `--ccm-*`, Dark Mode, Card-Border-Radius 1rem).
- `add_customer_to_lobby_playlist` entfernt `welcome.html` sowohl als String als auch als Dict aus der Playlist vor dem Einfügen. - `add_customer_to_playlists` (früher `add_customer_to_lobby_playlist`) entfernt `welcome.html` als String *und* als Dict aus **allen** Screens des Standorts und fügt sie danach nur in den Zielscreens an Position 0 wieder ein – ein abgewählter Screen bleibt so nicht mit einer alten Begrüßung stehen. Eingefügt wird ein Dict, kein String, damit `enabled` und `schedule` mitgeführt werden können.
- `welcome_target_screens(config, site)` fällt nur dann auf `lobby` zurück, wenn **kein** Screen des Standorts den Schlüssel `welcome_enabled` besitzt. Wurde er überall bewusst auf `False` gesetzt, ist die leere Liste das gewollte Ergebnis.
- Weder `save_welcome_page` noch `add_customer_to_playlists` legen Screens an. Das frühere `screens.setdefault("lobby", {})` erzeugte an Standorten ohne Lobby stillschweigend einen Screen.
- `add_screen` legt ein Verzeichnis unter `media/<site>/<screen>/` an und einen Config-Eintrag. - `add_screen` legt ein Verzeichnis unter `media/<site>/<screen>/` an und einen Config-Eintrag.
- `delete_screen` entfernt den Screen aus der Config und löscht das Verzeichnis rekursiv. - `delete_screen` entfernt den Screen aus der Config und löscht das Verzeichnis rekursiv.
- Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_enabled` + `custom_url_target` + `custom_url_position`) im Admin-Formular (Aktionen-Tab); wird im Player als Button an wählbarer Position (9 Positionen: oben/mitte/unten × links/mitte/rechts) angezeigt und öffnet die URL wahlweise in einem iframe-Overlay mit Zurück-Button (`overlay`) oder per Direkt-Weiterleitung (`redirect`); Player pausiert während das Overlay geöffnet ist. Die unteren Positionen weichen automatisch 16px über dem Newsticker-Balken aus. - Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_enabled` + `custom_url_target` + `custom_url_position`) im Admin-Formular (Aktionen-Tab); wird im Player als Button an wählbarer Position (9 Positionen: oben/mitte/unten × links/mitte/rechts) angezeigt und öffnet die URL wahlweise in einem iframe-Overlay mit Zurück-Button (`overlay`) oder per Direkt-Weiterleitung (`redirect`); Player pausiert während das Overlay geöffnet ist. Die unteren Positionen weichen automatisch 16px über dem Newsticker-Balken aus.

View File

@@ -10,7 +10,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge - Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
- **Multi-Standort**: Standorte (sites) gruppieren Screens - **Multi-Standort**: Standorte (sites) gruppieren Screens
- **Priority-Playlist**: globale Inhalte wirken auf alle Player - **Priority-Playlist**: globale Inhalte wirken auf alle Player
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch, pro Standort konfigurierbares Hintergrundbild - **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch, 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 - Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
- Auto-Reload bei Playlist-Änderungen - Auto-Reload bei Playlist-Änderungen
- Newsticker pro Screen - Newsticker pro Screen
@@ -418,6 +418,7 @@ OPENAI_API_KEY=sk-...
| `custom_url_target` | string | `"overlay"` (iframe) oder `"redirect"` | | `custom_url_target` | string | `"overlay"` (iframe) oder `"redirect"` |
| `custom_url_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` | | `custom_url_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
| `stay_on_first` | bool | Player bleibt auf erstem Element stehen (kein Refresh) | | `stay_on_first` | bool | Player bleibt auf erstem Element stehen (kein Refresh) |
| `welcome_enabled` | bool | Willkommensseite wird auf diesem Screen vorne in die Playlist gesetzt |
| `voice_agent_enabled` | bool | Voice-Agent-Button im Player anzeigen | | `voice_agent_enabled` | bool | Voice-Agent-Button im Player anzeigen |
| `voice_agent_show_image` | bool | Bild (dva.png) über dem Button anzeigen | | `voice_agent_show_image` | bool | Bild (dva.png) über dem Button anzeigen |
| `voice_agent_label` | string | Button-Beschriftung (Default: "Digitaler Assistent") | | `voice_agent_label` | string | Button-Beschriftung (Default: "Digitaler Assistent") |
@@ -496,6 +497,7 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen - `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips - `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
- `load_priority_files()` – Priority-Playlist + Dateien laden - `load_priority_files()` – Priority-Playlist + Dateien laden
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
- `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite - `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite
- `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None` - `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None`
- `load_users()` / `save_users()` / `get_user()` – User aus `users.json` - `load_users()` / `save_users()` / `get_user()` – User aus `users.json`

45
app.py
View File

@@ -892,13 +892,15 @@ def add_customer():
Formular für die Willkommensseite: Formular für die Willkommensseite:
- Bis zu 3 Kundennamen eingeben - Bis zu 3 Kundennamen eingeben
- Logos per OpenAI → Brandfetch suchen - Logos per OpenAI → Brandfetch suchen
- welcome.html wird generiert und in der Lobby-Playlist vorne platziert - welcome.html wird generiert und in den Playlists aller Zielscreens
vorne platziert (Screens mit "welcome_enabled")
""" """
error = None error = None
success = None success = None
logo_urls = [None, None, None] logo_urls = [None, None, None]
customer_names = ["", "", ""] customer_names = ["", "", ""]
contacts = ["", "", ""] contacts = ["", "", ""]
valid_until = (request.form.get("valid_until") or "").strip()
site = request.args.get("site") or request.form.get("site") or "" site = request.args.get("site") or request.form.get("site") or ""
# Fallback auf ersten für den User zugänglichen Standort # Fallback auf ersten für den User zugänglichen Standort
@@ -959,20 +961,34 @@ def add_customer():
break break
bg_url = get_background_url(site) bg_url = get_background_url(site)
html_filename = generate_welcome_page.save_welcome_page(valid_names, valid_urls, contacts=valid_contacts, site=site, background_url=bg_url) targets = generate_welcome_page.welcome_target_screens(load_config(), site)
if not targets:
error = ("Kein Screen für die Willkommensseite ausgewählt – "
"im Tab 'Einstellungen' eines Screens aktivieren")
html_filename = None
else:
html_filename = generate_welcome_page.save_welcome_page(
valid_names, valid_urls, contacts=valid_contacts,
site=site, background_url=bg_url, screens=targets)
if html_filename: if html_filename:
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site): if generate_welcome_page.add_customer_to_playlists(
html_filename, site=site, screens=targets, valid_until=valid_until):
# Letzte Kundendaten für Background-Update speichern # Letzte Kundendaten für Background-Update speichern
config = load_config() config = load_config()
site_cfg = config.setdefault("sites", {}).setdefault(site, {}) site_cfg = config.setdefault("sites", {}).setdefault(site, {})
site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls, "contacts": valid_contacts} site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls, "contacts": valid_contacts}
save_config(config) save_config(config)
add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt", current_user.email) add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt (Screens: {', '.join(targets)})", current_user.email)
success = f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt!" success = (f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt "
f"– angezeigt auf: {', '.join(targets)}")
if valid_until:
success += f" (bis einschliesslich {valid_until})"
else: else:
error = "Fehler beim Hinzufügen zur Playliste" error = "Fehler beim Hinzufügen zur Playliste"
else: elif targets:
# Ohne Zielscreens steht die Meldung dazu bereits in error
error = "Fehler beim Speichern der Willkommensseite" error = "Fehler beim Speichern der Willkommensseite"
except ValueError as e: except ValueError as e:
error = f"Konfigurationsfehler: {str(e)}" error = f"Konfigurationsfehler: {str(e)}"
@@ -1002,6 +1018,7 @@ def add_customer():
logo_urls=logo_urls, logo_urls=logo_urls,
customer_names=customer_names, customer_names=customer_names,
contacts=contacts, contacts=contacts,
welcome_screens=generate_welcome_page.welcome_target_screens(config, site) if site else [],
site=site, site=site,
site_list=site_list, site_list=site_list,
current_site=site, current_site=site,
@@ -1047,18 +1064,27 @@ def api_add_customer():
contact = data.get("contact", "").strip() contact = data.get("contact", "").strip()
html_filename = generate_welcome_page.save_welcome_page([customer_name], [logo_url], contacts=[contact], site=site) valid_until = (data.get("valid_until") or "").strip()
targets = generate_welcome_page.welcome_target_screens(load_config(), site)
if not targets:
return jsonify({"error": "No welcome screen configured for this site"}), 400
html_filename = generate_welcome_page.save_welcome_page(
[customer_name], [logo_url], contacts=[contact], site=site, screens=targets)
if not html_filename: if not html_filename:
return jsonify({"error": "Failed to create welcome page"}), 500 return jsonify({"error": "Failed to create welcome page"}), 500
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site): if generate_welcome_page.add_customer_to_playlists(
html_filename, site=site, screens=targets, valid_until=valid_until):
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Customer '{customer_name}' added successfully", "message": f"Customer '{customer_name}' added successfully",
"welcome_page": html_filename, "welcome_page": html_filename,
"logo_url": logo_url, "logo_url": logo_url,
"site": site "site": site,
"screens": targets
}), 201 }), 201
else: else:
return jsonify({"error": "Failed to add to playlist"}), 500 return jsonify({"error": "Failed to add to playlist"}), 500
@@ -1527,6 +1553,7 @@ def update_screen(site, screen):
cfg["newsticker_text"] = request.form.get("newsticker_text", "") cfg["newsticker_text"] = request.form.get("newsticker_text", "")
cfg["newsticker_enabled"] = "newsticker_enabled" in request.form cfg["newsticker_enabled"] = "newsticker_enabled" in request.form
cfg["stay_on_first"] = "stay_on_first" in request.form cfg["stay_on_first"] = "stay_on_first" in request.form
cfg["welcome_enabled"] = "welcome_enabled" in request.form
save_config(config) save_config(config)
return redirect(f"/admin/{site}") return redirect(f"/admin/{site}")

View File

@@ -296,57 +296,134 @@ def generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttg
# Speichern + Playlist-Einfügung # Speichern + Playlist-Einfügung
# ------------------------------------------------- # -------------------------------------------------
def save_welcome_page(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None): def welcome_target_screens(config, site):
"""Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis. """Ermittelt die Screens eines Standorts, auf denen die Willkommensseite laufen soll.
Ein Screen ist Ziel, wenn "welcome_enabled" gesetzt ist. Ist der Schluessel bei
keinem Screen des Standorts vorhanden, greift das alte Verhalten: nur "lobby".
Damit bleiben bestehende Standorte ohne Konfigurationsaufwand unveraendert."""
screens = config.get("sites", {}).get(site, {}).get("screens", {})
configured = [name for name, cfg in screens.items()
if isinstance(cfg, dict) and cfg.get("welcome_enabled")]
if configured:
return sorted(configured)
# Kein Screen markiert: nur dann auf "lobby" zurückfallen, wenn noch nirgends
# eine bewusste Entscheidung getroffen wurde. Wurde das Häkchen überall
# entfernt, ist "keine Screens" das gewollte Ergebnis.
entschieden = any(isinstance(cfg, dict) and "welcome_enabled" in cfg
for cfg in screens.values())
if entschieden:
return []
return ["lobby"] if "lobby" in screens else []
def save_welcome_page(customer_names, logo_urls, contacts=None, site="stuttgart",
background_url=None, screens=None):
"""Speichert die generierte Willkommensseite als welcome.html in den Zielscreens.
screens: Liste der Screen-Namen; None → Zielscreens aus der Konfiguration.
contacts: Liste der Ansprechpartner-Texte (Markdown, optional) contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard). background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard).
Gibt den Dateinamen zurück oder None bei Fehler.""" Gibt den Dateinamen zurück oder None bei Fehler.
Die Seite referenziert alle Bilder absolut (/static/..., /media/<site>/...),
ist also ortsunabhängig - eine Kopie je Screen rendert identisch."""
try: try:
welcome_dir = os.path.join(MEDIA_DIR, site, "lobby") from app import load_config
if screens is None:
screens = welcome_target_screens(load_config(), site)
if not screens:
print(f"⚠️ Generate - Kein Zielscreen für die Willkommensseite (site: {site})")
return None
html_content = generate_welcome_html(customer_names, logo_urls, contacts=contacts,
site=site, background_url=background_url)
for screen in screens:
welcome_dir = os.path.join(MEDIA_DIR, site, screen)
os.makedirs(welcome_dir, exist_ok=True) os.makedirs(welcome_dir, exist_ok=True)
filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME) filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME)
html_content = generate_welcome_html(customer_names, logo_urls, contacts=contacts, site=site, background_url=background_url)
with open(filepath, "w", encoding="utf-8") as f: with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content) f.write(html_content)
if os.path.getsize(filepath) == 0: if os.path.getsize(filepath) == 0:
raise IOError("File not written") raise IOError("File not written")
names_str = ", ".join(customer_names)
print(f"✅ Generate - Willkommensseite generiert für: {names_str} (site: {site})")
print(f"✅ Generate - Willkommensseite gespeichert in: {filepath}") print(f"✅ Generate - Willkommensseite gespeichert in: {filepath}")
names_str = ", ".join(customer_names)
print(f"✅ Generate - Willkommensseite generiert für: {names_str} "
f"(site: {site}, screens: {', '.join(screens)})")
return WELCOME_FILENAME return WELCOME_FILENAME
except Exception as e: except Exception as e:
print(f"❌ Error saving welcome page: {e}") print(f"❌ Error saving welcome page: {e}")
return None return None
def add_customer_to_lobby_playlist(html_filename, site="stuttgart"): def _strip_welcome(playlist, filename):
"""Fügt die Willkommensseite (welcome.html) an Position 0 in die Lobby-Playlist ein. """Entfernt alle welcome.html-Einträge aus einer Playlist (String- und Dict-Form)."""
Entfernt vorher alle vorhandenen welcome.html-Einträge (als String oder Dict). playlist[:] = [
item for item in playlist
if not (
(isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("name") == filename)
)
]
def add_customer_to_playlists(html_filename, site="stuttgart", screens=None, valid_until=None):
"""Setzt die Willkommensseite an Position 0 der Playlist jedes Zielscreens.
screens: Liste der Zielscreens; None → aus der Konfiguration ermitteln.
valid_until: optionales Datum "YYYY-MM-DD". Ist es gesetzt, bekommt der
Eintrag eine Zeitsteuerung und verschwindet danach von selbst.
Aus allen übrigen Screens des Standorts wird die Seite entfernt, damit ein
abgewählter Screen nicht mit einer alten Begrüßung stehen bleibt.
Importiert load_config/save_config aus app.py (kein Zirkelimport).""" Importiert load_config/save_config aus app.py (kein Zirkelimport)."""
try: try:
from app import load_config, save_config from app import load_config, save_config
config = load_config() config = load_config()
sites_cfg = config.setdefault("sites", {}) site_cfg = config.setdefault("sites", {}).setdefault(site, {"screens": {}})
site_cfg = sites_cfg.setdefault(site, {"screens": {}}) all_screens = site_cfg.setdefault("screens", {})
screens = site_cfg.setdefault("screens", {})
lobby = screens.setdefault("lobby", {}) if screens is None:
playlist = lobby.setdefault("playlist", []) screens = welcome_target_screens(config, site)
# Nur Screens, die es wirklich gibt - kein stillschweigendes Anlegen
targets = [name for name in screens if name in all_screens]
entry = {"name": html_filename, "enabled": True}
if valid_until:
# Ganztaegig bis einschliesslich valid_until
entry["schedule"] = {
"mode": "daily",
"start": "00:00",
"end": "23:59",
"valid_until": valid_until
}
for name, screen_cfg in all_screens.items():
if not isinstance(screen_cfg, dict):
continue
playlist = screen_cfg.setdefault("playlist", [])
_strip_welcome(playlist, html_filename)
if name in targets:
playlist.insert(0, dict(entry))
full_filename = f"{html_filename}"
# Alte welcome.html-Einträge entfernen (sowohl String- als auch Dict-Form)
playlist[:] = [
item for item in playlist
if not (
(isinstance(item, str) and item == full_filename) or
(isinstance(item, dict) and item.get("name") == full_filename)
)
]
playlist.insert(0, full_filename)
save_config(config) save_config(config)
print(f"✅ Generate - Willkommensseite in Lobby-Playliste platziert (site: {site})")
return True if targets:
print(f"✅ Generate - Willkommensseite platziert (site: {site}, "
f"screens: {', '.join(targets)})")
else:
print(f"⚠️ Generate - Kein Zielscreen vorhanden (site: {site})")
return bool(targets)
except Exception as e: except Exception as e:
print(f"❌ Generate - Error adding to playlist: {e}") print(f"❌ Generate - Error adding to playlist: {e}")
return False return False

View File

@@ -344,13 +344,21 @@
<label class="form-check-label">Videos anzeigen</label> <label class="form-check-label">Videos anzeigen</label>
</div> </div>
<div class="form-check form-switch mb-4"> <div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
name="stay_on_first" name="stay_on_first"
{% if cfg.stay_on_first %}checked{% endif %}> {% if cfg.stay_on_first %}checked{% endif %}>
<label class="form-check-label">Auf erstem Element stehen bleiben (kein Refresh)</label> <label class="form-check-label">Auf erstem Element stehen bleiben (kein Refresh)</label>
</div> </div>
<div class="form-check form-switch mb-1">
<input class="form-check-input" type="checkbox"
name="welcome_enabled"
{% if cfg.welcome_enabled %}checked{% endif %}>
<label class="form-check-label">Willkommensseite hier anzeigen</label>
</div>
<div class="form-hint mb-4">Erzeugte Willkommensseiten werden auf allen so markierten Screens dieses Standorts vorne in die Playlist gesetzt.</div>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
Einstellungen speichern Einstellungen speichern
</button> </button>

View File

@@ -153,9 +153,19 @@
</div> </div>
</div> </div>
<hr class="mt-4 mb-3">
<div class="mb-3">
<label class="form-label" for="valid_until">Anzeigen bis <span class="text-muted">(optional)</span></label>
<input type="date" class="form-control" id="valid_until" name="valid_until"
value="{{ request.form.get('valid_until', '') }}" style="max-width: 260px;">
<div class="form-hint">Ohne Angabe bleibt die Willkommensseite stehen, bis sie von Hand entfernt wird. Mit Datum verschwindet sie nach diesem Tag auf allen Screens von selbst.</div>
</div>
<div class="mb-3 text-muted"> <div class="mb-3 text-muted">
<p class="mb-1">Das System sucht automatisch nach den Kundenlogos</p> <p class="mb-1">Das System sucht automatisch nach den Kundenlogos</p>
<p class="mb-0">und erstellt eine angepasste Willkommensseite für die Lobby-Playlist.</p> <p class="mb-0">und erstellt eine angepasste Willkommensseite.</p>
<p class="mb-1">Sie erscheint auf allen Screens dieses Standorts, bei denen im Tab „Einstellungen" die Option <em>Willkommensseite hier anzeigen</em> aktiv ist{% if welcome_screens %} – aktuell: <strong>{{ welcome_screens | join(', ') }}</strong>{% else %} – <strong>aktuell keiner</strong>{% endif %}.</p>
<p class="mb-0 small"><i class="ti ti-markdown me-1"></i>Ansprechpartner unterstützen <strong>fett</strong>, <em>kursiv</em>, <code>Code</code>, Aufzählungen (<code>- </code>) sowie Zeilenumbrüche.</p> <p class="mb-0 small"><i class="ti ti-markdown me-1"></i>Ansprechpartner unterstützen <strong>fett</strong>, <em>kursiv</em>, <code>Code</code>, Aufzählungen (<code>- </code>) sowie Zeilenumbrüche.</p>
</div> </div>

View File

@@ -249,7 +249,9 @@
<p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p> <p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p>
<ul> <ul>
<li>Aufruf über <code>/willkommen?site=&lt;standort&gt;</code> (oder Menüpunkt)</li> <li>Aufruf über <code>/willkommen?site=&lt;standort&gt;</code> (oder Menüpunkt)</li>
<li>Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Lobby-Playlist gesetzt</li> <li><strong>Zielscreens:</strong> Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Playlist <em>jedes</em> Screens gesetzt, bei dem im Tab "Einstellungen" die Option <em>Willkommensseite hier anzeigen</em> aktiv ist. So lässt sich dieselbe Begrüßung auf mehreren Screens zeigen, ohne sie mehrfach anzulegen. Ist an einem Standort nichts eingestellt, geht sie wie bisher in die Lobby.</li>
<li>Wird ein Screen später abgewählt, verschwindet die Willkommensseite dort beim nächsten Generieren automatisch.</li>
<li><strong>Anzeigen bis:</strong> Optionales Datum im Formular. Ist es gesetzt, verschwindet die Willkommensseite nach diesem Tag auf allen Screens von selbst – kein manuelles Aufräumen nach dem Besuch. Ohne Angabe bleibt sie stehen.</li>
<li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li> <li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li>
<li>Pro Kunde können <strong>Ansprechpartner</strong> eingegeben werden (Markdown-Format): <li>Pro Kunde können <strong>Ansprechpartner</strong> eingegeben werden (Markdown-Format):
<ul> <ul>