Compare commits

...

3 Commits

Author SHA1 Message Date
Erik Thiele
317f2a6eb2 Doku: README, AGENTS und Hilfeseite aktualisiert
- README: Willkommensseite-Sektion mit Ansprechpartner/Markdown/Layout
- AGENTS: generate_welcome_html Signatur + Verhalten dokumentiert
- help.html: Ansprechpartner-Felder, Markdown-Hilfe, Portrait-Layout
2026-07-04 10:34:11 +02:00
Erik Thiele
a0753ea4c3 Willkommensseite: Kontaktinfo linksbündig mit Aufzählungen, Portrait-Modus verbessert
- md_to_html: Listen-Support (- /*) und Zeilenumbrüche erhalten
- Kein Firmenname mehr unter den Logos (customer-name entfernt)
- text-align: left + align-items: stretch für linksbündige Texte
- Feste Logo-Höhe 120px für gleiche Text-Startposition
- Portrait-Modus: Text links vom Logo (via order), gap 40px
- Schrift 1.3rem, margin-top 24px, Listeneinzug entfernt
- Reset-Button im Willkommensseite-Formular
- Markdown-Hilfetext aktualisiert
2026-07-04 10:31:02 +02:00
Erik Thiele
c1a043fc75 Ansprechpartner-Felder für Willkommensseite mit Markdown-Support 2026-07-04 09:14:38 +02:00
7 changed files with 169 additions and 20 deletions

View File

@@ -112,7 +112,8 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`. - New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`.
- Priority playlist (`config.priority`) is global and affects all sites/screens. - 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). - Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column).
- `generate_welcome_html(customer_names, logo_urls)` takes lists for up to 3 customers; logos have equal width (280px) with `max-height: 180px`. - `generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None)` takes lists for up to 3 customers; logos have equal width (280px) with fixed height 120px, text left-aligned below each logo.
- Contact info supports Markdown: `**bold**`, `*italic*`, `` `code` ``, `- list items`, and line breaks (preserved as `<br>`).
- `customer_names` are preserved in form fields after POST (via `value`-Attribute). - `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")`. - 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). - Gemeinsame HTML-Bausteine: `_header.html`, `_footer.html`, `_styles.html` (CSS-Variablen `--ccm-*`, Dark Mode, Card-Border-Radius 1rem).
@@ -148,7 +149,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`. - Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert. - 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. - `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. - `generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None)` akzeptiert optionale `contacts`-, `site`- und `background_url`-Parameter.
- `welcome.html` wird in `media/<site>/lobby/` gespeichert. - `welcome.html` wird in `media/<site>/lobby/` gespeichert.
- `search_customer_logo` in `generate_welcome_page.py` nutzt OpenAI GPT-4 + Brandfetch CDN. - `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`. - `admin_priority` rendert `priority.html` mit `site_list`, `current_site`, `priority_files`, `server_url`.

View File

@@ -279,6 +279,17 @@ http://localhost:5005/willkommen?site=stuttgart
Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `welcome.html` wird in der Lobby-Playlist vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf `static/wallpaper.png`). Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `welcome.html` wird in der Lobby-Playlist vorne eingefügt. Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Fallback auf `static/wallpaper.png`).
**Ansprechpartner:** Pro Kunde können Kontaktdaten eingegeben werden (Markdown-Format).
- **Fett**: `**Text**`
- *Kursiv*: `*Text*`
- `Code`: `` `Code` ``
- Aufzählungen: `- Punkt` oder `* Punkt`
- Zeilenumbrüche werden als `<br>` übernommen
**Layout:**
- Landscape: Logos nebeneinander (280px breit, 120px hoch), Texte linksbündig darunter
- Portrait: Texte links vom Logo, per `order`-CSS gesteuert
--- ---
### Globale Konfiguration (config.json) ### Globale Konfiguration (config.json)

38
app.py
View File

@@ -30,7 +30,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from flask import ( from flask import (
Flask, jsonify, render_template, Flask, jsonify, render_template,
send_from_directory, redirect, send_from_directory, redirect,
request, abort, flash, session request, abort, flash, session, url_for
) )
from flask_login import ( from flask_login import (
LoginManager, login_user, current_user, LoginManager, login_user, current_user,
@@ -685,6 +685,7 @@ def add_customer():
success = None success = None
logo_urls = [None, None, None] logo_urls = [None, None, None]
customer_names = ["", "", ""] customer_names = ["", "", ""]
contacts = ["", "", ""]
site = request.args.get("site") or request.form.get("site") or "" site = request.args.get("site") or request.form.get("site") or ""
# Fallback auf ersten für den User zugänglichen Standort # Fallback auf ersten für den User zugänglichen Standort
@@ -703,6 +704,11 @@ def add_customer():
request.form.get("customer_name_2", "").strip(), request.form.get("customer_name_2", "").strip(),
request.form.get("customer_name_3", "").strip(), request.form.get("customer_name_3", "").strip(),
] ]
contacts = [
request.form.get("contact_1", "").strip(),
request.form.get("contact_2", "").strip(),
request.form.get("contact_3", "").strip(),
]
filled_names = [n for n in customer_names if n] filled_names = [n for n in customer_names if n]
print(f"🔍 APP.PY - Suche nach Kunden: {filled_names} (site: {site})") print(f"🔍 APP.PY - Suche nach Kunden: {filled_names} (site: {site})")
@@ -725,6 +731,14 @@ def add_customer():
else: else:
valid_names = [n for n, l in valid] valid_names = [n for n, l in valid]
valid_urls = [l 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 i, name in enumerate(customer_names):
for j, (vn, vu) in enumerate(zip(valid_names, valid_urls)): for j, (vn, vu) in enumerate(zip(valid_names, valid_urls)):
if vn == name: if vn == name:
@@ -732,14 +746,14 @@ def add_customer():
break break
bg_url = get_background_url(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) html_filename = generate_welcome_page.save_welcome_page(valid_names, valid_urls, contacts=valid_contacts, site=site, background_url=bg_url)
if html_filename: if html_filename:
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site): if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site):
# Letzte Kundendaten für Background-Update speichern # Letzte Kundendaten für Background-Update speichern
config = load_config() config = load_config()
site_cfg = config.setdefault("sites", {}).setdefault(site, {}) site_cfg = config.setdefault("sites", {}).setdefault(site, {})
site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls} site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls, "contacts": valid_contacts}
save_config(config) save_config(config)
add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt", current_user.email) 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!" success = f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt!"
@@ -755,12 +769,26 @@ def add_customer():
config = load_config() config = load_config()
site_list = get_site_list(config) site_list = get_site_list(config)
background_url = get_background_url(site) background_url = get_background_url(site)
if request.args.get("reset") and site:
site_cfg = config.setdefault("sites", {}).setdefault(site, {})
site_cfg.pop("welcome_data", None)
save_config(config)
return redirect(url_for("add_customer", site=site))
welcome_data = config.get("sites", {}).get(site, {}).get("welcome_data", {})
saved_contacts = welcome_data.get("contacts", ["", "", ""])
if request.method != "POST":
contacts = saved_contacts
customer_names = welcome_data.get("names", ["", "", ""])
logo_urls = welcome_data.get("logo_urls", [None, None, None])
return render_template( return render_template(
"customer.html", "customer.html",
error=error, error=error,
success=success, success=success,
logo_urls=logo_urls, logo_urls=logo_urls,
customer_names=customer_names, customer_names=customer_names,
contacts=contacts,
site=site, site=site,
site_list=site_list, site_list=site_list,
current_site=site, current_site=site,
@@ -804,7 +832,9 @@ def api_add_customer():
if not logo_url: if not logo_url:
return jsonify({"error": "Logo not found"}), 400 return jsonify({"error": "Logo not found"}), 400
html_filename = generate_welcome_page.save_welcome_page(customer_name, logo_url, site=site) contact = data.get("contact", "").strip()
html_filename = generate_welcome_page.save_welcome_page([customer_name], [logo_url], contacts=[contact], site=site)
if not html_filename: if not html_filename:
return jsonify({"error": "Failed to create welcome page"}), 500 return jsonify({"error": "Failed to create welcome page"}), 500

View File

@@ -97,14 +97,47 @@ def search_customer_logo(customer_name):
# HTML-Generierung # HTML-Generierung
# ------------------------------------------------- # -------------------------------------------------
def generate_welcome_html(customer_names, logo_urls, site="stuttgart", background_url=None): def md_to_html(text):
"""Wandelt einfaches Markdown in HTML um (fett, kursiv, code, zeilenumbrüche, aufzählungen)."""
import re
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
text = re.sub(r'`(.+?)`', r'<code>\1</code>', text)
lines = text.split('\n')
parts = []
in_list = False
for line in lines:
stripped = line.strip()
is_li = stripped.startswith('- ') or stripped.startswith('* ')
if is_li:
if not in_list:
parts.append('<ul>')
in_list = True
parts.append(f'<li>{stripped[2:]}</li>')
else:
if in_list:
parts.append('</ul>')
in_list = False
if stripped:
parts.append(stripped)
parts.append('<br>')
if in_list:
parts.append('</ul>')
if parts and parts[-1] == '<br>':
parts.pop()
return ''.join(parts)
def generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None):
""" """
Generiert den HTML-Code für die Willkommensseite. Generiert den HTML-Code für die Willkommensseite.
- Links oben: CANCOM-Logo + "meets" + Kundenlogos (280px Breite) - Links oben: CANCOM-Logo + "meets" + Kundenlogos
- Rechts unten: "Herzlich Willkommen in unserer Niederlassung {Standort}" - Unter jedem Logo: Ansprechpartner-Kontaktdaten (Landscape)
- Portrait: Kontaktdaten links neben den Logos
customer_names: Liste der Kundennamen (max. 3) customer_names: Liste der Kundennamen (max. 3)
logo_urls: Liste der Logo-URLs (gleiche Reihenfolge) logo_urls: Liste der Logo-URLs (gleiche Reihenfolge)
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
site: Standort-Name für die Anzeige site: Standort-Name für die Anzeige
background_url: URLs zum Hintergrundbild (None → Standard-Wallpaper) background_url: URLs zum Hintergrundbild (None → Standard-Wallpaper)
""" """
@@ -113,15 +146,24 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart", backgroun
print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})") print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})")
site_display = site.capitalize() site_display = site.capitalize()
if contacts is None:
contacts = [""] * len(customer_names)
bg_image = background_url if background_url else "/static/wallpaper.png" 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>''' 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 = "" logo_html = ""
for i, (name, url) in enumerate(zip(customer_names, logo_urls)): for i, (name, url) in enumerate(zip(customer_names, logo_urls)):
if url: if url:
contact_html = ""
if i < len(contacts) and contacts[i]:
contact_html = f'''<div class="contact-info">{md_to_html(contacts[i])}</div>'''
logo_html += f''' logo_html += f'''
<div class="customer-logo-item"> <div class="customer-logo-item">
<img src="{url}" alt="{name}" class="customer-logo" crossorigin="anonymous"> <img src="{url}" alt="{name}" class="customer-logo" crossorigin="anonymous">
<div class="customer-meta">
{contact_html}
</div>
</div>''' </div>'''
html_content = f"""<!DOCTYPE html> html_content = f"""<!DOCTYPE html>
@@ -165,29 +207,59 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart", backgroun
}} }}
.customer-logos {{ .customer-logos {{
display: flex; flex-direction: row; display: flex; flex-direction: row;
align-items: center; justify-content: flex-end; align-items: flex-start; justify-content: flex-end;
gap: 48px; max-width: 100%; gap: 48px; max-width: 100%;
}} }}
.customer-logo-item {{ .customer-logo-item {{
flex: 0 0 280px; flex: 0 0 280px;
display: flex; align-items: center; justify-content: center; display: flex; flex-direction: column;
min-height: 100px; align-items: stretch; justify-content: flex-start;
}}
.customer-meta {{
width: 100%; text-align: left;
}} }}
.customer-logo {{ .customer-logo {{
width: 100%; height: auto; width: 100%; height: 120px;
max-height: 180px;
object-fit: contain; object-fit: contain;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15)); filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
display: block;
}}
.contact-info {{
font-size: 1.3rem; color: #333; line-height: 1.3;
margin-top: 24px; text-align: left;
}}
.contact-info strong {{ color: #000; }}
.contact-info code {{
font-size: 0.8rem; padding: 1px 4px;
background: rgba(0,0,0,0.06); border-radius: 3px;
}}
.contact-info ul {{
margin: 0; padding-left: 0;
list-style-position: inside;
}}
.contact-info li {{
margin: 0; padding: 0;
}} }}
@media (orientation: portrait) {{ @media (orientation: portrait) {{
.customer-logos {{ .customer-logos {{
flex-direction: column; gap: 24px; flex-direction: column; gap: 20px;
}} }}
.customer-logo-item {{ .customer-logo-item {{
flex: 0 0 200px; flex: 0 0 auto;
flex-direction: row; gap: 40px;
width: 100%; max-width: 600px;
}} }}
.customer-logo {{ .customer-logo {{
max-height: 120px; max-height: 100px; width: 180px; flex-shrink: 0;
order: 2;
}}
.contact-info {{
text-align: left;
}}
.customer-meta {{
display: flex; flex-direction: column;
align-items: flex-start; flex: 1;
order: 1;
}} }}
}} }}
.content {{ .content {{
@@ -226,15 +298,16 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart", backgroun
# Speichern + Playlist-Einfügung # Speichern + Playlist-Einfügung
# ------------------------------------------------- # -------------------------------------------------
def save_welcome_page(customer_names, logo_urls, site="stuttgart", background_url=None): 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. """Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis.
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard). background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard).
Gibt den Dateinamen zurück oder None bei Fehler.""" Gibt den Dateinamen zurück oder None bei Fehler."""
try: try:
welcome_dir = os.path.join(MEDIA_DIR, site, "lobby") welcome_dir = os.path.join(MEDIA_DIR, site, "lobby")
os.makedirs(welcome_dir, exist_ok=True) os.makedirs(welcome_dir, exist_ok=True)
filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME) filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME)
html_content = generate_welcome_html(customer_names, logo_urls, site=site, background_url=background_url) html_content = generate_welcome_html(customer_names, logo_urls, contacts=contacts, site=site, background_url=background_url)
with open(filepath, "w", encoding="utf-8") as f: with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content) f.write(html_content)

View File

@@ -1,4 +1,10 @@
[ [
{
"timestamp": "2026-07-02 12:25:24",
"action": "user_login",
"detail": "User 'admin' angemeldet",
"user": "admin"
},
{ {
"timestamp": "2026-06-28 16:04:39", "timestamp": "2026-06-28 16:04:39",
"action": "user_login", "action": "user_login",

View File

@@ -129,29 +129,42 @@
<input type="text" class="form-control" id="customer_name_1" <input type="text" class="form-control" id="customer_name_1"
name="customer_name_1" placeholder="z. B. Beispiel GmbH" name="customer_name_1" placeholder="z. B. Beispiel GmbH"
value="{{ customer_names[0] if customer_names else '' }}"> value="{{ customer_names[0] if customer_names else '' }}">
<label class="form-label mt-2" for="contact_1">Ansprechpartner (optional)</label>
<textarea class="form-control" id="contact_1" name="contact_1" rows="4"
placeholder="**Max Mustermann**&#10;+49 123 4567890&#10;*E-Mail*: max@beispiel.de">{{ contacts[0] if contacts else '' }}</textarea>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label" for="customer_name_2">Kunde 2 (optional)</label> <label class="form-label" for="customer_name_2">Kunde 2 (optional)</label>
<input type="text" class="form-control" id="customer_name_2" <input type="text" class="form-control" id="customer_name_2"
name="customer_name_2" placeholder="z. B. Muster AG" name="customer_name_2" placeholder="z. B. Muster AG"
value="{{ customer_names[1] if customer_names else '' }}"> value="{{ customer_names[1] if customer_names else '' }}">
<label class="form-label mt-2" for="contact_2">Ansprechpartner (optional)</label>
<textarea class="form-control" id="contact_2" name="contact_2" rows="4"
placeholder="**Maria Schmidt**&#10;+49 456 7890123&#10;*E-Mail*: maria@muster.de">{{ contacts[1] if contacts else '' }}</textarea>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label" for="customer_name_3">Kunde 3 (optional)</label> <label class="form-label" for="customer_name_3">Kunde 3 (optional)</label>
<input type="text" class="form-control" id="customer_name_3" <input type="text" class="form-control" id="customer_name_3"
name="customer_name_3" placeholder="z. B. Firma GmbH" name="customer_name_3" placeholder="z. B. Firma GmbH"
value="{{ customer_names[2] if customer_names else '' }}"> value="{{ customer_names[2] if customer_names else '' }}">
<label class="form-label mt-2" for="contact_3">Ansprechpartner (optional)</label>
<textarea class="form-control" id="contact_3" name="contact_3" rows="4"
placeholder="**Sabine Weber**&#10;+49 789 0123456&#10;*E-Mail*: sabine@firma.de">{{ contacts[2] if contacts else '' }}</textarea>
</div> </div>
</div> </div>
<div class="mb-3 text-muted"> <div class="mb-3 text-muted">
<p class="mb-1">Das System sucht automatisch nach den Kundenlogos</p> <p class="mb-1">Das System sucht automatisch nach den Kundenlogos</p>
<p class="mb-0">und erstellt eine angepasste Willkommensseite für die Lobby-Playlist.</p> <p class="mb-0">und erstellt eine angepasste Willkommensseite für die Lobby-Playlist.</p>
<p class="mb-0 small"><i class="ti ti-markdown me-1"></i>Ansprechpartner unterstützen <strong>fett</strong>, <em>kursiv</em>, <code>Code</code>, Aufzählungen (<code>- </code>) sowie Zeilenumbrüche.</p>
</div> </div>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="ti ti-file-plus me-1"></i>Logos &amp; Willkommensseite generieren <i class="ti ti-file-plus me-1"></i>Logos &amp; Willkommensseite generieren
</button> </button>
<a href="?reset=1&amp;site={{ site }}" class="btn btn-outline-secondary ms-2">
<i class="ti ti-trash me-1"></i>Zurücksetzen
</a>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -193,7 +193,22 @@
<li>Aufruf über <code>/willkommen?site=&lt;standort&gt;</code> (oder Menüpunkt)</li> <li>Aufruf über <code>/willkommen?site=&lt;standort&gt;</code> (oder Menüpunkt)</li>
<li>Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Lobby-Playlist gesetzt</li> <li>Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Lobby-Playlist gesetzt</li>
<li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li> <li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li>
<li>Im Hochformat (Portrait) werden die Logos untereinander angezeigt</li> <li>Pro Kunde können <strong>Ansprechpartner</strong> eingegeben werden (Markdown-Format):
<ul>
<li><strong>Fett</strong>: <code>**Text**</code></li>
<li><em>Kursiv</em>: <code>*Text*</code></li>
<li><code>Code</code>: <code>`Code`</code></li>
<li>Aufzählungen: <code>- Punkt</code> oder <code>* Punkt</code></li>
<li>Zeilenumbrüche werden automatisch übernommen</li>
</ul>
</li>
<li><strong>Layout:</strong>
<ul>
<li>Landscape: Logos nebeneinander, Texte linksbündig darunter</li>
<li>Portrait: Texte <em>links</em> vom Logo, Abstand 40px</li>
</ul>
</li>
<li>Reset-Button setzt das Formular und die gespeicherten Daten zurück</li>
</ul> </ul>
</div> </div>
</div> </div>