diff --git a/AGENTS.md b/AGENTS.md index 5ea5f8f..759f280 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - `.html` items in `media/` are rendered inline as content, not in an iframe. - `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///` directories and optionally adding config to `config.json["sites"][]`. - 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). @@ -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). - 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). -- `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///` an und einen Config-Eintrag. - `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. diff --git a/README.md b/README.md index fd333d5..ff2f520 100755 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens. - 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 -- **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 - Auto-Reload bei Playlist-Änderungen - Newsticker pro Screen @@ -418,6 +418,7 @@ OPENAI_API_KEY=sk-... | `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` | | `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_show_image` | bool | Bild (dva.png) über dem Button anzeigen | | `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 - `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips - `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 - `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None` - `load_users()` / `save_users()` / `get_user()` – User aus `users.json` diff --git a/app.py b/app.py index 4ba4eb1..f32e8a5 100755 --- a/app.py +++ b/app.py @@ -892,13 +892,15 @@ def add_customer(): Formular für die Willkommensseite: - Bis zu 3 Kundennamen eingeben - 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 success = None logo_urls = [None, None, None] customer_names = ["", "", ""] contacts = ["", "", ""] + valid_until = (request.form.get("valid_until") or "").strip() site = request.args.get("site") or request.form.get("site") or "" # Fallback auf ersten für den User zugänglichen Standort @@ -959,20 +961,34 @@ def add_customer(): break 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 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 config = load_config() site_cfg = config.setdefault("sites", {}).setdefault(site, {}) site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls, "contacts": valid_contacts} save_config(config) - add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt", current_user.email) - success = f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt!" + 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 " + f"– angezeigt auf: {', '.join(targets)}") + if valid_until: + success += f" (bis einschliesslich {valid_until})" else: 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" except ValueError as e: error = f"Konfigurationsfehler: {str(e)}" @@ -1002,6 +1018,7 @@ def add_customer(): logo_urls=logo_urls, customer_names=customer_names, contacts=contacts, + welcome_screens=generate_welcome_page.welcome_target_screens(config, site) if site else [], site=site, site_list=site_list, current_site=site, @@ -1047,18 +1064,27 @@ def api_add_customer(): 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: 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({ "success": True, "message": f"Customer '{customer_name}' added successfully", "welcome_page": html_filename, "logo_url": logo_url, - "site": site + "site": site, + "screens": targets }), 201 else: 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_enabled"] = "newsticker_enabled" in request.form cfg["stay_on_first"] = "stay_on_first" in request.form + cfg["welcome_enabled"] = "welcome_enabled" in request.form save_config(config) return redirect(f"/admin/{site}") diff --git a/generate_welcome_page.py b/generate_welcome_page.py index 851a32c..5a3a34b 100644 --- a/generate_welcome_page.py +++ b/generate_welcome_page.py @@ -296,57 +296,134 @@ def generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttg # Speichern + Playlist-Einfügung # ------------------------------------------------- -def save_welcome_page(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None): - """Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis. +def welcome_target_screens(config, site): + """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) background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard). - Gibt den Dateinamen zurück oder None bei Fehler.""" - try: - welcome_dir = os.path.join(MEDIA_DIR, site, "lobby") - os.makedirs(welcome_dir, exist_ok=True) - 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) + Gibt den Dateinamen zurück oder None bei Fehler. + + Die Seite referenziert alle Bilder absolut (/static/..., /media//...), + ist also ortsunabhängig - eine Kopie je Screen rendert identisch.""" + try: + 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) + filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(html_content) + if os.path.getsize(filepath) == 0: + raise IOError("File not written") + print(f"✅ Generate - Willkommensseite gespeichert in: {filepath}") - with open(filepath, "w", encoding="utf-8") as f: - f.write(html_content) - if os.path.getsize(filepath) == 0: - 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 generiert für: {names_str} " + f"(site: {site}, screens: {', '.join(screens)})") return WELCOME_FILENAME except Exception as e: print(f"❌ Error saving welcome page: {e}") return None -def add_customer_to_lobby_playlist(html_filename, site="stuttgart"): - """Fügt die Willkommensseite (welcome.html) an Position 0 in die Lobby-Playlist ein. - Entfernt vorher alle vorhandenen welcome.html-Einträge (als String oder Dict). +def _strip_welcome(playlist, filename): + """Entfernt alle welcome.html-Einträge aus einer Playlist (String- und Dict-Form).""" + 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).""" try: from app import load_config, save_config config = load_config() - sites_cfg = config.setdefault("sites", {}) - site_cfg = sites_cfg.setdefault(site, {"screens": {}}) - screens = site_cfg.setdefault("screens", {}) - lobby = screens.setdefault("lobby", {}) - playlist = lobby.setdefault("playlist", []) + site_cfg = config.setdefault("sites", {}).setdefault(site, {"screens": {}}) + all_screens = site_cfg.setdefault("screens", {}) + + if screens is None: + 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) - 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: print(f"❌ Generate - Error adding to playlist: {e}") return False diff --git a/templates/admin.html b/templates/admin.html index 02c2a19..957508a 100755 --- a/templates/admin.html +++ b/templates/admin.html @@ -344,13 +344,21 @@ -
+
+
+ + +
+
Erzeugte Willkommensseiten werden auf allen so markierten Screens dieses Standorts vorne in die Playlist gesetzt.
+ diff --git a/templates/customer.html b/templates/customer.html index 22551a7..5a8adbf 100644 --- a/templates/customer.html +++ b/templates/customer.html @@ -153,9 +153,19 @@
+
+ +
+ + +
Ohne Angabe bleibt die Willkommensseite stehen, bis sie von Hand entfernt wird. Mit Datum verschwindet sie nach diesem Tag auf allen Screens von selbst.
+
+

Das System sucht automatisch nach den Kundenlogos

-

und erstellt eine angepasste Willkommensseite für die Lobby-Playlist.

+

und erstellt eine angepasste Willkommensseite.

+

Sie erscheint auf allen Screens dieses Standorts, bei denen im Tab „Einstellungen" die Option Willkommensseite hier anzeigen aktiv ist{% if welcome_screens %} – aktuell: {{ welcome_screens | join(', ') }}{% else %} – aktuell keiner{% endif %}.

Ansprechpartner unterstützen fett, kursiv, Code, Aufzählungen (- ) sowie Zeilenumbrüche.

diff --git a/templates/help.html b/templates/help.html index bcd8652..16bdad2 100644 --- a/templates/help.html +++ b/templates/help.html @@ -249,7 +249,9 @@

Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.

  • Aufruf über /willkommen?site=<standort> (oder Menüpunkt)
  • -
  • Die generierte welcome.html wird automatisch an den Anfang der Lobby-Playlist gesetzt
  • +
  • Zielscreens: Die generierte welcome.html wird automatisch an den Anfang der Playlist jedes Screens gesetzt, bei dem im Tab "Einstellungen" die Option Willkommensseite hier anzeigen 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.
  • +
  • Wird ein Screen später abgewählt, verschwindet die Willkommensseite dort beim nächsten Generieren automatisch.
  • +
  • Anzeigen bis: 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.
  • Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)
  • Pro Kunde können Ansprechpartner eingegeben werden (Markdown-Format):