diff --git a/.gitignore b/.gitignore index b96917b..7c5033a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ firmware/ dist/ build/ .vscode/ +static/custom/ +busylight.db-shm +busylight.db-wal +Status.csv diff --git a/README.md b/README.md index 0a37d80..85ec481 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Jedes WLED-Geraet verfuegt ueber zwei Segmente (oben/unten), die jeweils einem B - **Modulare Zeiterfassung** per CSV oder REST API - kompatibel mit jeder Zeiterfassung - **Webbasierte Azure-Authentifizierung** direkt im Browser (kein Terminal noetig) - **Zentrale (Self-Service)** - wechselnde Besetzung des Empfangs-Busylights per Durchwahl, ohne Admin-Passwort +- **Eigenes Logo** pro Kunde hochladbar, ueberlebt Updates - **Einstellungen** fuer Azure, Polling-Intervall und Zeiterfassung - **Protokoll** der letzten Statusaenderungen @@ -175,6 +176,20 @@ GET/POST http://SERVER:8081/{durchwahl}/callend - Anruf beendet (WLED -> zu - Waehrend eines aktiven Telefonats ueberschreibt das Teams-Polling die Farbe nicht - Die Telefon-Farbe (`phone_active`) ist unter **Farb-Regeln** konfigurierbar +## Logo + +Unter **Einstellungen > Logo** kann ein kundenspezifisches Logo hochgeladen werden. Es +erscheint in der Seitenleiste, auf der Login-Seite und auf der Zentrale-Seite. + +- Formate: PNG, JPG, SVG, WEBP, GIF, maximal 2 MB +- Dargestellt wird eine Hoehe von etwa 40 Pixeln. Die Seitenleiste ist dunkel, ein Logo + mit heller Schrift oder transparentem Hintergrund wirkt dort am besten - die + Einstellungsseite zeigt eine Vorschau auf hellem und dunklem Grund. +- Die Datei liegt unter `static/custom/` und ist per `.gitignore` von der Versionierung + ausgenommen. **Ein `git pull` ueberschreibt das Kundenlogo nicht.** +- Mit **Zurueck zum Standardlogo** wird die Datei entfernt und wieder + `static/logo.png` aus dem Repository verwendet. + ## Zentrale (Self-Service) Ein Busylight im Empfangsbereich wird wechselnd besetzt. Damit die Mitarbeiter das diff --git a/app.py b/app.py index c839c09..3728f46 100644 --- a/app.py +++ b/app.py @@ -45,6 +45,7 @@ async def lifespan(application: FastAPI): os.makedirs(BACKUP_DIR, exist_ok=True) os.makedirs(WLED_BACKUP_DIR, exist_ok=True) os.makedirs(FIRMWARE_DIR, exist_ok=True) + os.makedirs(CUSTOM_DIR, exist_ok=True) # Start background polling interval = int(database.get_setting("poll_interval", "15")) @@ -65,6 +66,11 @@ async def lifespan(application: FastAPI): BACKUP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backups") WLED_BACKUP_DIR = os.path.join(BACKUP_DIR, "wled") FIRMWARE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "firmware") +# Kundenspezifische Dateien (Logo). Liegt bewusst ausserhalb der Versionierung, +# damit ein git pull das Kundenlogo nicht ueberschreibt. +CUSTOM_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "custom") +LOGO_EXTENSIONS = (".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif") +LOGO_MAX_BYTES = 2 * 1024 * 1024 def auto_backup_db(): @@ -118,6 +124,27 @@ templates = Jinja2Templates(directory="templates") SESSION_TOKEN = secrets.token_hex(32) # Regenerated on restart +DEFAULT_LOGO = "/static/logo.png" + + +def _logo_url(): + """URL des anzuzeigenden Logos - kundenspezifisch, sonst das mitgelieferte. + + Als Jinja-Global registriert, damit auch Seiten ohne _ctx() (Login, + Zentrale) dasselbe Logo zeigen. + """ + name = database.get_setting("logo_file", "") + if name: + # Kein Pfad, nur Dateiname - der Wert kommt aus der eigenen DB, aber + # ein Traversal soll auch dann nicht moeglich sein. + name = os.path.basename(name) + if os.path.exists(os.path.join(CUSTOM_DIR, name)): + return f"/static/custom/{name}" + return DEFAULT_LOGO + + +templates.env.globals["logo_url"] = _logo_url + def _hash_password(pw): return hashlib.sha256(pw.encode()).hexdigest() @@ -984,6 +1011,57 @@ async def update_settings(request: Request): return RedirectResponse("/settings?saved=1", status_code=303) +@app.post("/api/settings/logo") +async def upload_logo(request: Request, logo: UploadFile = File(...)): + """Kundenspezifisches Logo hochladen.""" + redir = _require_login(request) + if redir: return redir + + ext = os.path.splitext(logo.filename or "")[1].lower() + if ext not in LOGO_EXTENSIONS: + return RedirectResponse("/settings?logo_error=format", status_code=303) + + content = await logo.read() + if not content: + return RedirectResponse("/settings?logo_error=empty", status_code=303) + if len(content) > LOGO_MAX_BYTES: + return RedirectResponse("/settings?logo_error=size", status_code=303) + + os.makedirs(CUSTOM_DIR, exist_ok=True) + # Nur ein Logo behalten, sonst bleiben alte Endungen liegen + for old in os.listdir(CUSTOM_DIR): + if old.startswith("logo."): + try: + os.remove(os.path.join(CUSTOM_DIR, old)) + except OSError: + pass + + filename = f"logo{ext}" + with open(os.path.join(CUSTOM_DIR, filename), "wb") as f: + f.write(content) + database.set_setting("logo_file", filename) + logger.info("Kundenlogo hochgeladen: %s (%d KB)", filename, len(content) // 1024) + return RedirectResponse("/settings?logo_saved=1", status_code=303) + + +@app.post("/api/settings/logo/reset") +async def reset_logo(request: Request): + """Zurueck auf das mitgelieferte Logo.""" + redir = _require_login(request) + if redir: return redir + + if os.path.isdir(CUSTOM_DIR): + for old in os.listdir(CUSTOM_DIR): + if old.startswith("logo."): + try: + os.remove(os.path.join(CUSTOM_DIR, old)) + except OSError: + pass + database.set_setting("logo_file", "") + logger.info("Kundenlogo entfernt, Standardlogo aktiv") + return RedirectResponse("/settings?logo_reset=1", status_code=303) + + # ── Log ──────────────────────────────────────────────────────────────────── @app.get("/log", response_class=HTMLResponse) diff --git a/database.py b/database.py index ff8db14..30b2277 100644 --- a/database.py +++ b/database.py @@ -150,6 +150,7 @@ def init_db(): "attendance_status_homeoffice": "~~2~~", "selfservice_device_id": "", "selfservice_segment": "0", + "logo_file": "", } for key, value in defaults.items(): conn.execute( diff --git a/templates/base.html b/templates/base.html index 9852c2d..3aeca8c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@