fix: hochgeladene Logos nicht mehr in einem Screen-Ordner ablegen

Hochgeladene Logos landeten unter media/<site>/logos/. admin() behandelt
aber jeden Unterordner von media/<site>/ als Screen und schreibt ihn per
get_screen_config() dauerhaft in die Konfiguration. Der Logo-Ordner
erschien dadurch als Screen in der Oberflaeche, bekam beim Generieren
eine eigene welcome.html und liess sich wie ein Screen bespielen - und
die darin liegenden Logos wie Screen-Medien loeschen. Genau so ist eines
der hochgeladenen Logos verschwunden.

Logos liegen jetzt als media/<site>/logo_<slot>.png direkt im
Standort-Verzeichnis, so wie das Hintergrundbild als
media/<site>/background.* - aus demselben Grund. Die URL bleibt
/media/<site>/logos/<datei>, die Route liest aus dem
Standort-Verzeichnis; bestehende welcome.html funktionieren damit
unveraendert weiter.

Zusaetzlich wird der Ordnername "logos" bei der Screen-Erkennung
uebersprungen, damit Altbestaende nicht erneut als Screen auftauchen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-09-02 15:14:20 +02:00
parent 8de8a116bb
commit a3b9fe59b7
4 changed files with 26 additions and 13 deletions

View File

@@ -121,7 +121,8 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- 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_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. - `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. - `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.
- **Logo-Upload**: Pro Kunde kann im Willkommensseiten-Formular eine Bilddatei hochgeladen werden (`logo_file_1..3`); ein Upload hat Vorrang vor `search_customer_logo`. Ablage als `media/<site>/logos/logo_<slot>.png`, Auslieferung über `/media/<site>/logos/<datei>`. Die URL traegt einen `?v=<timestamp>`, damit Player kein altes Logo aus dem Cache zeigen. - **Logo-Upload**: Pro Kunde kann im Willkommensseiten-Formular eine Bilddatei hochgeladen werden (`logo_file_1..3`); ein Upload hat Vorrang vor `search_customer_logo`. Ablage als `media/<site>/logo_<slot>.png`, Auslieferung über `/media/<site>/logos/<datei>` (die Route liest aus dem Standort-Verzeichnis).
- **Keine neuen Unterordner unter `media/<site>/` anlegen.** `admin()` behandelt jeden Unterordner dort als Screen und persistiert ihn ueber `get_screen_config()` in der Konfiguration - ein Ordner `logos` erschien dadurch als Phantom-Screen, bekam eine `welcome.html` und liess sich ueber die Admin-UI bespielen und loeschen. Deshalb liegen Logos wie das Hintergrundbild als Dateien direkt im Standort-Verzeichnis. Der Name `logos` ist in der Screen-Erkennung zusaetzlich ausgenommen, damit Altbestaende nicht wieder auftauchen. Die URL traegt einen `?v=<timestamp>`, damit Player kein altes Logo aus dem Cache zeigen.
- Nach dem Freistellen schneidet `trim_transparent_border()` die durchsichtigen Raender weg. Das ist kein Schoenheitsdetail: `.customer-logo` skaliert mit `object-fit: contain` auf 120px Hoehe, ein breiter Rand zaehlt dabei mit und laesst ein hochgeladenes Logo deutlich kleiner erscheinen als ein eng beschnittenes Brandfetch-Logo (gemessen: 30px statt 120px). Der Schnitt greift auch bei bereits transparenten PNGs, aber nicht bei abgewaehltem Freistellen - dort gibt es keinen transparenten Rand. - Nach dem Freistellen schneidet `trim_transparent_border()` die durchsichtigen Raender weg. Das ist kein Schoenheitsdetail: `.customer-logo` skaliert mit `object-fit: contain` auf 120px Hoehe, ein breiter Rand zaehlt dabei mit und laesst ein hochgeladenes Logo deutlich kleiner erscheinen als ein eng beschnittenes Brandfetch-Logo (gemessen: 30px statt 120px). Der Schnitt greift auch bei bereits transparenten PNGs, aber nicht bei abgewaehltem Freistellen - dort gibt es keinen transparenten Rand.
- `remove_logo_background()` arbeitet mit Pillow (Flood Fill von den Raendern, weiche Kante). **Bewusst kein ML-Modell** wie rembg: der Container laeuft mit read-only Root-FS ohne Modell-Cache. Flood Fill statt globalem Farbvergleich ist wesentlich - sonst verschwindet auch Weiss *innerhalb* des Logos. Bereits transparente Bilder werden unveraendert durchgereicht; bei unruhigem Bildrand bleibt das Bild unangetastet und die Funktion gibt einen Hinweis zurueck. - `remove_logo_background()` arbeitet mit Pillow (Flood Fill von den Raendern, weiche Kante). **Bewusst kein ML-Modell** wie rembg: der Container laeuft mit read-only Root-FS ohne Modell-Cache. Flood Fill statt globalem Farbvergleich ist wesentlich - sonst verschwindet auch Weiss *innerhalb* des Logos. Bereits transparente Bilder werden unveraendert durchgereicht; bei unruhigem Bildrand bleibt das Bild unangetastet und die Funktion gibt einen Hinweis zurueck.
- `welcome_data` in der Site-Config wird **feldweise** gespeichert (Slot 1-3, auch leere), nicht nur die erfolgreichen Kunden - sonst verschiebt sich die Zuordnung von Name, Logo und Ansprechpartner beim naechsten Aufruf. Ein zuvor hochgeladenes Logo wird beim erneuten Generieren wiederverwendet, solange der Kundenname im selben Feld unveraendert ist. - `welcome_data` in der Site-Config wird **feldweise** gespeichert (Slot 1-3, auch leere), nicht nur die erfolgreichen Kunden - sonst verschiebt sich die Zuordnung von Name, Logo und Ansprechpartner beim naechsten Aufruf. Ein zuvor hochgeladenes Logo wird beim erneuten Generieren wiederverwendet, solange der Kundenname im selben Feld unveraendert ist.

View File

@@ -298,7 +298,7 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
**Der Schalter wirkt sofort:** Beim Einschalten wird eine bereits vorhandene Willkommensseite unmittelbar in die Playlist des Screens gesetzt, beim Ausschalten wieder entfernt (samt Datei) – ohne erneutes Generieren. Die Seite ist je Standort identisch, ein zweiter Handgriff wäre also nur Aufwand ohne anderes Ergebnis. Beim Generieren wird zusätzlich eine Standort-Kopie unter `media/<site>/welcome.html` abgelegt, die nie gelöscht wird; dadurch findet der Schalter auch dann noch eine Quelle, wenn die Seite zwischenzeitlich auf allen Screens abgeschaltet war. **Der Schalter wirkt sofort:** Beim Einschalten wird eine bereits vorhandene Willkommensseite unmittelbar in die Playlist des Screens gesetzt, beim Ausschalten wieder entfernt (samt Datei) – ohne erneutes Generieren. Die Seite ist je Standort identisch, ein zweiter Handgriff wäre also nur Aufwand ohne anderes Ergebnis. Beim Generieren wird zusätzlich eine Standort-Kopie unter `media/<site>/welcome.html` abgelegt, die nie gelöscht wird; dadurch findet der Schalter auch dann noch eine Quelle, wenn die Seite zwischenzeitlich auf allen Screens abgeschaltet war.
**Logo-Upload:** Findet die automatische Suche kein Logo (oder passt es nicht), kann pro Kunde eine Bilddatei hochgeladen werden. Ein Upload hat Vorrang vor der Suche. Die Datei wird als `media/<site>/logos/logo_<slot>.png` abgelegt und über `/media/<site>/logos/<datei>` ausgeliefert. **Logo-Upload:** Findet die automatische Suche kein Logo (oder passt es nicht), kann pro Kunde eine Bilddatei hochgeladen werden. Ein Upload hat Vorrang vor der Suche. Die Datei wird als `media/<site>/logo_<slot>.png` abgelegt und über `/media/<site>/logos/<datei>` ausgeliefert. **Bewusst als Datei im Standort-Verzeichnis, nicht in einem Unterordner:** `admin()` behandelt jeden Unterordner von `media/<site>/` als Screen und schreibt ihn dauerhaft in die Konfiguration – ein Ordner `logos` würde also als Phantom-Screen in der Oberfläche auftauchen. Das Hintergrundbild liegt aus demselben Grund als `media/<site>/background.*`. Der Ordnername `logos` ist zusätzlich reserviert und wird bei der Screen-Erkennung übersprungen.
**Freistellen und Zuschneiden:** Beim Upload wird der Hintergrund automatisch entfernt, damit das Logo nicht in einem weissen Kasten auf dem Hintergrundbild steht. Anschliessend werden die durchsichtigen Ränder weggeschnitten (`trim_transparent_border`). Ohne diesen Schnitt erscheint ein hochgeladenes Logo kleiner als ein automatisch gefundenes: die Willkommensseite skaliert mit `object-fit: contain` auf eine feste Höhe von 120px, und ein breiter Rand zählt dabei mit – Brandfetch-Logos sind bereits eng beschnitten. Verfahren: Flood Fill von den Bildrändern her (bewusst kein globaler Farbvergleich – so bleibt Weiss *innerhalb* des Logos erhalten), weiche Kante gegen Treppeneffekte, Speicherung immer als PNG. Bereits transparente Bilder werden unverändert übernommen; ist der Bildrand nicht einfarbig, bleibt das Bild unangetastet und die Oberfläche weist darauf hin. Mit der Checkbox „Hintergrund behalten" lässt sich das Freistellen pro Kunde abschalten. Umgesetzt mit Pillow – bewusst ohne ML-Modell, da der Container mit read-only Root-FS läuft und kein Modell-Cache zur Verfügung steht. **Freistellen und Zuschneiden:** Beim Upload wird der Hintergrund automatisch entfernt, damit das Logo nicht in einem weissen Kasten auf dem Hintergrundbild steht. Anschliessend werden die durchsichtigen Ränder weggeschnitten (`trim_transparent_border`). Ohne diesen Schnitt erscheint ein hochgeladenes Logo kleiner als ein automatisch gefundenes: die Willkommensseite skaliert mit `object-fit: contain` auf eine feste Höhe von 120px, und ein breiter Rand zählt dabei mit – Brandfetch-Logos sind bereits eng beschnitten. Verfahren: Flood Fill von den Bildrändern her (bewusst kein globaler Farbvergleich – so bleibt Weiss *innerhalb* des Logos erhalten), weiche Kante gegen Treppeneffekte, Speicherung immer als PNG. Bereits transparente Bilder werden unverändert übernommen; ist der Bildrand nicht einfarbig, bleibt das Bild unangetastet und die Oberfläche weist darauf hin. Mit der Checkbox „Hintergrund behalten" lässt sich das Freistellen pro Kunde abschalten. Umgesetzt mit Pillow – bewusst ohne ML-Modell, da der Container mit read-only Root-FS läuft und kein Modell-Cache zur Verfügung steht.

17
app.py
View File

@@ -496,8 +496,10 @@ def media_background(site, filename):
@app.route("/media/<site>/logos/<path:filename>") @app.route("/media/<site>/logos/<path:filename>")
def media_logo(site, filename): def media_logo(site, filename):
"""Liefert ein manuell hochgeladenes Kundenlogo aus. """Liefert ein manuell hochgeladenes Kundenlogo aus.
Ohne Login erreichbar wie die übrigen Medien - der Player ist nicht angemeldet.""" Ohne Login erreichbar wie die übrigen Medien - der Player ist nicht angemeldet.
path = os.path.join(MEDIA_DIR, site, generate_welcome_page.LOGO_DIRNAME) Die Dateien liegen direkt im Standort-Verzeichnis, damit kein Unterordner
entsteht, den admin() als Screen interpretieren wuerde."""
path = os.path.join(MEDIA_DIR, site)
return send_from_directory(path, filename) return send_from_directory(path, filename)
@@ -950,7 +952,7 @@ def add_customer():
frueher = load_config().get("sites", {}).get(site, {}).get("welcome_data", {}) frueher = load_config().get("sites", {}).get(site, {}).get("welcome_data", {})
frueher_namen = frueher.get("names", []) frueher_namen = frueher.get("names", [])
frueher_urls = frueher.get("logo_urls", []) frueher_urls = frueher.get("logo_urls", [])
logo_prefix = f"/media/{site}/{generate_welcome_page.LOGO_DIRNAME}/" logo_prefix = f"/media/{site}/{generate_welcome_page.LOGO_URL_PREFIX}/"
found_logos = [None, None, None] found_logos = [None, None, None]
for i, name in enumerate(customer_names): for i, name in enumerate(customer_names):
@@ -1401,11 +1403,16 @@ def admin(site):
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}}) site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
config_screens = site_cfg.setdefault("screens", {}) config_screens = site_cfg.setdefault("screens", {})
# Ordner im Medienverzeichnis = 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.
if os.path.isdir(site_media_base): if os.path.isdir(site_media_base):
for entry in os.listdir(site_media_base): for entry in os.listdir(site_media_base):
entry_path = os.path.join(site_media_base, entry) entry_path = os.path.join(site_media_base, entry)
if os.path.isdir(entry_path) and not entry.startswith("._"): if (os.path.isdir(entry_path)
and not entry.startswith("._")
and entry != generate_welcome_page.LOGO_URL_PREFIX):
known_screens.add(entry) known_screens.add(entry)
# Screens aus Config ergänzen (auch ohne Ordner) # Screens aus Config ergänzen (auch ohne Ordner)

View File

@@ -161,7 +161,12 @@ def remove_logo_background(image, tolerance=LOGO_TOLERANCE):
return img, None return img, None
LOGO_DIRNAME = "logos" # Logos liegen als Dateien direkt im Standort-Verzeichnis, nicht in einem
# Unterordner: admin() behandelt jeden Unterordner von media/<site>/ als Screen
# und legt ihn dauerhaft in der Konfiguration an. Das Hintergrundbild wird aus
# demselben Grund als media/<site>/background.* abgelegt.
LOGO_URL_PREFIX = "logos" # nur der Pfadbestandteil in der URL
LOGO_FILENAME = "logo_{slot}.png"
LOGO_EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp") LOGO_EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
@@ -198,12 +203,12 @@ def save_uploaded_logo(file_storage, site, slot, freistellen=True):
if img.size != vorher: if img.size != vorher:
print(f"✅ Generate - Logo zugeschnitten: {vorher[0]}x{vorher[1]} → {img.size[0]}x{img.size[1]}") print(f"✅ Generate - Logo zugeschnitten: {vorher[0]}x{vorher[1]} → {img.size[0]}x{img.size[1]}")
logo_dir = os.path.join(MEDIA_DIR, site, LOGO_DIRNAME) site_dir = os.path.join(MEDIA_DIR, site)
os.makedirs(logo_dir, exist_ok=True) os.makedirs(site_dir, exist_ok=True)
dateiname = f"logo_{slot}.png" dateiname = LOGO_FILENAME.format(slot=slot)
img.save(os.path.join(logo_dir, dateiname), "PNG") img.save(os.path.join(site_dir, dateiname), "PNG")
url = f"/media/{site}/{LOGO_DIRNAME}/{dateiname}?v={int(time.time())}" url = f"/media/{site}/{LOGO_URL_PREFIX}/{dateiname}?v={int(time.time())}"
print(f"✅ Generate - Logo hochgeladen: {url}" print(f"✅ Generate - Logo hochgeladen: {url}"
+ (f" (Hinweis: {hinweis})" if hinweis else "")) + (f" (Hinweis: {hinweis})" if hinweis else ""))
return url, hinweis return url, hinweis