Hintergrundbild Willkommensseite

This commit is contained in:
Erik Thiele
2026-06-10 10:32:04 +02:00
parent e860e4b2fe
commit f5f1d52239
6 changed files with 192 additions and 16 deletions

View File

@@ -29,8 +29,11 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
| `GET /admin/<site>/priority` | Priority-Playlist als separate Seite |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) |
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload |
| `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren |
| `POST /admin/<site>/upload-background` | Hintergrundbild für Willkommensseite hochladen |
| `POST /admin/<site>/delete-background` | Hintergrundbild zurücksetzen auf Standard |
| `POST /api/customer` | API-Endpunkt (JSON mit "site"-Feld) |
| `GET /admin/<site>/add-screen?name=<name>` | Neuen Screen anlegen |
| `POST /admin/<site>/delete-screen/<screen>` | Screen + Medien löschen |
@@ -59,8 +62,8 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<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.
- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Medien** (3); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**.
- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Medien.
- Screen-Card-Body hat Tabler-Tabs: **Playlist** (1, aktiv), **Einstellungen** (2), **Aktionen** (3), **Medien** (4); Priority-Seite ebenfalls Tabs **Playlist** und **Medien**.
- Tab-Reihenfolge in Screen-Cards: Playlist → Einstellungen → Aktionen → Medien.
- Player-URL im Screen-Header ist ein klickbarer Link in grauer Farbe.
## Repo Quirks
@@ -68,7 +71,10 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- `media/` and media file extensions are gitignored.
- The app has no configured tests, lint, typecheck, formatter, or CI.
- Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `playlist_item_enabled()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`.
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert.
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.
- `generate_welcome_html(customer_names, logo_urls, background_url=None)` akzeptiert optionalen `background_url`-Parameter.
- `welcome.html` wird in `media/<site>/lobby/` gespeichert.
- `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN.
- `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.

99
app.py
View File

@@ -185,6 +185,44 @@ def prio_redirect(site):
return f"/admin/{site}/priority"
def get_background_url(site):
"""Prüft, ob für einen Standort ein benutzerdefiniertes Hintergrundbild existiert.
Gibt die URL zurück oder None (dann wird das Standard-Wallpaper verwendet)."""
site_dir = os.path.join(MEDIA_DIR, site)
if not os.path.isdir(site_dir):
return None
for f in sorted(os.listdir(site_dir)):
if f.startswith("background."):
ext = os.path.splitext(f)[1].lower()
if ext in (".jpg", ".jpeg", ".png"):
return f"/media/{site}/background/{f}"
return None
def _regenerate_welcome_for_site(site):
"""Regeneriert die Willkommensseite mit aktuellem Hintergrund,
falls zuvor Kundendaten gespeichert wurden."""
config = load_config()
welcome_data = config.get("sites", {}).get(site, {}).get("welcome_data")
if welcome_data:
names = welcome_data.get("names", [])
urls = welcome_data.get("logo_urls", [])
if names:
bg_url = get_background_url(site)
generate_welcome_page.save_welcome_page(names, urls, site=site, background_url=bg_url)
print(f"♻️ Willkommensseite für {site} mit aktualisiertem Hintergrund neu generiert")
# -------------------------------------------------
# Medien ausliefern (statische Dateien)
# -------------------------------------------------
@app.route("/media/<site>/background/<path:filename>")
def media_background(site, filename):
"""Liefert das Hintergrundbild für die Willkommensseite aus."""
path = os.path.join(MEDIA_DIR, site)
return send_from_directory(path, filename)
# -------------------------------------------------
# User & Login (Flask-Login)
# -------------------------------------------------
@@ -285,10 +323,16 @@ def add_customer():
logo_urls[i] = vu
break
html_filename = generate_welcome_page.save_welcome_page(valid_names, valid_urls, site=site)
bg_url = get_background_url(site)
html_filename = generate_welcome_page.save_welcome_page(valid_names, valid_urls, site=site, background_url=bg_url)
if html_filename:
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site):
# 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}
save_config(config)
success = f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt!"
else:
error = "Fehler beim Hinzufügen zur Playliste"
@@ -301,6 +345,7 @@ def add_customer():
config = load_config()
site_list = get_site_list(config)
background_url = get_background_url(site)
return render_template(
"customer.html",
error=error,
@@ -312,7 +357,8 @@ def add_customer():
current_site=site,
version=APP_VERSION,
year=datetime.now().year,
hostname=os.uname().nodename
hostname=os.uname().nodename,
background_url=background_url
)
@@ -711,6 +757,55 @@ def admin_priority(site):
)
# -------------------------------------------------
# Admin: Hintergrundbild für Willkommensseite hochladen
# -------------------------------------------------
@app.route("/admin/<site>/upload-background", methods=["POST"])
@login_required
def upload_background(site):
"""Lädt ein Hintergrundbild für die Willkommensseite hoch (pro Standort).
Gespeichert als media/<site>/background.<ext>."""
file = request.files.get("background")
if not file:
return redirect(f"/willkommen?site={site}")
ext = os.path.splitext(file.filename)[1].lower()
if ext not in UPLOAD_EXTENSIONS:
return "Ungültiger Dateityp", 400
site_dir = os.path.join(MEDIA_DIR, site)
os.makedirs(site_dir, exist_ok=True)
# Alte Hintergrundbilder löschen
for f in os.listdir(site_dir):
if f.startswith("background."):
os.remove(os.path.join(site_dir, f))
filename = f"background{ext}"
file.save(os.path.join(site_dir, filename))
# Willkommensseite mit neuen Hintergrund neu generieren
_regenerate_welcome_for_site(site)
return redirect(f"/willkommen?site={site}")
@app.route("/admin/<site>/delete-background", methods=["POST"])
@login_required
def delete_background(site):
"""Löscht das Hintergrundbild für die Willkommensseite."""
site_dir = os.path.join(MEDIA_DIR, site)
if os.path.isdir(site_dir):
for f in os.listdir(site_dir):
if f.startswith("background."):
os.remove(os.path.join(site_dir, f))
# Willkommensseite mit Standard-Hintergrund neu generieren
_regenerate_welcome_for_site(site)
return redirect(f"/willkommen?site={site}")
# -------------------------------------------------
# Admin: Screen-Einstellungen speichern
# -------------------------------------------------

View File

@@ -15,7 +15,7 @@
"screens": {
"lobby": {
"interval": 10,
"show_images": true,
"show_images": false,
"show_videos": false,
"playlist": [
"welcome.html",
@@ -34,12 +34,12 @@
"enabled": false
}
],
"newsticker_text": "Herzlich Willkommen bei der CANCOM - wir begr\u00fc\u00dfen unsere G\u00e4ste und w\u00fcnschen Ihnen einen angenehmen Tag! !",
"newsticker_enabled": true,
"newsticker_text": "",
"newsticker_enabled": false,
"custom_url": "https://dva-stage.cancom-ds.de/voice-agent",
"custom_url_label": "Welcome Agent",
"custom_url_enabled": false,
"custom_url_target": "redirect"
"custom_url_target": "overlay"
},
"casino": {
"interval": 15,
@@ -70,6 +70,14 @@
"newsticker_text": "Hallo dies ist ein Test f\u00fcr Michael",
"newsticker_enabled": false
}
},
"welcome_data": {
"names": [
"cisco"
],
"logo_urls": [
"https://cdn.brandfetch.io/www.cisco.com/logo?c=1idyd4Tpb2nKaXIIc8T"
]
}
},
"karlsruhe": {
@@ -84,6 +92,14 @@
"newsticker_text": "Herzlich Willkommen bei der CANCOM - wir begr\u00fc\u00dfen unsere G\u00e4ste und w\u00fcnschen Ihnen einen angenehmen Tag!",
"newsticker_enabled": true
}
},
"welcome_data": {
"names": [
"cisco"
],
"logo_urls": [
"https://cdn.brandfetch.io/www.cisco.com/logo?c=1idyd4Tpb2nKaXIIc8T"
]
}
}
},

View File

@@ -97,7 +97,7 @@ def search_customer_logo(customer_name):
# HTML-Generierung
# -------------------------------------------------
def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
def generate_welcome_html(customer_names, logo_urls, site="stuttgart", background_url=None):
"""
Generiert den HTML-Code für die Willkommensseite.
- Links oben: CANCOM-Logo + "meets" + Kundenlogos (280px Breite)
@@ -106,11 +106,14 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
customer_names: Liste der Kundennamen (max. 3)
logo_urls: Liste der Logo-URLs (gleiche Reihenfolge)
site: Standort-Name für die Anzeige
background_url: URLs zum Hintergrundbild (None → Standard-Wallpaper)
"""
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})")
site_display = site.capitalize()
bg_image = background_url if background_url else "/static/wallpaper.png"
cancom_svg = '''<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xcs="https://www.xtool.com/pages/software" version="1.1" preserveAspectRatio="xMinYMin meet" width="61.682394106284505mm" height="9.909010347638684mm" viewBox="136.55880294685778 209.2954948261807 61.682394106284505 9.909010347638684" xcs:version="2.7.22"><style></style><path transform="matrix(0.158038,0,0,0.158038,133.2558,163.322122)" stroke="none" fill="currentColor" data-view-type="laser" d="M125.9,352.1h18.9L119,291.7h-16.2l-25,60.4h18.2l14.6-38.9L125.9,352.1z M323.6,322 c0,8.8-6.1,16.4-15.4,16.4c-9.1,0-15.4-7.6-15.4-16.4s6.1-16.4,15.2-16.4C317,305.6,323.6,313.4,323.6,322 M340.5,322.3 c0-17.4-13.4-31.3-32.3-31.3c-18.7,0-32.3,14.1-32.3,31.3c0,17.4,13.4,31.3,32.3,31.3C326.9,353.6,340.5,339.4,340.5,322.3 M230.4,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.2,0-31.6,14.1-31.6,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C236.5,338.4,230.4,331.4,230.4,322.5 M38,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.4,0-31.8,14.1-31.8,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C44,338.4,38,331.4,38,322.5 M191.8,352.1h14.4v-59.8H190v33.1l-25-33.1h-15.4v60.1h16.2v-34.1L191.8,352.1z M411.2,352.1v-59.8h-17.7l-14.4,23.2l-14.4-23.2 h-17.7v60.1h16.4V318l15.4,23.2h0.3l15.4-23.5v34.6L411.2,352.1L411.2,352.1z" fill-rule="nonzero"></path></svg>'''
logo_html = ""
@@ -138,7 +141,7 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
width: 100vw; height: 100vh; overflow: hidden;
font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;
display: flex; align-items: center; justify-content: center;
background-image: url("/static/wallpaper.png");
background-image: url("{bg_image}");
background-repeat: no-repeat; background-size: cover;
}}
.screen {{
@@ -212,14 +215,15 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
# Speichern + Playlist-Einfügung
# -------------------------------------------------
def save_welcome_page(customer_names, logo_urls, site="stuttgart"):
def save_welcome_page(customer_names, logo_urls, site="stuttgart", background_url=None):
"""Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis.
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, site=site)
html_content = generate_welcome_html(customer_names, logo_urls, site=site, background_url=background_url)
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)

View File

@@ -200,6 +200,11 @@
<i class="ti ti-settings me-1"></i>Einstellungen
</a>
</li>
<li class="nav-item" role="presentation">
<a href="#actions-{{ screen }}" class="nav-link" data-bs-toggle="tab" role="tab">
<i class="ti ti-click me-1"></i>Aktionen
</a>
</li>
<li class="nav-item" role="presentation">
<a href="#media-{{ screen }}" class="nav-link" data-bs-toggle="tab" role="tab">
<i class="ti ti-upload me-1"></i>Medien
@@ -310,7 +315,15 @@
<label class="form-check-label">Videos anzeigen</label>
</div>
<hr class="my-4">
<button type="submit" class="btn btn-primary">
Einstellungen speichern
</button>
</form>
</div>
<!-- ═══ Tab: Aktionen ═══ -->
<div class="tab-pane" id="actions-{{ screen }}" role="tabpanel">
<form action="/admin/{{ current_site }}/update/{{ screen }}" method="post">
<h3>Aktions-Button (Player)</h3>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox"
@@ -335,7 +348,6 @@
<option value="redirect" {% if cfg.custom_url_target|default('overlay') == 'redirect' %}selected{% endif %}>Weiterleitung (gleiches Fenster)</option>
</select>
</div>
<button type="submit" class="btn btn-primary">
Einstellungen speichern
</button>

View File

@@ -155,13 +155,56 @@
</form>
</div>
</div>
<!-- ═══════════════════════════════════════════════════
Karte: Hintergrundbild hochladen (pro Standort)
═══════════════════════════════════════════════════ -->
<div class="card mb-4">
<div class="card-header">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Willkommensseite</div>
<h2 class="page-title">Hintergrund</h2>
</div>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Aktuelles Hintergrundbild</label>
<div class="logo-preview-box" style="min-height:200px; background: #f0f0f0;">
<img src="{{ background_url or '/static/wallpaper.png' }}" alt="Hintergrund" style="max-height:180px; width:100%; object-fit:contain; background:transparent; padding:0;">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Neues Hintergrundbild hochladen</label>
<form method="POST" action="/admin/{{ site }}/upload-background" enctype="multipart/form-data">
<input type="file" class="form-control mb-2" name="background" accept=".jpg,.jpeg,.png" required>
<button type="submit" class="btn btn-primary">
<i class="ti ti-upload me-1"></i>Hochladen
</button>
</form>
{% if background_url %}
<form method="POST" action="/admin/{{ site }}/delete-background" class="mt-2">
<button type="submit" class="btn btn-outline-danger">
<i class="ti ti-trash me-1"></i>Zurücksetzen auf Standard
</button>
</form>
{% endif %}
<p class="text-muted mt-2 mb-0 small">
<i class="ti ti-info-circle me-1"></i>
Wenn kein Hintergrundbild hochgeladen ist, wird das Standard-Wallpaper verwendet.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="app-footer">
{% include "_footer.html" %}
</footer>
</div>
<script>
window.addEventListener("load", () => {