From 0e192a615ddd505d52872e9259b6d7e23736d94b Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Sun, 7 Jun 2026 12:35:10 +0200 Subject: [PATCH] =?UTF-8?q?Hilfetexte=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- README.md | 32 +++++++- app.py | 157 ++++++++++++++++++++++++++++----------- generate_welcome_page.py | 75 ++++++++++++++----- templates/_footer.html | 6 +- templates/_header.html | 16 +++- templates/admin.html | 63 ++++++++++------ templates/customer.html | 15 +++- templates/login.html | 6 +- templates/player.html | 135 ++++++++++++++++----------------- 10 files changed, 339 insertions(+), 170 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4f9a3ad..8ef7f4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ docker compose up -d - Single Flask app in `app.py`; there is no database. - Persistent state is `config.json` plus files under `media///`. -- `README.md` is stale on the port number; trust `app.py`. +- Port 5005; `README.md` hat den korrekten Port. - Existing repo instructions in this file are the main local guidance; there is no `opencode.json` or workflow config in this repo. ## Multi-Standort-URL-Struktur (seit v4.2.0) @@ -55,7 +55,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens. - `add_customer_to_lobby_playlist` entfernt `welcome.html` sowohl als String als auch als Dict aus der Playlist vor dem Einfügen. - `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_target`) im Admin-Formular; wird im Player als Button oben links 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. +- Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_enabled` + `custom_url_target`) im Admin-Formular; wird im Player als Button oben links 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. - Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=` angelegt werden. - `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv. - `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet. diff --git a/README.md b/README.md index 6cb6927..f0b5c07 100755 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ signage/ ### Setup ```bash -pip install flask flask-login werkzeug openai +pip install -r requirements.txt ``` ### Starten @@ -135,8 +135,6 @@ docker compose up -d | `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) | | `GET /media///` | Medien-Datei ausliefern | | `GET /media/priority/` | Priority-Medien (global) | -| `GET /add-site?name=` | Neuen Standort anlegen | -| `POST /admin//delete-site` | Standort + alle Screens/Medien löschen | ### Admin-Portal @@ -182,7 +180,18 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → ` "sites": { "stuttgart": { "screens": { - "lobby": { "playlist": [...], "interval": 10 }, + "lobby": { + "playlist": [...], + "interval": 10, + "show_images": true, + "show_videos": true, + "newsticker_text": "...", + "newsticker_enabled": false, + "custom_url_enabled": false, + "custom_url": "https://...", + "custom_url_label": "Infos", + "custom_url_target": "overlay" + }, "casino": { "playlist": [...], "interval": 15 } } } @@ -196,6 +205,21 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → ` --- +## Screen-Konfiguration (pro Screen in `config.json`) + +| Feld | Typ | Beschreibung | +|------|-----|-------------| +| `playlist` | Array | Playlist-Einträge (Strings oder Dicts mit `url`/`zoom`) | +| `interval` | int | Anzeige-Intervall in Sekunden | +| `show_images` | bool | Bilder anzeigen | +| `show_videos` | bool | Videos anzeigen | +| `newsticker_text` | string | Text für Newsticker (max. 200 Zeichen) | +| `newsticker_enabled` | bool | Newsticker anzeigen | +| `custom_url_enabled` | bool | Custom-URL-Button im Player anzeigen | +| `custom_url` | string | URL des Aktions-Buttons | +| `custom_url_label` | string | Button-Beschriftung | +| `custom_url_target` | string | `"overlay"` (iframe) oder `"redirect"` | + ## Helper-Funktionen (`app.py`) - `load_config()` / `save_config()` – JSON lesen/schreiben diff --git a/app.py b/app.py index de498b5..41ddea2 100755 --- a/app.py +++ b/app.py @@ -1,8 +1,17 @@ +#!/usr/bin/env python3 +""" +CANCOM Simple Signage – Flask-App für Digital-Signage-Anzeigen. +────────────────────────────────────────────────────────────── +Diese App verwaltet mehrere Standorte (sites) mit jeweils mehreren +Screens. Jeder Screen hat eine eigene Playlist aus Bildern, Videos, +HTML-Seiten und URLs. Optional kann eine globale Priority-Playlist +zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird. +""" + import os import json import hashlib -# Import welcome page functions -import generate_welcome_page +import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung from datetime import datetime @@ -23,8 +32,8 @@ from werkzeug.utils import secure_filename # Grundkonfiguration # ------------------------------------------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -MEDIA_DIR = os.path.join(BASE_DIR, "media") -CONFIG_FILE = os.path.join(BASE_DIR, "config.json") +MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/Screen +CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration APP_VERSION = "5.1.0" UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} @@ -33,28 +42,35 @@ app = Flask(__name__) app.secret_key = "CHANGE_THIS_SECRET!!!" login_manager = LoginManager(app) -login_manager.login_view = "login" +login_manager.login_view = "login" # Nicht eingeloggte User werden zum Login umgeleitet + # ------------------------------------------------- -# Config Helpers +# Config-Helfer # ------------------------------------------------- def load_config(): + """Liest die config.json und gibt das Dict zurück. Legt ein leeres an, falls nicht vorhanden.""" if not os.path.exists(CONFIG_FILE): return {"admin": {}, "sites": {}} with open(CONFIG_FILE) as f: return json.load(f) + def save_config(cfg): + """Schreibt das Config-Dict zurück in die config.json.""" with open(CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) f.flush() def get_site_list(cfg): + """Gibt eine sortierte Liste aller konfigurierten Standort-Namen zurück.""" return sorted(cfg.get("sites", {}).keys()) def get_screen_config(cfg, site, screen): + """Holt die Konfiguration für einen bestimmten Screen. + Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen).""" return ( cfg .setdefault("sites", {}) @@ -65,12 +81,15 @@ def get_screen_config(cfg, site, screen): def is_url(item): + """Prüft, ob ein Playlist-Eintrag eine URL ist (String mit http/https) oder ein Dict mit "url"-Key.""" if isinstance(item, str): return item.startswith("http://") or item.startswith("https://") return isinstance(item, dict) and "url" in item def normalize_url(item): + """Normalisiert einen URL-Eintrag zu einem Dict mit "url" und "zoom". + Zoom wird aus Dict übernommen oder defaultmäßig auf 1.0 gesetzt.""" if isinstance(item, str): return {"url": item, "zoom": 1.0} if isinstance(item, dict): @@ -79,6 +98,7 @@ def normalize_url(item): def playlist_item_name(item): + """Ermittelt den Anzeigenamen eines Playlist-Eintrags (String → direkt, Dict → "url" oder "name").""" if isinstance(item, str): return item if isinstance(item, dict): @@ -87,6 +107,8 @@ def playlist_item_name(item): def playlist_item_enabled(item): + """Prüft, ob ein Playlist-Eintrag aktiviert ist. + Strings sind immer aktiv, Dicts haben ein optionales "enabled"-Feld (default: True).""" if isinstance(item, (str,)): return True if isinstance(item, dict): @@ -95,9 +117,12 @@ def playlist_item_enabled(item): # ------------------------------------------------- -# Priority-Helper +# 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. + 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") os.makedirs(prio_dir, exist_ok=True) @@ -105,6 +130,7 @@ def load_priority_files(): priority_files = [] prio_playlist = cfg.get("priority", {}).get("playlist", []) + # 1. Einträge aus der config.json-Playlist for item in prio_playlist: item_name = playlist_item_name(item) if is_url(item): @@ -134,6 +160,7 @@ def load_priority_files(): "enabled": playlist_item_enabled(item) }) + # 2. Neue Dateien aus dem Ordner, die noch nicht in der Playlist sind playlist_names = {playlist_item_name(item) for item in prio_playlist} for f in sorted(os.listdir(prio_dir)): if f in playlist_names or f.startswith("._"): @@ -154,26 +181,30 @@ def load_priority_files(): def prio_redirect(site): + """Gibt den Redirect-Pfad zurück, der nach Priority-Operationen aufgerufen wird.""" return f"/admin/{site}/priority" # ------------------------------------------------- -# User & Login -# ------------------------------------------------- -# Auth / Benutzer +# User & Login (Flask-Login) # ------------------------------------------------- class Admin(UserMixin): + """Einfacher Admin-User. Es gibt nur einen Admin (keine DB).""" id = 1 + @login_manager.user_loader def load_user(user_id): + """Lädt den Admin-User für Flask-Login. user_id wird ignoriert – es gibt nur einen User.""" return Admin() + # ------------------------------------------------- # Login / Logout # ------------------------------------------------- @app.route("/login", methods=["GET", "POST"]) def login(): + """Login-Seite. Bei POST werden die Zugangsdaten aus config.json.admin geprüft.""" config = load_config() error = None @@ -188,26 +219,33 @@ def login(): return render_template("login.html", error=error) + @app.route("/logout") def logout(): + """Loggt den Admin aus und leitet zur Login-Seite weiter.""" logout_user() return redirect("/login") + # ------------------------------------------------- -# Customer / Willkommensseite +# Willkommensseite (Kunden-Logos + Generierung) # ------------------------------------------------- @app.route("/willkommen", methods=["GET", "POST"]) @app.route("/customer", methods=["GET", "POST"]) -# @login_required def add_customer(): - """Add new customer with logo search and welcome page""" + """ + 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 + """ error = None success = None logo_urls = [None, None, None] customer_names = ["", "", ""] site = request.args.get("site") or request.form.get("site") or "" - # Default to first site if not specified + # Fallback auf ersten Standort, wenn keiner angegeben if not site: config = load_config() sites = get_site_list(config) @@ -277,10 +315,14 @@ def add_customer(): hostname=os.uname().nodename ) + @app.route("/api/customer", methods=["POST"]) -# @login_required def api_add_customer(): - """API endpoint for customer creation""" + """ + API-Endpunkt für die Willkommensseite (JSON). + Erwartet: {"customer_name": "...", "site": "..."} + Gibt die Logo-URL und den Status zurück. + """ try: data = request.get_json() customer_name = data.get("customer_name", "").strip() @@ -289,7 +331,7 @@ def api_add_customer(): if not customer_name: return jsonify({"error": "Customer name required"}), 400 - # Default to first site + # Default auf ersten Standort if not site: config = load_config() sites = get_site_list(config) @@ -324,25 +366,34 @@ def api_add_customer(): except Exception as e: return jsonify({"error": str(e)}), 500 + # ------------------------------------------------- -# Medien ausliefern +# Medien ausliefern (statische Dateien) # ------------------------------------------------- @app.route("/media/priority/") 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///") def media(site, screen, filename): + """Liefert Medien-Dateien für einen bestimmten Screen aus.""" path = os.path.join(MEDIA_DIR, site, screen) return send_from_directory(path, filename) + # ------------------------------------------------- # Player # ------------------------------------------------- - @app.route("/player//") def player(site, screen): + """ + Player-Ansicht für einen Screen. + Baut die Playlist (normal + optional priority) zusammen und rendert player.html. + Der Player zeigt abwechselnd Bilder, Videos, HTML-Seiten und URLs an. + """ config = load_config() sites_cfg = config.setdefault("sites", {}) site_cfg = sites_cfg.get(site) @@ -363,6 +414,7 @@ def player(site, screen): show_videos = screen_cfg.get("show_videos", True) def allowed_file(name): + """Prüft, ob eine Datei angezeigt werden darf (abhängig von show_images / show_videos).""" if name.startswith("._"): return False ext = name.lower() @@ -374,12 +426,10 @@ def player(site, screen): return True return False - # ------------------------------ - # Normale Screen-Playlist - # ------------------------------ + # ─── Normale Screen-Playlist ─── normal_files = [] - # 1. Playlist-Reihenfolge + # 1. Playlist-Reihenfolge aus config.json for item in playlist: if not playlist_item_enabled(item): continue @@ -397,7 +447,7 @@ def player(site, screen): item["name"] for item in normal_files if item["kind"] == "file" } - # 2. Neue Dateien hintendran + # 2. Neue Dateien aus dem Medien-Ordner (ohne Playlist-Eintrag) hinten anhängen if not playlist: for f in sorted(os.listdir(folder)): if f in existing_file_names or f.startswith("._"): @@ -405,9 +455,7 @@ def player(site, screen): if allowed_file(f): normal_files.append({"kind": "file", "name": f}) - # ------------------------------ - # Priority-Playlist (global) - # ------------------------------ + # ─── Priority-Playlist (global) ─── priority_cfg = config.get("priority", {}) if priority_cfg.get("enabled", False): @@ -446,9 +494,14 @@ def player(site, screen): # ------------------------------------------------- # Hash für automatisches Player-Reload +# ───────────────────────────────────────────────── +# Der Player pollt alle 5s diese Route. +# Wenn sich der Hash ändert, wird neu geladen. # ------------------------------------------------- @app.route("/playlist///hash") def playlist_hash(site, screen): + """Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority). + Ändert sich der Hash, lädt der Player automatisch neu.""" config = load_config() relevant = { @@ -463,13 +516,13 @@ def playlist_hash(site, screen): # ------------------------------------------------- -# Admin Dashboard +# Admin # ------------------------------------------------- @app.route("/admin") @login_required def admin_redirect(): - """Redirect to first available site""" + """Leitet zum ersten konfigurierten Standort weiter.""" cfg = load_config() sites = get_site_list(cfg) if sites: @@ -480,6 +533,7 @@ def admin_redirect(): @app.route("/add-site") @login_required def add_site(): + """Legt einen neuen Standort an (Verzeichnis + Config-Eintrag).""" name = request.args.get("name", "").strip().lower() if not name: return redirect("/admin") @@ -495,6 +549,7 @@ def add_site(): @app.route("/admin//delete-site", methods=["POST"]) @login_required def delete_site(site): + """Löscht einen Standort inkl. aller Screens und Medien (Config + Verzeichnis).""" cfg = load_config() cfg.get("sites", {}).pop(site, None) save_config(cfg) @@ -511,13 +566,16 @@ def delete_site(site): @app.route("/admin/") @login_required def admin(site): + """ + Admin-Dashboard für einen Standort. + Zeigt alle Screens als Cards mit Tabler-Tabs (Playlist, Einstellungen, Medien). + """ cfg = load_config() site_list = get_site_list(cfg) - # Validate site exists + # Prüfen, ob der Standort existiert if site not in site_list: - # Create default site config if filesystem dir exists but no config if os.path.isdir(os.path.join(MEDIA_DIR, site)): cfg.setdefault("sites", {})[site] = {"screens": {}} save_config(cfg) @@ -532,19 +590,19 @@ def admin(site): site_media_base = os.path.join(MEDIA_DIR, current_site) - # Discover screens: from config + filesystem + # Screens erkennen: aus Config + Dateisystem known_screens = set() site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}}) config_screens = site_cfg.setdefault("screens", {}) - # Merge with filesystem directories + # Ordner im Medienverzeichnis = Screens 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("._"): known_screens.add(entry) - # Add screens from config that might not have a directory yet + # Screens aus Config ergänzen (auch ohne Ordner) for s in config_screens: known_screens.add(s) @@ -558,7 +616,7 @@ def admin(site): screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "") screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False) - # 1 Playlist-Reihenfolge + # 1. Playlist-Einträge in Config-Reihenfolge for item in playlist: item_name = playlist_item_name(item) @@ -595,7 +653,7 @@ def admin(site): "enabled": playlist_item_enabled(item) }) - # 2 Neue Dateien anhängen + # 2. Neue Dateien (ohne Playlist-Eintrag) anhängen playlist_names = {playlist_item_name(item) for item in playlist} for f in sorted(os.listdir(path)): if f in playlist_names or f.startswith("._"): @@ -631,11 +689,12 @@ def admin(site): # ------------------------------------------------- -# Admin: Priority-Playlist (separate Seite) +# Admin: Priority-Playlist (eigene Seite) # ------------------------------------------------- @app.route("/admin//priority") @login_required def admin_priority(site): + """Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien).""" cfg = load_config() site_list = sorted(cfg.get("sites", {}).keys()) priority_files = load_priority_files() @@ -658,6 +717,7 @@ def admin_priority(site): @app.route("/admin//update/", methods=["POST"]) @login_required def update_screen(site, screen): + """Speichert die Einstellungen für einen Screen (Intervall, Newsticker, Show-Bilder/Videos, Custom-URL).""" config = load_config() cfg = get_screen_config(config, site, screen) @@ -681,14 +741,13 @@ def update_screen(site, screen): @app.route("/admin//delete-screen/", methods=["POST"]) @login_required def delete_screen(site, screen): + """Löscht einen Screen (Config-Eintrag + Medien-Verzeichnis).""" config = load_config() - # Aus config.json entfernen screens_cfg = config.get("sites", {}).get(site, {}).get("screens", {}) screens_cfg.pop(screen, None) save_config(config) - # Medien-Verzeichnis löschen screen_dir = os.path.join(MEDIA_DIR, site, screen) if os.path.isdir(screen_dir): import shutil @@ -703,6 +762,7 @@ def delete_screen(site, screen): @app.route("/admin//add-screen", methods=["GET"]) @login_required def add_screen(site): + """Legt einen neuen Screen an (Verzeichnis + Config-Eintrag).""" screen_name = request.args.get("name", "").strip().lower() if not screen_name: return redirect(f"/admin/{site}") @@ -720,11 +780,12 @@ def add_screen(site): # ------------------------------------------------- -# Admin: Upload +# Admin: Medien-Upload # ------------------------------------------------- @app.route("/admin//upload/", methods=["POST"]) @login_required def upload(site, screen): + """Lädt eine Datei hoch (Bild oder Video). Bei screen="priority" ins globale Verzeichnis.""" file = request.files.get("file") if not file: return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}") @@ -744,11 +805,12 @@ def upload(site, screen): # ------------------------------------------------- -# Admin: URL hinzufügen +# Admin: URL zur Playlist hinzufügen # ------------------------------------------------- @app.route("/admin//add-url/", methods=["POST"]) @login_required def add_url(site, screen): + """Fügt eine URL (mit Zoom-Faktor) zur Playlist eines Screens oder der Priority-Playlist hinzu.""" url = request.form.get("url", "").strip() zoom = request.form.get("zoom", "1.0").strip() @@ -776,12 +838,12 @@ def add_url(site, screen): # ------------------------------------------------- -# Admin: Datei löschen +# Admin: Datei aus Playlist löschen # ------------------------------------------------- - @app.route("/admin//delete//", methods=["POST"]) @login_required def delete_file(site, screen, filename): + """Löscht eine Datei aus der Playlist (Config + Dateisystem).""" cfg = load_config() if screen == "priority": @@ -808,12 +870,15 @@ def delete_file(site, screen, filename): # ------------------------------------------------- -# Admin: Playlist speichern +# Admin: Playlist-Reihenfolge speichern (Drag & Drop) # ------------------------------------------------- - @app.route("/admin//playlist/", methods=["POST"]) @login_required def save_playlist(site, screen): + """ + Speichert die über SortableJS per Drag & Drop neu sortierte Playlist. + Der Client schickt ein JSON mit der neuen Reihenfolge und enabled-Status. + """ cfg = load_config() data = request.get_json() new_order = data["playlist"] @@ -825,10 +890,12 @@ def save_playlist(site, screen): screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {}) old_playlist = screens_cfg.get(screen, {}).get("playlist", []) + # Mapping: Name → Original-Item (für Zoom etc.) item_map = {} for item in old_playlist: item_map[playlist_item_name(item)] = item + # Neue Playlist in der gewünschten Reihenfolge bauen new_playlist = [] for key in new_order: if key in item_map: diff --git a/generate_welcome_page.py b/generate_welcome_page.py index 4a41f4b..fe2f50d 100644 --- a/generate_welcome_page.py +++ b/generate_welcome_page.py @@ -1,40 +1,61 @@ +#!/usr/bin/env python3 +""" +Willkommensseite-Generierung für CANCOM Simple Signage. +─────────────────────────────────────────────────────── +Diese Hilfsdatei sucht anhand von Kundennamen Logos (via OpenAI + Brandfetch), +generiert daraus eine HTML-Willkommensseite und fügt sie in die Lobby-Playlist ein. +""" + import os import json from openai import OpenAI -# ------------------------------------------------- -# Customer / Willkommensseite -# ------------------------------------------------- - BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MEDIA_DIR = os.path.join(BASE_DIR, "media") WELCOME_FILENAME = "welcome.html" + +# ------------------------------------------------- +# OpenAI-Client +# ------------------------------------------------- + def get_openai_client(): - """Initialize OpenAI client from environment variable""" - # api_key = os.getenv("OPENAI_API_KEY") + """Initialisiert den OpenAI-Client mit dem API-Key. + Der Key ist aktuell direkt im Code hinterlegt – sollte in env var ausgelagert werden.""" api_key = "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" if not api_key: raise ValueError("OPENAI_API_KEY environment variable not set") return OpenAI(api_key=api_key) + +# ------------------------------------------------- +# Brandfetch-Logo-Suche +# ------------------------------------------------- + def get_brandfetch_logo(domain): - """Try to fetch a logo URL from Brandfetch CDN.""" + """Versucht, ein Firmenlogo über Brandfetch CDN abzurufen. + Baut eine direkte CDN-URL: https://cdn.brandfetch.io/{domain}/logo""" print(f"🔍 Generate - Versuche Brandfetch für Domain: {domain}") - # Direct CDN URL construction - Brandfetch provides logos via cdn.brandfetch.io logo_url = f"https://cdn.brandfetch.io/{domain}/logo?c=1idyd4Tpb2nKaXIIc8T" - # if validate_logo_url(logo_url): print(f"✅ Generate - Brandfetch Logo gefunden: {logo_url}") return logo_url - # return None +# ------------------------------------------------- +# Logo-Ermittlung via OpenAI +# ------------------------------------------------- def search_customer_logo(customer_name): + """ + Sucht das Logo eines Kunden über einen zweistufigen Prozess: + 1. OpenAI GPT-4 ermittelt die Domain des Unternehmens + 2. Brandfetch CDN liefert das Logo zur Domain + + Gibt die Logo-URL zurück oder None, wenn nichts gefunden wurde. + """ print(f"✅ Generate - Suche nach Kundenlogo für: {customer_name}") - """Search for customer logo URL using OpenAI and web search""" try: client = get_openai_client() @@ -57,7 +78,7 @@ def search_customer_logo(customer_name): ) logo_url = response.choices[0].message.content.strip() - print (f"🔍 OpenAI Logo-URL: {logo_url}") + print(f"🔍 OpenAI Logo-URL: {logo_url}") if logo_url.upper() == "FALLBACK": logo_url = None @@ -72,7 +93,20 @@ def search_customer_logo(customer_name): return None +# ------------------------------------------------- +# HTML-Generierung +# ------------------------------------------------- + def generate_welcome_html(customer_names, logo_urls, site="stuttgart"): + """ + Generiert den HTML-Code für die Willkommensseite. + - Links oben: CANCOM-Logo + "meets" + Kundenlogos (280px Breite) + - Rechts unten: "Herzlich Willkommen in unserer Niederlassung {Standort}" + + customer_names: Liste der Kundennamen (max. 3) + logo_urls: Liste der Logo-URLs (gleiche Reihenfolge) + site: Standort-Name für die Anzeige + """ site_display = site.capitalize() names_str = ", ".join(customer_names) if customer_names else "Unbekannt" print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})") @@ -174,8 +208,13 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart"): return html_content +# ------------------------------------------------- +# Speichern + Playlist-Einfügung +# ------------------------------------------------- + def save_welcome_page(customer_names, logo_urls, site="stuttgart"): - """Save welcome page and return filename""" + """Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis. + 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) @@ -196,9 +235,10 @@ def save_welcome_page(customer_names, logo_urls, site="stuttgart"): def add_customer_to_lobby_playlist(html_filename, site="stuttgart"): - """Add customer welcome page to lobby playlist for a specific site""" + """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). + Importiert load_config/save_config aus app.py (kein Zirkelimport).""" try: - # Import here to avoid circular imports from app import load_config, save_config config = load_config() @@ -208,7 +248,8 @@ def add_customer_to_lobby_playlist(html_filename, site="stuttgart"): lobby = screens.setdefault("lobby", {}) playlist = lobby.setdefault("playlist", []) - full_filename = f"{html_filename}" + 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 ( @@ -222,4 +263,4 @@ def add_customer_to_lobby_playlist(html_filename, site="stuttgart"): return True except Exception as e: print(f"❌ Generate - Error adding to playlist: {e}") - return False \ No newline at end of file + return False diff --git a/templates/_footer.html b/templates/_footer.html index 28eb16d..0f126ac 100644 --- a/templates/_footer.html +++ b/templates/_footer.html @@ -1,4 +1,8 @@ - +