From e18414ab79aaf06c966ecc2afed53fd8dec7738d Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Sat, 20 Jun 2026 16:40:02 +0200 Subject: [PATCH] Admin Dashboard und Hilfeseite --- README.md | 18 ++- app.py | 210 ++++++++++++++++++++++++++++++-- history.json | 200 ++++++++++++++++++++++++++++++ templates/_header.html | 4 +- templates/_styles.html | 17 +++ templates/admin.html | 18 --- templates/admin_dashboard.html | 206 +++++++++++++++++++++++++++++++ templates/help.html | 216 +++++++++++++++++++++++++++++++++ templates/user_create.html | 20 +++ templates/user_edit.html | 20 +++ templates/user_list.html | 8 +- users.json | 24 ++++ 12 files changed, 929 insertions(+), 32 deletions(-) create mode 100644 history.json create mode 100644 templates/admin_dashboard.html create mode 100644 templates/help.html diff --git a/README.md b/README.md index 83d6796..50ded9f 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens. - **Stay-on-First**: Screen kann auf erstem Playlist-Element stehen bleiben (kein automatischer Refresh) - **User-Verwaltung**: Mehrere User mit Rollen (Admin / Superuser / User), E-Mail als Login, Passwort-Hashing (scrypt), Berechtigungen pro Standort - **Passwort-Workflow**: First-Login-Änderung, Admin-Reset mit temporärem Passwort, Notfall-Login bei fehlendem Hash +- **Admin Dashboard**: `/admin/dashboard` – Statistiken (Sites, Screens, User, Admins, Superuser) mit Sparkline-Charts + Trendanzeige, Aktivitätsverlauf +- **Aktivitätsverlauf**: Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden in `history.json` protokolliert und im Dashboard angezeigt - Tab-basierte Admin-UI pro Screen: Playlist, Einstellungen, Aktionen, Digital Voice Agent, Medien (Tabler Tabs) - Priority-Seite ebenfalls mit Tabs: Playlist und Medien - Dark Mode (localStorage-persistiert) @@ -38,6 +40,7 @@ Flask App (Server) ├── Priority-Seite /admin//priority ├── config.json ├── users.json +├── history.json ├── media/ │ ├── / │ │ ├── / @@ -63,6 +66,7 @@ signage/ ├── generate_welcome_page.py # Logo-Suche + Willkommensseite-Generierung ├── config.json # Persistente Konfiguration ├── users.json # User-Datenbank (gehashte Passwörter) +├── history.json # Aktivitätsverlauf (Dashboard) ├── media/ │ ├── / │ │ ├── lobby/ @@ -71,6 +75,7 @@ signage/ │ └── priority/ ├── templates/ │ ├── admin.html # Admin-Dashboard (Übersicht) +│ ├── admin_dashboard.html # Admin Dashboard (Statistiken + Verlauf) │ ├── priority.html # Priority-Playlist (eigene Seite) │ ├── customer.html # Willkommensseite-Formular │ ├── player.html # Player-Ansicht @@ -129,6 +134,7 @@ docker compose up -d | `GET /login` | Admin-Login | | `GET /logout` | Ausloggen | | `GET /admin` | Redirect zum ersten konfigurierten Standort | +| `GET /admin/dashboard` | Admin Dashboard (Statistiken + Verlauf) | | `GET /admin/` | Admin-Dashboard für einen Standort | | `GET /admin//priority` | Priority-Playlist (separate Seite) | | `POST /admin//update/` | Allgemeine Screen-Einstellungen speichern (Intervall, Newsticker, Bilder/Videos, stay_on_first) | @@ -160,7 +166,7 @@ docker compose up -d | `GET /media/priority/` | Priority-Medien (global) | | `GET /media//background/` | Hintergrundbild der Willkommensseite | -### Admin-Portal +### Admin Portal ``` http://localhost:5005/admin/ @@ -177,6 +183,16 @@ http://localhost:5005/admin/ - Standorte anlegen & löschen - **Userverwaltung**: User anlegen, bearbeiten, löschen, Passwort-Reset (temporäres Passwort wird angezeigt) +### Admin Dashboard + +``` +http://localhost:5005/admin/dashboard +``` + +- 5 Statistik-Cards (Standorte, Screens, User, Admins, Superuser) mit ApexCharts-Sparkline + Trendanzeige +- Aktivitätsverlauf (History) als Liste mit Zeitstempel, Aktion und User +- Alle Erstell-/Lösch-/Änderungsaktionen sowie Login/Logout werden protokolliert + ### Player-URL ``` diff --git a/app.py b/app.py index 74bdcf1..52f2172 100755 --- a/app.py +++ b/app.py @@ -38,6 +38,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/Screen CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration USERS_FILE = os.path.join(BASE_DIR, "users.json") # User-Datenbank +HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf APP_VERSION = "6.0.1" UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} @@ -72,6 +73,40 @@ def get_site_list(cfg): return sorted(cfg.get("sites", {}).keys()) +def add_history_entry(action, detail, user_email): + """Hängt einen Eintrag in history.json an (Aktivitätsverlauf für das Admin-Dashboard).""" + entry = { + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "action": action, + "detail": detail, + "user": user_email + } + history = [] + if os.path.exists(HISTORY_FILE): + try: + with open(HISTORY_FILE) as f: + history = json.load(f) + except (json.JSONDecodeError, OSError): + history = [] + history.insert(0, entry) # Neueste zuerst + # Maximal 200 Einträge behalten + history = history[:200] + with open(HISTORY_FILE, "w") as f: + json.dump(history, f, indent=2) + + +def get_history(limit=50): + """Gibt die letzten history-Einträge zurück.""" + if not os.path.exists(HISTORY_FILE): + return [] + try: + with open(HISTORY_FILE) as f: + history = json.load(f) + return history[:limit] + except (json.JSONDecodeError, OSError): + return [] + + def get_screen_config(cfg, site, screen): """Holt die Konfiguration für einen bestimmten Screen. Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen).""" @@ -231,15 +266,21 @@ def media_background(site, filename): # User-Verwaltung (Flask-Login mit users.json) # ------------------------------------------------- class User(UserMixin): - """User mit E-Mail (id), Rolle und Standort-Berechtigungen.""" + """User mit E-Mail (id), Rolle, Standort-Berechtigungen und Profilfeldern.""" - def __init__(self, email, password_hash="", role="user", sites=None, must_change_password=False): + def __init__(self, email, password_hash="", role="user", sites=None, + must_change_password=False, first_name="", last_name="", + department="", notes=""): self.email = email self.id = email self.password_hash = password_hash self.role = role self.sites = sites or [] self.must_change_password = must_change_password + self.first_name = first_name + self.last_name = last_name + self.department = department + self.notes = notes @property def is_admin(self): @@ -289,7 +330,11 @@ def init_user_db(): "password_hash": generate_password_hash(password), "role": "admin", "sites": [], - "must_change_password": False + "must_change_password": False, + "first_name": "", + "last_name": "", + "department": "", + "notes": "" } } save_users(users) @@ -362,11 +407,13 @@ def login(): if valid: login_user(user) + add_history_entry("user_login", f"User '{email}' angemeldet", email) if user.must_change_password: return redirect("/change-password") return redirect("/admin") elif not user.password_hash: login_user(user) + add_history_entry("user_login", f"User '{email}' angemeldet (Passwort-Änderung erforderlich)", email) return redirect("/change-password") error = "Ungültige Zugangsdaten" @@ -376,7 +423,9 @@ def login(): @app.route("/logout") def logout(): """Loggt den Admin aus und leitet zur Login-Seite weiter.""" + email = current_user.email if current_user.is_authenticated else "unknown" logout_user() + add_history_entry("user_logout", f"User '{email}' abgemeldet", email) return redirect("/login") @@ -453,6 +502,7 @@ def add_customer(): site_cfg = config.setdefault("sites", {}).setdefault(site, {}) site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls} save_config(config) + 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!" else: error = "Fehler beim Hinzufügen zur Playliste" @@ -723,6 +773,8 @@ def add_site(): os.makedirs(os.path.join(MEDIA_DIR, name), exist_ok=True) save_config(cfg) + add_history_entry("site_created", f"Standort '{name}' angelegt", current_user.email) + return redirect(f"/admin/{name}") @@ -739,6 +791,8 @@ def delete_site(site): import shutil shutil.rmtree(site_dir) + add_history_entry("site_deleted", f"Standort '{site}' gelöscht", current_user.email) + sites = get_site_list(cfg) return redirect(f"/admin/{sites[0]}" if sites else "/admin") @@ -911,7 +965,6 @@ def upload_background(site): 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)) @@ -919,9 +972,10 @@ def upload_background(site): filename = f"background{ext}" file.save(os.path.join(site_dir, filename)) - # Willkommensseite mit neuen Hintergrund neu generieren _regenerate_welcome_for_site(site) + add_history_entry("background_uploaded", f"Hintergrundbild für Standort '{site}' hochgeladen", current_user.email) + return redirect(f"/willkommen?site={site}") @@ -935,9 +989,10 @@ def delete_background(site): if f.startswith("background."): os.remove(os.path.join(site_dir, f)) - # Willkommensseite mit Standard-Hintergrund neu generieren _regenerate_welcome_for_site(site) + add_history_entry("background_deleted", f"Hintergrundbild für Standort '{site}' zurückgesetzt", current_user.email) + return redirect(f"/willkommen?site={site}") @@ -1017,6 +1072,8 @@ def delete_screen(site, screen): import shutil shutil.rmtree(screen_dir) + add_history_entry("screen_deleted", f"Screen '{screen}' an Standort '{site}' gelöscht", current_user.email) + return redirect(f"/admin/{site}") @@ -1040,6 +1097,7 @@ def add_screen(site): screens_cfg[screen_name] = {"playlist": [], "interval": 10, "show_images": True, "show_videos": True} save_config(config) + add_history_entry("screen_created", f"Screen '{screen_name}' an Standort '{site}' angelegt", current_user.email) return redirect(f"/admin/{site}#screen-{screen_name}") @@ -1206,6 +1264,7 @@ def change_password(): users[email]["password_hash"] = generate_password_hash(new_pw) users[email]["must_change_password"] = False save_users(users) + add_history_entry("password_changed", f"Passwort für User '{email}' geändert", current_user.email) return redirect("/admin") return render_template( @@ -1224,6 +1283,96 @@ def change_password(): # ------------------------------------------------- # User-Verwaltung (nur für Admins) # ------------------------------------------------- +@app.route("/admin/dashboard") +@admin_required +def admin_dashboard(): + """Admin-Dashboard mit Statistiken: Sites, Screens, User, Admins, Superuser.""" + cfg = load_config() + users_data = load_users() + + sites = get_site_list(cfg) + screen_count = sum(len(site_cfg.get("screens", {})) for site_cfg in cfg.get("sites", {}).values()) + + user_count = len(users_data) + admin_count = sum(1 for u in users_data.values() if u.get("role") == "admin") + superuser_count = sum(1 for u in users_data.values() if u.get("role") == "superuser") + + history = get_history(30) + + # Sparkline-Daten aus dem Verlauf berechnen + def build_sparkline(entries, creates, deletes): + seq = list(reversed(entries)) + data = [0] + for e in seq: + a = e.get("action") + if a in creates: + data.append(data[-1] + 1) + elif a in deletes: + data.append(data[-1] - 1) + if len(data) < 3: + data = [0, 0, 0] + return data + + site_spark = build_sparkline(history, {"site_created"}, {"site_deleted"}) + screen_spark = build_sparkline(history, {"screen_created"}, {"screen_deleted"}) + user_spark = build_sparkline(history, {"user_created"}, {"user_deleted"}) + admin_spark = build_sparkline(history, {"admin_created"}, {"admin_deleted"}) + superuser_spark = build_sparkline(history, {"superuser_created"}, {"superuser_deleted"}) + + def trend_val(creates, deletes): + c = sum(1 for e in history if e.get("action") in creates) + d = sum(1 for e in history if e.get("action") in deletes) + return c - d + + site_trend = trend_val({"site_created"}, {"site_deleted"}) + screen_trend = trend_val({"screen_created"}, {"screen_deleted"}) + user_trend = trend_val({"user_created"}, {"user_deleted"}) + admin_trend = trend_val({"admin_created"}, {"admin_deleted"}) + superuser_trend = trend_val({"superuser_created"}, {"superuser_deleted"}) + + return render_template( + "admin_dashboard.html", + site_count=len(sites), + screen_count=screen_count, + user_count=user_count, + admin_count=admin_count, + superuser_count=superuser_count, + site_spark=site_spark, + screen_spark=screen_spark, + user_spark=user_spark, + admin_spark=admin_spark, + superuser_spark=superuser_spark, + site_trend=site_trend, + screen_trend=screen_trend, + user_trend=user_trend, + admin_trend=admin_trend, + superuser_trend=superuser_trend, + history=history, + site_list=get_accessible_sites(cfg, current_user), + current_site=get_site_list(cfg)[0] if get_site_list(cfg) else "", + version=APP_VERSION, + year=datetime.now().year, + hostname=os.uname().nodename, + server_url=cfg.get("server_url", "") + ) + + +@app.route("/admin/help") +@login_required +def admin_help(): + """Hilfeseite zur Bedienung des Tools.""" + cfg = load_config() + return render_template( + "help.html", + site_list=get_accessible_sites(cfg, current_user), + current_site=get_site_list(cfg)[0] if get_site_list(cfg) else "", + version=APP_VERSION, + year=datetime.now().year, + hostname=os.uname().nodename, + server_url=cfg.get("server_url", "") + ) + + @app.route("/admin/users") @admin_required def admin_users(): @@ -1237,12 +1386,16 @@ def admin_users(): "email": email, "role": data.get("role", "user"), "sites": data.get("sites", []), - "must_change_password": data.get("must_change_password", False) + "must_change_password": data.get("must_change_password", False), + "first_name": data.get("first_name", ""), + "last_name": data.get("last_name", ""), + "department": data.get("department", ""), + "notes": data.get("notes", "") }) return render_template( "user_list.html", - users=sorted(users_list, key=lambda u: u["email"]), + users=sorted(users_list, key=lambda u: (u["last_name"].lower(), u["first_name"].lower())), site_list=get_accessible_sites(cfg, current_user), current_site=get_site_list(cfg)[0] if get_site_list(cfg) else "", version=APP_VERSION, @@ -1296,9 +1449,18 @@ def admin_users_create(): "password_hash": generate_password_hash(password), "role": role, "sites": sites if role == "user" else [], - "must_change_password": True + "must_change_password": True, + "first_name": request.form.get("first_name", "").strip(), + "last_name": request.form.get("last_name", "").strip(), + "department": request.form.get("department", "").strip(), + "notes": request.form.get("notes", "").strip() } save_users(users) + add_history_entry("user_created", f"User '{email}' angelegt (Rolle: {role})", current_user.email) + if role == "admin": + add_history_entry("admin_created", f"Admin '{email}' angelegt", current_user.email) + elif role == "superuser": + add_history_entry("superuser_created", f"Superuser '{email}' angelegt", current_user.email) return redirect("/admin/users") @@ -1326,17 +1488,35 @@ def admin_users_edit(email): error = "Ungültige Rolle" else: user_data = users_data.pop(email) + old_role = user_data.get("role", "user") user_data["role"] = new_role user_data["sites"] = new_sites if new_role == "user" else [] + user_data["first_name"] = request.form.get("first_name", "").strip() + user_data["last_name"] = request.form.get("last_name", "").strip() + user_data["department"] = request.form.get("department", "").strip() + user_data["notes"] = request.form.get("notes", "").strip() users_data[new_email] = user_data save_users(users_data) + add_history_entry("user_updated", f"User '{email}' bearbeitet (Rolle: {old_role} → {new_role})", current_user.email) + if old_role == "admin" and new_role != "admin": + add_history_entry("admin_deleted", f"Admin-Rechte von '{email}' entzogen", current_user.email) + elif old_role != "admin" and new_role == "admin": + add_history_entry("admin_created", f"'{email}' zum Admin ernannt", current_user.email) + if old_role == "superuser" and new_role != "superuser": + add_history_entry("superuser_deleted", f"Superuser-Rechte von '{email}' entzogen", current_user.email) + elif old_role != "superuser" and new_role == "superuser": + add_history_entry("superuser_created", f"'{email}' zum Superuser ernannt", current_user.email) return redirect(f"/admin/users?updated={new_email}") user = { "email": email, "role": users_data[email].get("role", "user"), "sites": users_data[email].get("sites", []), - "must_change_password": users_data[email].get("must_change_password", False) + "must_change_password": users_data[email].get("must_change_password", False), + "first_name": users_data[email].get("first_name", ""), + "last_name": users_data[email].get("last_name", ""), + "department": users_data[email].get("department", ""), + "notes": users_data[email].get("notes", "") } return render_template( @@ -1363,14 +1543,21 @@ def admin_users_delete(email): users = load_users() + role = users.get(email, {}).get("role", "user") + # Prüfen, ob es der letzte Admin wäre - if users.get(email, {}).get("role") == "admin": + if role == "admin": admin_count = sum(1 for u in users.values() if u.get("role") == "admin") if admin_count <= 1: return "Letzter Admin kann nicht gelöscht werden", 400 users.pop(email, None) save_users(users) + add_history_entry("user_deleted", f"User '{email}' gelöscht", current_user.email) + if role == "admin": + add_history_entry("admin_deleted", f"Admin '{email}' gelöscht", current_user.email) + elif role == "superuser": + add_history_entry("superuser_deleted", f"Superuser '{email}' gelöscht", current_user.email) return redirect("/admin/users") @@ -1389,6 +1576,7 @@ def admin_users_reset_password(email): users[email]["password_hash"] = generate_password_hash(temp_password) users[email]["must_change_password"] = True save_users(users) + add_history_entry("user_password_reset", f"Passwort für User '{email}' zurückgesetzt", current_user.email) return redirect(f"/admin/users?reset={email}&temp={temp_password}") diff --git a/history.json b/history.json new file mode 100644 index 0000000..2d9f960 --- /dev/null +++ b/history.json @@ -0,0 +1,200 @@ +[ + { + "timestamp": "2026-06-20 16:38:54", + "action": "superuser_deleted", + "detail": "Superuser-Rechte von 'test1@test.de' entzogen", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:38:54", + "action": "user_updated", + "detail": "User 'test1@test.de' bearbeitet (Rolle: superuser \u2192 user)", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:38:19", + "action": "site_deleted", + "detail": "Standort 'wdqwsqws' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:30:45", + "action": "screen_created", + "detail": "Screen '11wssss' an Standort 'wdqwsqws' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:30:41", + "action": "screen_created", + "detail": "Screen 'dqwqqq' an Standort 'wdqwsqws' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:30:38", + "action": "screen_created", + "detail": "Screen 'asdsad' an Standort 'wdqwsqws' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:29:59", + "action": "site_deleted", + "detail": "Standort 'qwsqwsqs' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:29:53", + "action": "site_created", + "detail": "Standort 'qwsqwsqs' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:29:49", + "action": "site_created", + "detail": "Standort 'wdqwsqws' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:29:33", + "action": "site_deleted", + "detail": "Standort 'wsqwdsqws' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:29:28", + "action": "site_created", + "detail": "Standort 'wsqwdsqws' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:27:58", + "action": "site_deleted", + "detail": "Standort 'sdwqdwddd' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:27:45", + "action": "site_created", + "detail": "Standort 'sdwqdwddd' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:26:50", + "action": "site_deleted", + "detail": "Standort 'test1' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:26:43", + "action": "site_deleted", + "detail": "Standort 'test2' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:26:40", + "action": "screen_deleted", + "detail": "Screen 'dasdsfs' an Standort 'test2' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:26:36", + "action": "screen_deleted", + "detail": "Screen 'asdwedwed' an Standort 'test2' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:22:12", + "action": "site_deleted", + "detail": "Standort 'dwedwed' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:22:05", + "action": "site_created", + "detail": "Standort 'dwedwed' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:21:38", + "action": "screen_created", + "detail": "Screen 'asdwedwed' an Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:21:34", + "action": "screen_created", + "detail": "Screen 'dasdsfs' an Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:21:31", + "action": "screen_created", + "detail": "Screen 'asdasd' an Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:19:27", + "action": "screen_deleted", + "detail": "Screen 'test2' an Standort 'test2' gel\u00f6scht", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:18:49", + "action": "screen_created", + "detail": "Screen 'test2' an Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:18:45", + "action": "screen_created", + "detail": "Screen 'test1' an Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:18:40", + "action": "site_created", + "detail": "Standort 'test2' angelegt", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:16:32", + "action": "password_changed", + "detail": "Passwort f\u00fcr User 'erik.thiele@cancom.de' ge\u00e4ndert", + "user": "erik.thiele@cancom.de" + }, + { + "timestamp": "2026-06-20 16:14:03", + "action": "superuser_created", + "detail": "'test1@test.de' zum Superuser ernannt", + "user": "admin" + }, + { + "timestamp": "2026-06-20 16:14:03", + "action": "user_updated", + "detail": "User 'test1@test.de' bearbeitet (Rolle: user \u2192 superuser)", + "user": "admin" + }, + { + "timestamp": "2026-06-20 16:13:29", + "action": "user_created", + "detail": "User 'test1@test.de' angelegt (Rolle: user)", + "user": "admin" + }, + { + "timestamp": "2026-06-20 15:51:36", + "action": "screen_created", + "detail": "Screen 'test2' an Standort 'test1' angelegt", + "user": "admin" + }, + { + "timestamp": "2026-06-20 15:51:31", + "action": "screen_created", + "detail": "Screen 'test1' an Standort 'test1' angelegt", + "user": "admin" + }, + { + "timestamp": "2026-06-20 15:51:20", + "action": "site_created", + "detail": "Standort 'test1' angelegt", + "user": "admin" + } +] \ No newline at end of file diff --git a/templates/_header.html b/templates/_header.html index 2fb61a1..b9fc13c 100644 --- a/templates/_header.html +++ b/templates/_header.html @@ -33,7 +33,7 @@ + style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;padding:0;">
@@ -50,7 +50,9 @@ @@ -53,6 +55,16 @@
+
+
+ + +
+
+ + +
+
@@ -83,6 +95,14 @@
+
+ + +
+
+ + +
Abbrechen diff --git a/templates/user_edit.html b/templates/user_edit.html index d3f1b7d..a139089 100644 --- a/templates/user_edit.html +++ b/templates/user_edit.html @@ -32,8 +32,10 @@ @@ -57,6 +59,16 @@ +
+
+ + +
+
+ + +
+
+
+
+ + +
Abbrechen diff --git a/templates/user_list.html b/templates/user_list.html index 8f09d62..099a907 100644 --- a/templates/user_list.html +++ b/templates/user_list.html @@ -36,8 +36,10 @@ @@ -69,9 +71,11 @@ + + @@ -79,6 +83,7 @@ {% for u in users %} + + -
Name E-Mail Rolle StandorteAbteilung Status Aktionen
{{ u.last_name }}{% if u.last_name and u.first_name %}, {% endif %}{{ u.first_name }} {{ u.email }} {% if u.role == "admin" %} @@ -100,6 +105,7 @@ {% endfor %} {% endif %} {{ u.department }} {% if u.must_change_password %} Passwort-Änderung erforderlich @@ -107,7 +113,7 @@ Aktiv {% endif %} +
{% if u.email != current_user.email %}