Admin Dashboard und Hilfeseite
This commit is contained in:
210
app.py
210
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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user