feat: Logo-Upload mit automatischem Freistellen

Findet die automatische Suche kein Logo, kann pro Kunde eine Bilddatei
hochgeladen werden. Ein Upload hat Vorrang vor der Suche.

Freistellen:
- remove_logo_background() entfernt den Hintergrund per Flood Fill von
  den Bildraendern her. Bewusst kein globaler Farbvergleich - sonst
  verschwindet auch Weiss innerhalb des Logos (etwa Text in einer
  Flaeche). Weiche Kante gegen Treppeneffekte.
- Umgesetzt mit Pillow, das ueber qrcode[pil] ohnehin vorhanden ist.
  Bewusst kein ML-Modell wie rembg: der Container laeuft mit read-only
  Root-FS, ein Modell-Cache zur Laufzeit waere dort nicht moeglich.
- Bereits transparente Bilder bleiben unangetastet. Ist der Bildrand
  nicht einfarbig, wird nicht freigestellt und die Oberflaeche weist
  darauf hin, statt ein beschaedigtes Logo zu liefern.
- Checkbox "Hintergrund behalten" schaltet das Freistellen pro Kunde ab.
- Gespeichert wird immer als PNG (JPEG kann keine Transparenz), grosse
  Bilder werden auf 800px verkleinert.

Ablage unter media/<site>/logos/logo_<slot>.png, neue Route
/media/<site>/logos/<datei>. Die URL traegt einen Zeitstempel, damit
Player kein altes Logo aus dem Cache zeigen.

Nebenbei behoben: welcome_data speicherte nur die erfolgreichen Kunden
statt der Felder 1-3. Fiel Kunde 1 aus, rutschte Kunde 2 beim naechsten
Aufruf in Feld 1 und Logo wie Ansprechpartner verschoben sich mit. Jetzt
feldweise - dadurch bleibt auch ein hochgeladenes Logo beim erneuten
Generieren erhalten, solange der Kundenname im selben Feld steht.

Die drei Kundenbloecke im Formular sind zu einer Schleife
zusammengefasst, damit die Upload-Felder nicht dreifach gepflegt werden
muessen.

README, AGENTS.md und Hilfeseite nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-09-02 13:55:40 +02:00
parent 4974ad92df
commit 64db742d31
6 changed files with 272 additions and 49 deletions

82
app.py
View File

@@ -493,6 +493,14 @@ def media_background(site, filename):
return send_from_directory(path, filename)
@app.route("/media/<site>/logos/<path:filename>")
def media_logo(site, filename):
"""Liefert ein manuell hochgeladenes Kundenlogo aus.
Ohne Login erreichbar wie die übrigen Medien - der Player ist nicht angemeldet."""
path = os.path.join(MEDIA_DIR, site, generate_welcome_page.LOGO_DIRNAME)
return send_from_directory(path, filename)
# -------------------------------------------------
# User-Verwaltung (Flask-Login mit users.json)
# -------------------------------------------------
@@ -901,6 +909,7 @@ def add_customer():
customer_names = ["", "", ""]
contacts = ["", "", ""]
valid_until = (request.form.get("valid_until") or "").strip()
logo_hinweise = []
site = request.args.get("site") or request.form.get("site") or ""
# Fallback auf ersten für den User zugänglichen Standort
@@ -934,31 +943,52 @@ def add_customer():
error = "Kein Standort ausgewählt"
else:
try:
found_logos = []
for name in filled_names:
logo = generate_welcome_page.search_customer_logo(name)
found_logos.append(logo)
# Logo je Kunde bestimmen: ein hochgeladenes Logo hat Vorrang vor
# der automatischen Suche - der Upload ist eine bewusste Entscheidung.
# Zuletzt verwendete Logos, um einen früheren Upload nicht bei jedem
# erneuten Generieren zu verlieren
frueher = load_config().get("sites", {}).get(site, {}).get("welcome_data", {})
frueher_namen = frueher.get("names", [])
frueher_urls = frueher.get("logo_urls", [])
logo_prefix = f"/media/{site}/{generate_welcome_page.LOGO_DIRNAME}/"
valid = [(n, l) for n, l in zip(filled_names, found_logos) if l]
found_logos = [None, None, None]
for i, name in enumerate(customer_names):
if not name:
continue
datei = request.files.get(f"logo_file_{i + 1}")
if datei and datei.filename:
url, hinweis = generate_welcome_page.save_uploaded_logo(
datei, site, i + 1,
freistellen=(f"logo_keep_bg_{i + 1}" not in request.form))
found_logos[i] = url
if hinweis:
logo_hinweise.append(f"{name}: {hinweis}")
elif not url:
logo_hinweise.append(f"{name}: Logo konnte nicht übernommen werden")
continue
# Kein neuer Upload: ein zuvor hochgeladenes Logo weiterverwenden,
# solange der Kundenname in diesem Feld derselbe geblieben ist
alt_url = frueher_urls[i] if i < len(frueher_urls) else None
alt_name = frueher_namen[i] if i < len(frueher_namen) else None
if alt_url and alt_name == name and str(alt_url).startswith(logo_prefix):
found_logos[i] = alt_url
continue
found_logos[i] = generate_welcome_page.search_customer_logo(name)
logo_urls = list(found_logos)
valid = [(customer_names[i], found_logos[i], contacts[i])
for i in range(len(customer_names))
if customer_names[i] and found_logos[i]]
if not valid:
error = "Kein Logo konnte gefunden werden"
error = "Kein Logo gefunden – bitte für die betroffenen Kunden ein Logo hochladen"
else:
valid_names = [n for n, l in valid]
valid_urls = [l for n, l in valid]
valid_contacts = []
for name in valid_names:
for i, cn in enumerate(customer_names):
if cn == name:
valid_contacts.append(contacts[i] if i < len(contacts) else "")
break
else:
valid_contacts.append("")
for i, name in enumerate(customer_names):
for j, (vn, vu) in enumerate(zip(valid_names, valid_urls)):
if vn == name:
logo_urls[i] = vu
break
valid_names = [n for n, u, k in valid]
valid_urls = [u for n, u, k in valid]
valid_contacts = [k for n, u, k in valid]
bg_url = get_background_url(site)
targets = generate_welcome_page.welcome_target_screens(load_config(), site)
@@ -978,7 +1008,14 @@ def add_customer():
# 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}
# Feldweise speichern (Slot 1-3), nicht nur die erfolgreichen -
# sonst rutscht Kunde 2 beim naechsten Aufruf in Feld 1 und die
# Zuordnung von Logo und Ansprechpartner verschiebt sich mit.
site_cfg["welcome_data"] = {
"names": list(customer_names),
"logo_urls": list(found_logos),
"contacts": list(contacts)
}
save_config(config)
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 "
@@ -1019,6 +1056,7 @@ def add_customer():
customer_names=customer_names,
contacts=contacts,
welcome_screens=generate_welcome_page.welcome_target_screens(config, site) if site else [],
logo_hinweise=logo_hinweise,
site=site,
site_list=site_list,
current_site=site,