Admin Dashboard und Hilfeseite

This commit is contained in:
Erik Thiele
2026-06-20 16:40:02 +02:00
parent ab1a36dee1
commit e18414ab79
12 changed files with 929 additions and 32 deletions

210
app.py
View File

@@ -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}")