From dba39b4f0493904d186bf835bfdc529ecb11a38a Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Tue, 10 Mar 2026 14:34:25 +0000 Subject: [PATCH] =?UTF-8?q?Logger=20eingef=C3=BCgt.=20Loggt=20ein=20paar?= =?UTF-8?q?=20Funktionen.=20Admin=20Panel=20zeigt=20Logs=20an.=20Bilder=20?= =?UTF-8?q?in=20die=20Gamesystem=20Cards=20eingef=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 1 + data/data_api.py | 64 +++++++++++++--- data/database.py | 2 +- gui/admin_gui.py | 37 ++++++++- gui/main_gui.py | 40 ++++------ main.py | 3 + wood/logger.py | 75 +++++++++++++++++++ 7 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json create mode 100644 wood/logger.py diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json new file mode 100644 index 0000000..e026266 --- /dev/null +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 2d9e783..20f0e78 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -1,5 +1,6 @@ import sqlite3 import random +from wood import logger from data.setup_database import DB_PATH @@ -50,13 +51,11 @@ def get_or_create_player(discord_id, discord_name, avatar_url): player = cursor.fetchone() if player is None: - # Random Silly Name Generator für neue Spieler. Damit sie angeregt werden ihren richtigen Namen einzutragen. - silly_name = generate_silly_name() cursor.execute("INSERT INTO players (discord_id, discord_name, display_name, discord_avatar_url) VALUES (?, ?, ?, ?)", (discord_id, discord_name, silly_name, avatar_url)) + logger.log("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + discord_name), player_id) connection.commit() - cursor.execute("SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = ?", (discord_id,)) player = cursor.fetchone() else: @@ -225,7 +224,7 @@ def join_league(player_id, gamesystem_id): INSERT INTO player_game_statistic (player_id, gamesystem_id) VALUES (?, ?) """ - + logger.log("INTO", f"{get_player_name(player_id)} ist Liga {gamesystem_id} beigetreten", player_id) cursor.execute(query, (player_id, gamesystem_id)) connection.commit() @@ -233,7 +232,6 @@ def join_league(player_id, gamesystem_id): - def get_recent_matches_for_player(player_id): connection = sqlite3.connect(DB_PATH) connection.row_factory = sqlite3.Row @@ -298,6 +296,8 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2): cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2)) new_match_id = cursor.lastrowid + logger.log("MATCH", f"Match eingetragen. {get_system_name(sys_id)} - {get_player_name(player1_id)} ({score_p1}) vs. {get_player_name(player2_id)} ({score_p2})", player1_id) + connection.commit() connection.close() @@ -394,8 +394,6 @@ def get_match_by_id(match_id): return dict(row) return None # Falls die ID nicht existiert - - from datetime import datetime def get_days_since_last_game(player_id): @@ -465,7 +463,7 @@ def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, score # ACHTUNG: Wir müssen scored_points ZWEIMAL übergeben! # Einmal für die 'points' und einmal für die Berechnung der 'avv_points'. cursor.execute(query, (mmr_change, scored_points, scored_points, player_id, gamesystem_id)) - + logger.log("MATCH CALC", f"Bei {get_player_name(player_id)} haben sich die MMR Punkte um {mmr_change} geändert.") connection.commit() connection.close() @@ -515,6 +513,48 @@ def update_match_mmr_change(match_id, p1_change, p2_change): +def get_player_name(player_id): + """Gibt den Namen eines Spielers im Format 'Anzeigename (Discordname)' zurück.""" + + # Sicherheits-Check: Falls aus Versehen gar keine ID übergeben wird + if player_id is None: + return "Unbekannter Spieler" + + connection = sqlite3.connect(DB_PATH) + cursor = connection.cursor() + + cursor.execute("SELECT display_name, discord_name FROM players WHERE id = ?", (player_id,)) + row = cursor.fetchone() + connection.close() + + # Wenn die Datenbank den Spieler gefunden hat: + if row: + display_name = row[0] + discord_name = row[1] + return f"{display_name} ({discord_name})" + else: + return "Gelöschter Spieler" + +def get_system_name(sys_id): + if sys_id is None: + return "Unbekanntes System" + connection = sqlite3.connect(DB_PATH) + cursor = connection.cursor() + cursor.execute("SELECT name FROM gamesystems WHERE id = ?", (sys_id,)) + + row = cursor.fetchone() + connection.close() + + if row: + return row[0] + else: + return "Gelöschtes System" + + + + + + # ----------------------------------------------------- # Matches Bestätigen, Löschen, Berechnen, ... # ----------------------------------------------------- @@ -544,14 +584,14 @@ def get_unconfirmed_matches(player_id): return [dict(row) for row in rows] -def delete_match(match_id): +def delete_match(match_id, player_id): """Löscht ein Match anhand seiner ID komplett aus der Datenbank.""" connection = sqlite3.connect(DB_PATH) cursor = connection.cursor() # DELETE FROM löscht die gesamte Zeile, bei der die ID übereinstimmt. cursor.execute("DELETE FROM matches WHERE id = ?", (match_id,)) - + logger.log ("MATCH", f"Match mit ID:{match_id} wurde gelöscht von {get_player_name(player_id)}") connection.commit() connection.close() @@ -562,7 +602,7 @@ def confirm_match(match_id): # Ändert nur die Spalte player2_check auf 1 (True) cursor.execute("UPDATE matches SET player2_check = 1 WHERE id = ?", (match_id,)) - + logger.log ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Confirmed' gekennzeichnet") connection.commit() connection.close() @@ -574,7 +614,7 @@ def set_match_counted(match_id): # Ändert nur die Spalte match_is_counted auf 1 (True) cursor.execute("UPDATE matches SET match_is_counted = 1 WHERE id = ?", (match_id,)) - + logger.log ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Counted' gekennzeichnet") connection.commit() connection.close() diff --git a/data/database.py b/data/database.py index 7d1e160..9bd6420 100644 --- a/data/database.py +++ b/data/database.py @@ -10,7 +10,7 @@ if __name__ == "__main__": def check_db(): # --- DATENBANK CHECK --- - # Prüfen, ob die Datei im aktuellen Ordner existiert + # Prüfen, ob die Datei existiert db_file = DB_PATH if not os.path.exists(db_file): diff --git a/gui/admin_gui.py b/gui/admin_gui.py index 7a30fa8..d299557 100644 --- a/gui/admin_gui.py +++ b/gui/admin_gui.py @@ -1,10 +1,41 @@ from nicegui import ui, app from data import database, data_api from gui import gui_style +from wood import logger def setup_routes(): @ui.page('/admin', dark=True) - def home_page(): + def admin_page(): gui_style.apply_design() - if app.storage.user.get('authenticated', False): - ui.card().classes("w-full") \ No newline at end of file + + with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): + ui.button("Zurück", on_click= lambda: ui.navigate.to("/") ) + + if app.storage.user.get('authenticated', False): + with ui.card().classes("w-full"): + with ui.row().classes("w-full"): + ui.button("test") + ui.button(icon="refresh", on_click=lambda: ui.navigate.reload) + + ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4') + + # Daten abrufen + log_data = logger.get_full_log() + + # Tabelle aufbauen + columns = [ + {'name': 'time', 'label': 'Zeitstempel', 'field': 'timestamp', 'align': 'left', 'sortable': True}, + {'name': 'user', 'label': 'Auslöser', 'field': 'player_name', 'align': 'left'}, + {'name': 'action', 'label': 'Aktion', 'field': 'action', 'align': 'left', 'sortable': True}, + {'name': 'details', 'label': 'Details', 'field': 'details', 'align': 'left'} + ] + + if len(log_data) > 0: + # Wir schneiden bei den Millisekunden vom Zeitstempel wieder etwas ab [:19] + for row in log_data: + row['timestamp'] = str(row['timestamp'])[:19] + + # Tabelle zeichnen + ui.table(columns=columns, rows=log_data, row_key='id').classes('w-full bg-zinc-900 text-gray-300') + else: + ui.label("Das Logbuch ist leer.").classes('text-gray-500 italic') \ No newline at end of file diff --git a/gui/main_gui.py b/gui/main_gui.py index 197eedb..43f0d57 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -105,10 +105,10 @@ def setup_routes(admin_discord_id): ui.button('Login with Discord', on_click=lambda: ui.navigate.to(auth_url)) - # --------------------------- - # --- Match Bestätigung --- - # --------------------------- - # --- Bestätigungs-Bereich für offene Spiele --- Der "Marian Balken !!!1!11!" + # --------------------------- + # --- Match Bestätigung --- + # --------------------------- + # Bestätigungs für offene Spiele --- Der "Marian Balken !!!1!11!" if app.storage.user.get('authenticated', False): unconfirmed_matches = data_api.get_unconfirmed_matches(player_id) @@ -119,7 +119,7 @@ def setup_routes(admin_discord_id): for match in unconfirmed_matches: - # --- NEU: Die Funktion, die beim Klick ausgeführt wird --- + # Button Funktionen. Akzeptieren oder Rejecten. Der Reject Button löscht das Match aus der DB. def reject_match(m_id): data_api.delete_match(m_id) ui.notify("Spiel abgelehnt und gelöscht!", color="warning") @@ -129,10 +129,7 @@ def setup_routes(admin_discord_id): ui.notify("Spiel akzeptiert. Wird Berechnet.") ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren calc_match.calculate_match(m_id) - - - # Für jedes Match machen wir eine kleine Reihe with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'): # Info-Text: Was wurde eingetragen? info_text = f"[{match['system_name']}] {match['p1_name']} behauptet: Er hat {match['score_player1']} : {match['score_player2']} gegen dich gespielt." @@ -146,11 +143,11 @@ def setup_routes(admin_discord_id): # BESTÄTIGEN und spiel berechnen lassen ui.button("Bestätigen", color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id)) + # --------------------------- # --- Selbst eingetragene, offene Spiele --- # --------------------------- submitted_matches = data_api.get_submitted_matches(player_id) - if len(submitted_matches) > 0: # Eine etwas dezentere Karte (grau) with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'): @@ -160,7 +157,7 @@ def setup_routes(admin_discord_id): # Die Lösch-Funktion, die beim Klick ausgeführt wird def retract_match(m_id): - data_api.delete_match(m_id) + data_api.delete_match(m_id, player_id) ui.notify("Eingetragenes Spiel zurückgezogen!", color="warning") ui.navigate.reload() @@ -178,7 +175,6 @@ def setup_routes(admin_discord_id): # --------------------------- # --- Spielsysteme --- # --------------------------- - if app.storage.user.get('authenticated', False): with ui.card().classes("w-full"): ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl") @@ -186,8 +182,6 @@ def setup_routes(admin_discord_id): placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_data() - # Python-Trick: Wir wandeln die Spieler-Stats in ein "Wörterbuch" (Dictionary) um. - # So können wir blitzschnell über die System-ID nachschauen, ob er Stats hat. my_stats = { p['gamesystem_id']: p for p in placements } def click_join_league(p_id, sys_id): @@ -203,22 +197,18 @@ def setup_routes(admin_discord_id): for sys in systems: sys_id = sys['id'] sys_name = sys['name'] + sys_logo = sys["picture"] + sys_description = sys['description'] sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors") - + with sys_card: - # Kopfzeile - ui.label(text=sys_name).classes('text-xl font-bold') + ui.label(text=sys_name).classes('text-xl font-bold text-center') + if sys_logo: + ui.image(f"/pictures/{sys_logo}").classes("w-60") - # --- BILD EINFÜGEN --- - # prüfen, ob die Spalte 'pictures' existiert und nicht leer ist - if 'pictures' in sys and sys['pictures']: - ui.image(f"/pictures/{sys['pictures']}").classes('w-24 h-24 object-contain m-2') - - if 'description' in sys and sys['description']: - ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center') - - ui.space() + if sys_description: + ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2') # Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR? if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None: diff --git a/main.py b/main.py index 006e3de..9c30b26 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from nicegui import ui, app from data import database from gui import main_gui, match_gui, discord_login, league_statistic, admin_gui, match_history_gui +from wood import logger # 1. Lade die geheimen Variablen aus der .env Datei in den Speicher load_dotenv() @@ -22,6 +23,8 @@ admin_discord_id = os.getenv("ADMIN") url = os.getenv("APP_URL") database.check_db() +logger.setup_log_db() + # 3. Seitenrouten aufbauen main_gui.setup_routes(admin_discord_id) diff --git a/wood/logger.py b/wood/logger.py new file mode 100644 index 0000000..c50652d --- /dev/null +++ b/wood/logger.py @@ -0,0 +1,75 @@ +import sqlite3 +import os + +from data.setup_database import DB_PATH + +DB_FILE = "wood/log.db" + +def log(action, details, player_id = None): + """Schreibt ein Ereignis (Audit Trail) in das System-Log.""" + connection = sqlite3.connect(DB_FILE) + cursor = connection.cursor() + + cursor.execute( + "INSERT INTO system_log (player_id, action, details) VALUES (?, ?, ?)", + (player_id, action, details) + ) + + connection.commit() + connection.close() + + +def setup_log_db(): + # --- DATENBANK CHECK --- + # ACHTUNG: Großschreibung bei DB_FILE beachten! + if not os.path.exists(DB_FILE): + print(f"WARNUNG: '{DB_FILE}' nicht gefunden!") + print("Starte Log-Datenbank-Einrichtung...") + + # Wir bauen die Datei direkt HIER auf, ohne setup_database aufzurufen! + connection = sqlite3.connect(DB_FILE) + cursor = connection.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + player_id INTEGER, + action TEXT, + details TEXT + ) + ''') + connection.commit() + connection.close() + print("Log-Datenbank aufgebaut!") + else: + print(f"OK: Log-Datenbank '{DB_FILE}' gefunden. Lade System...") + + +def get_full_log(): + """Holt das komplette Log und übersetzt die Spieler-IDs in Namen.""" + # 1. Alle Logs aus der log.db holen + log_conn = sqlite3.connect(DB_FILE) + log_conn.row_factory = sqlite3.Row + log_cursor = log_conn.cursor() + + log_cursor.execute("SELECT * FROM system_log ORDER BY timestamp DESC") + logs = [dict(row) for row in log_cursor.fetchall()] + log_conn.close() + + # 2. Spielernamen aus der Haupt-DB holen + main_conn = sqlite3.connect(DB_PATH) + main_cursor = main_conn.cursor() + main_cursor.execute("SELECT id, display_name, discord_name FROM players") + players_dict = {row[0]: f"{row[1]} ({row[2]})" for row in main_cursor.fetchall()} + main_conn.close() + + # 3. Die IDs in den Logs durch Namen ersetzen + for log_entry in logs: + p_id = log_entry['player_id'] + if p_id is not None and p_id in players_dict: + log_entry['player_name'] = players_dict[p_id] + else: + log_entry['player_name'] = "System / Unbekannt" + + return logs \ No newline at end of file