From aeb91738255067292109f49aeabba1bf5cedd45b Mon Sep 17 00:00:00 2001 From: MeBeBruno Date: Sun, 4 Oct 2026 22:47:53 +0200 Subject: [PATCH] More smoothness and persistence --- application.py | 1223 +++++++++++++++++----------------- bundle_project.py | 87 +++ game_database.sqlite | Bin 32768 -> 32768 bytes static/behavior.js | 683 ++++++++++--------- static/logo.svg | 11 +- static/stylesheet.css | 627 ++++++++++------- templates/administrator.html | 686 +++++++++---------- templates/game.html | 371 ++++++----- templates/layout.html | 54 +- templates/login.html | 147 ++-- 10 files changed, 2113 insertions(+), 1776 deletions(-) create mode 100644 bundle_project.py diff --git a/application.py b/application.py index 54f296a..f4504d3 100644 --- a/application.py +++ b/application.py @@ -19,654 +19,681 @@ from flask_socketio import SocketIO, emit, join_room application = Flask(__name__) application.secret_key = secrets.token_hex(64) websocket_manager = SocketIO(application, manage_session=False) -scheduler_instance = BackgroundScheduler(timezone="Europe/Berlin") -european_timezone = zoneinfo.ZoneInfo("Europe/Berlin") +scheduler_instance = BackgroundScheduler(timezone='Europe/Berlin') +european_timezone = zoneinfo.ZoneInfo('Europe/Berlin') IDENTIFIER_LENGTH = 5 def get_database_connection(): - connection = sqlite3.connect("game_database.sqlite") - connection.row_factory = sqlite3.Row - return connection + connection = sqlite3.connect('game_database.sqlite') + connection.row_factory = sqlite3.Row + return connection def initialize_database(): - connection = get_database_connection() - connection.execute(""" - CREATE TABLE IF NOT EXISTS game_configuration ( - configuration_key TEXT PRIMARY KEY, - configuration_value TEXT - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS player ( - player_identifier TEXT PRIMARY KEY, - password_hash TEXT, - display_name TEXT, - is_alive INTEGER DEFAULT 1, - score INTEGER DEFAULT 0, - target_identifier TEXT, - reset_token TEXT, - pending_attacker_identifier TEXT, - is_approved INTEGER DEFAULT 0, - pending_display_name TEXT, - late_joiner INTEGER DEFAULT 0 - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS administrator ( - administrator_identifier INTEGER PRIMARY KEY, - password_hash TEXT, - two_factor_secret TEXT, - two_factor_enabled INTEGER DEFAULT 0 - ) - """) - connection.execute(""" - CREATE TABLE IF NOT EXISTS kill_log ( - kill_identifier INTEGER PRIMARY KEY AUTOINCREMENT, - killer_identifier TEXT, - victim_identifier TEXT, - victim_previous_target TEXT, - kill_timestamp TEXT, - report_status TEXT DEFAULT 'confirmed' - ) - """) - try: - connection.execute("ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN pending_display_name TEXT") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0") - except sqlite3.OperationalError: - pass - try: - connection.execute("ALTER TABLE kill_log ADD COLUMN report_status TEXT DEFAULT 'confirmed'") - except sqlite3.OperationalError: - pass + connection = get_database_connection() + connection.execute(''' + CREATE TABLE IF NOT EXISTS game_configuration ( + configuration_key TEXT PRIMARY KEY, + configuration_value TEXT + ) + ''') + connection.execute(''' + CREATE TABLE IF NOT EXISTS player ( + player_identifier TEXT PRIMARY KEY, + password_hash TEXT, + display_name TEXT, + is_alive INTEGER DEFAULT 1, + score INTEGER DEFAULT 0, + target_identifier TEXT, + reset_token TEXT, + pending_attacker_identifier TEXT, + is_approved INTEGER DEFAULT 0, + pending_display_name TEXT, + late_joiner INTEGER DEFAULT 0 + ) + ''') + connection.execute(''' + CREATE TABLE IF NOT EXISTS administrator ( + administrator_identifier INTEGER PRIMARY KEY, + password_hash TEXT, + two_factor_secret TEXT, + two_factor_enabled INTEGER DEFAULT 0 + ) + ''') + connection.execute(''' + CREATE TABLE IF NOT EXISTS kill_log ( + kill_identifier INTEGER PRIMARY KEY AUTOINCREMENT, + killer_identifier TEXT, + victim_identifier TEXT, + victim_previous_target TEXT, + kill_timestamp TEXT, + report_status TEXT DEFAULT 'confirmed' + ) + ''') + try: + connection.execute('ALTER TABLE administrator ADD COLUMN two_factor_enabled INTEGER DEFAULT 0') + except sqlite3.OperationalError: + pass + try: + connection.execute('ALTER TABLE player ADD COLUMN is_approved INTEGER DEFAULT 0') + except sqlite3.OperationalError: + pass + try: + connection.execute('ALTER TABLE player ADD COLUMN pending_display_name TEXT') + except sqlite3.OperationalError: + pass + try: + connection.execute('ALTER TABLE player ADD COLUMN late_joiner INTEGER DEFAULT 0') + except sqlite3.OperationalError: + pass + try: + connection.execute('ALTER TABLE kill_log ADD COLUMN report_status TEXT DEFAULT "confirmed"') + except sqlite3.OperationalError: + pass - cursor = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'") - if not cursor.fetchone(): - connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('game_status', 'pending')") - connection.execute("INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ('start_time', '')") - connection.commit() - connection.close() + cursor = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"') + if not cursor.fetchone(): + connection.execute('INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ("game_status", "pending")') + connection.execute('INSERT INTO game_configuration (configuration_key, configuration_value) VALUES ("start_time", "")') + connection.commit() + connection.close() def require_player_authentication(function): - def wrapper(*arguments, **keyword_arguments): - if "player_identifier" not in session: - return redirect(url_for("handle_login")) - return function(*arguments, **keyword_arguments) - wrapper.__name__ = function.__name__ - return wrapper + def wrapper(*arguments, **keyword_arguments): + if 'player_identifier' not in session: + return redirect(url_for('handle_login')) + return function(*arguments, **keyword_arguments) + wrapper.__name__ = function.__name__ + return wrapper def require_administrator_authentication(function): - def wrapper(*arguments, **keyword_arguments): - if not session.get("administrator_authenticated"): - return redirect(url_for("handle_administrator")) - return function(*arguments, **keyword_arguments) - wrapper.__name__ = function.__name__ - return wrapper + def wrapper(*arguments, **keyword_arguments): + if not session.get('administrator_authenticated'): + return redirect(url_for('handle_administrator')) + return function(*arguments, **keyword_arguments) + wrapper.__name__ = function.__name__ + return wrapper def get_markdown_rules(): - if os.path.exists("rules.md"): - with open("rules.md", "r", encoding="utf-8") as markdown_file: - return markdown.markdown(markdown_file.read()) - return "

Regelwerk nicht gefunden

" + if os.path.exists('rules.md'): + with open('rules.md', 'r', encoding='utf-8') as markdown_file: + return markdown.markdown(markdown_file.read()) + return '

Regelwerk nicht gefunden

' def shuffle_player_targets(): - connection = get_database_connection() - players = connection.execute("SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall() - if len(players) > 1: - identifiers = [player_record["player_identifier"] for player_record in players] - secure_random_generator = secrets.SystemRandom() - secure_random_generator.shuffle(identifiers) - for index, identifier in enumerate(identifiers): - target = identifiers[(index + 1) % len(identifiers)] - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target, identifier)) - connection.commit() - connection.close() - websocket_manager.emit("game_state_changed") + connection = get_database_connection() + players = connection.execute('SELECT player_identifier FROM player WHERE is_alive = 1 AND is_approved = 1').fetchall() + if len(players) > 1: + identifiers = [player_record['player_identifier'] for player_record in players] + secure_random_generator = secrets.SystemRandom() + secure_random_generator.shuffle(identifiers) + for index, identifier in enumerate(identifiers): + target = identifiers[(index + 1) % len(identifiers)] + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (target, identifier)) + connection.commit() + connection.close() + websocket_manager.emit('game_state_changed') def process_administrator_action(action_type, form_data): - connection = get_database_connection() - has_game_state_changed = False - - if action_type == "start_game": - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone() - if alive_count_record["alive_count"] >= 2: - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE game_configuration SET configuration_value = ? WHERE configuration_key = 'start_time'", (datetime.now(european_timezone).isoformat(),)) - connection.commit() - connection.close() - shuffle_player_targets() - websocket_manager.emit("admin_state_changed", to="administrator_room") - return - - elif action_type == "stop_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "pause_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'paused' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "resume_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.commit() - has_game_state_changed = True - - elif action_type == "reset_game": - connection.execute("UPDATE game_configuration SET configuration_value = 'pending' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE game_configuration SET configuration_value = '' WHERE configuration_key = 'start_time'") - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, score = 0, target_identifier = NULL, pending_attacker_identifier = NULL") - connection.execute("DELETE FROM kill_log") - connection.commit() - has_game_state_changed = True - - elif action_type == "create_player": - display_name = form_data.get("display_name") - new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) - temporary_password = secrets.token_urlsafe(16) - hashed_password = argon2.hash(temporary_password) - connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name)) - connection.commit() - - elif action_type == "approve_player": - target_identifier = form_data.get("player_identifier") - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - is_late_joiner = 1 if status_record["configuration_value"] in ['running', 'paused'] else 0 - is_alive_status = 0 if is_late_joiner else 1 - connection.execute("UPDATE player SET is_approved = 1, is_alive = ?, late_joiner = ? WHERE player_identifier = ?", (is_alive_status, is_late_joiner, target_identifier)) - connection.commit() - has_game_state_changed = True - - elif action_type == "edit_name": - target_identifier = form_data.get("player_identifier") - new_name = form_data.get("new_display_name") - connection.execute("UPDATE player SET display_name = ? WHERE player_identifier = ?", (new_name, target_identifier)) - connection.commit() - has_game_state_changed = True - - elif action_type == "approve_name_change": - target_identifier = form_data.get("player_identifier") - connection.execute("UPDATE player SET display_name = pending_display_name, pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - has_game_state_changed = True - - elif action_type == "reject_name_change": - target_identifier = form_data.get("player_identifier") - connection.execute("UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - websocket_manager.emit("game_state_changed", to=target_identifier) - - elif action_type == "delete_player": - target_identifier = form_data.get("player_identifier") - connection.execute("DELETE FROM player WHERE player_identifier = ?", (target_identifier,)) - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - if status_record["configuration_value"] == 'running': - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - if alive_count_record["alive_count"] == 1: - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - connection.commit() - has_game_state_changed = True - - elif action_type == "revive_player": - target_identifier = form_data.get("player_identifier") - player_to_revive = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone() - if player_to_revive and player_to_revive["is_alive"] == 0: - alive_players = connection.execute("SELECT * FROM player WHERE is_alive = 1 AND is_approved = 1").fetchall() - if len(alive_players) > 0: - random_living_player = secrets.choice(alive_players) - random_living_identifier = random_living_player["player_identifier"] - attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (random_living_identifier,)).fetchone() - if attacker: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (target_identifier, attacker["player_identifier"])) - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = ? WHERE player_identifier = ?", (random_living_identifier, target_identifier)) - else: - connection.execute("UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?", (target_identifier,)) - connection.commit() - has_game_state_changed = True - - elif action_type == "eliminate_player": - target_identifier = form_data.get("player_identifier") - victim = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (target_identifier,)).fetchone() - if victim and victim["is_alive"] == 1: - attacker = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_identifier,)).fetchone() - new_target = victim["target_identifier"] - connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (target_identifier,)) - - attacker_identifier = None - if attacker: - attacker_identifier = attacker["player_identifier"] - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target, attacker_identifier)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, 'confirmed')", (attacker_identifier, target_identifier, new_target, timestamp)) - - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - current_status = status_record["configuration_value"] if status_record else "unknown" - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - - if alive_count_record["alive_count"] == 1 and current_status != 'finished': - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - - connection.commit() - has_game_state_changed = True + connection = get_database_connection() + has_game_state_changed = False + + if action_type == 'start_game': + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1').fetchone() + if alive_count_record['alive_count'] >= 2: + connection.execute('UPDATE game_configuration SET configuration_value = "running" WHERE configuration_key = "game_status"') + connection.execute('UPDATE game_configuration SET configuration_value = ? WHERE configuration_key = "start_time"', (datetime.now(european_timezone).isoformat(),)) + connection.commit() + connection.close() + shuffle_player_targets() + websocket_manager.emit('admin_state_changed', to='administrator_room') + return + + elif action_type == 'stop_game': + connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"') + connection.commit() + has_game_state_changed = True + + elif action_type == 'pause_game': + connection.execute('UPDATE game_configuration SET configuration_value = "paused" WHERE configuration_key = "game_status"') + connection.commit() + has_game_state_changed = True + + elif action_type == 'resume_game': + connection.execute('UPDATE game_configuration SET configuration_value = "running" WHERE configuration_key = "game_status"') + connection.commit() + has_game_state_changed = True + + elif action_type == 'reset_game': + connection.execute('UPDATE game_configuration SET configuration_value = "pending" WHERE configuration_key = "game_status"') + connection.execute('UPDATE game_configuration SET configuration_value = "" WHERE configuration_key = "start_time"') + connection.execute('UPDATE player SET is_alive = 1, late_joiner = 0, score = 0, target_identifier = NULL, pending_attacker_identifier = NULL') + connection.execute('DELETE FROM kill_log') + connection.commit() + has_game_state_changed = True + + elif action_type == 'create_player': + display_name = form_data.get('display_name') + new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) + temporary_password = secrets.token_urlsafe(16) + hashed_password = argon2.hash(temporary_password) + connection.execute('INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)', (new_identifier, hashed_password, display_name)) + connection.commit() + + elif action_type == 'approve_player': + target_identifier = form_data.get('player_identifier') + status_record = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + is_late_joiner = 1 if status_record['configuration_value'] in ['running', 'paused'] else 0 + is_alive_status = 0 if is_late_joiner else 1 + connection.execute('UPDATE player SET is_approved = 1, is_alive = ?, late_joiner = ? WHERE player_identifier = ?', (is_alive_status, is_late_joiner, target_identifier)) + connection.commit() + has_game_state_changed = True + websocket_manager.emit('game_state_changed', to=target_identifier) + + elif action_type == 'edit_name': + target_identifier = form_data.get('player_identifier') + new_name = form_data.get('new_display_name') + connection.execute('UPDATE player SET display_name = ? WHERE player_identifier = ?', (new_name, target_identifier)) + connection.commit() + has_game_state_changed = True + + elif action_type == 'approve_name_change': + target_identifier = form_data.get('player_identifier') + connection.execute('UPDATE player SET display_name = pending_display_name, pending_display_name = NULL WHERE player_identifier = ?', (target_identifier,)) + connection.commit() + has_game_state_changed = True + + elif action_type == 'reject_name_change': + target_identifier = form_data.get('player_identifier') + connection.execute('UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?', (target_identifier,)) + connection.commit() + websocket_manager.emit('game_state_changed', to=target_identifier) + + elif action_type == 'delete_player': + target_identifier = form_data.get('player_identifier') + connection.execute('DELETE FROM player WHERE player_identifier = ?', (target_identifier,)) + status_record = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + if status_record['configuration_value'] == 'running': + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1').fetchone() + if alive_count_record['alive_count'] <= 1: + connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"') + if alive_count_record['alive_count'] == 1: + connection.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1') + connection.commit() + has_game_state_changed = True + + elif action_type == 'revive_player': + target_identifier = form_data.get('player_identifier') + player_to_revive = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (target_identifier,)).fetchone() + if player_to_revive and player_to_revive['is_alive'] == 0: + alive_players = connection.execute('SELECT * FROM player WHERE is_alive = 1 AND is_approved = 1').fetchall() + if len(alive_players) > 0: + random_living_player = secrets.choice(alive_players) + random_living_identifier = random_living_player['player_identifier'] + attacker = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (random_living_identifier,)).fetchone() + if attacker: + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (target_identifier, attacker['player_identifier'])) + connection.execute('UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = ? WHERE player_identifier = ?', (random_living_identifier, target_identifier)) + else: + connection.execute('UPDATE player SET is_alive = 1, late_joiner = 0, target_identifier = NULL WHERE player_identifier = ?', (target_identifier,)) + connection.commit() + has_game_state_changed = True + + elif action_type == 'eliminate_player': + target_identifier = form_data.get('player_identifier') + victim = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (target_identifier,)).fetchone() + if victim and victim['is_alive'] == 1: + attacker = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (target_identifier,)).fetchone() + new_target = victim['target_identifier'] + connection.execute('UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?', (target_identifier,)) + + attacker_identifier = None + if attacker: + attacker_identifier = attacker['player_identifier'] + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (new_target, attacker_identifier)) + + timestamp = datetime.now(european_timezone).isoformat() + connection.execute('INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, "confirmed")', (attacker_identifier, target_identifier, new_target, timestamp)) + + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1').fetchone() + if alive_count_record['alive_count'] <= 1: + status_record = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + current_status = status_record['configuration_value'] if status_record else 'unknown' + connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"') + + if alive_count_record['alive_count'] == 1 and current_status != 'finished': + connection.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1') + + connection.commit() + has_game_state_changed = True - elif action_type == "delete_report": - kill_identifier = form_data.get("kill_identifier") - report = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() - - if report and report["report_status"] == 'pending': - connection.execute("UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?", (report["victim_identifier"],)) - - connection.execute("DELETE FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)) - connection.commit() - has_game_state_changed = True + elif action_type == 'delete_report': + kill_identifier = form_data.get('kill_identifier') + report = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone() + + if report and report['report_status'] == 'pending': + connection.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (report['victim_identifier'],)) + + connection.execute('DELETE FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)) + connection.commit() + has_game_state_changed = True - elif action_type == "force_confirm_report": - kill_identifier = form_data.get("kill_identifier") - report = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() - - if report and report["report_status"] == 'pending': - attacker_identifier = report["killer_identifier"] - victim_identifier = report["victim_identifier"] - victim = connection.execute("SELECT target_identifier, is_alive FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - - if victim and victim["is_alive"] == 1: - new_target = victim["target_identifier"] - connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,)) - connection.execute("UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?", (new_target, attacker_identifier)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("UPDATE kill_log SET report_status = 'confirmed', kill_timestamp = ? WHERE kill_identifier = ?", (timestamp, kill_identifier)) - - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - current_status = status_record["configuration_value"] if status_record else "unknown" - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - - if alive_count_record["alive_count"] == 1 and current_status != 'finished': - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - - connection.commit() - has_game_state_changed = True + elif action_type == 'force_confirm_report': + kill_identifier = form_data.get('kill_identifier') + report = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone() + + if report and report['report_status'] == 'pending': + attacker_identifier = report['killer_identifier'] + victim_identifier = report['victim_identifier'] + victim = connection.execute('SELECT target_identifier, is_alive FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone() + + if victim and victim['is_alive'] == 1: + new_target = victim['target_identifier'] + connection.execute('UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim_identifier,)) + connection.execute('UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?', (new_target, attacker_identifier)) + + timestamp = datetime.now(european_timezone).isoformat() + connection.execute('UPDATE kill_log SET report_status = "confirmed", kill_timestamp = ? WHERE kill_identifier = ?', (timestamp, kill_identifier)) + + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1').fetchone() + if alive_count_record['alive_count'] <= 1: + status_record = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + current_status = status_record['configuration_value'] if status_record else 'unknown' + connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"') + + if alive_count_record['alive_count'] == 1 and current_status != 'finished': + connection.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1') + + connection.commit() + has_game_state_changed = True - elif action_type == "revoke_kill": - kill_identifier = form_data.get("kill_identifier") - kill_record = connection.execute("SELECT * FROM kill_log WHERE kill_identifier = ?", (kill_identifier,)).fetchone() - - if kill_record and kill_record["report_status"] == 'confirmed': - if kill_record["killer_identifier"]: - connection.execute("UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?", (kill_record["killer_identifier"],)) - - victim_identifier = kill_record["victim_identifier"] - target_to_restore = kill_record["victim_previous_target"] - - player_pointing_to_target = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (target_to_restore,)).fetchone() - - connection.execute("UPDATE player SET is_alive = 1, target_identifier = ? WHERE player_identifier = ?", (target_to_restore, victim_identifier)) - if player_pointing_to_target: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (victim_identifier, player_pointing_to_target["player_identifier"])) - - connection.execute("UPDATE kill_log SET report_status = 'revoked' WHERE kill_identifier = ?", (kill_identifier,)) - - alive_count = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone()["alive_count"] - if alive_count > 1: - status = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone()["configuration_value"] - if status == 'finished': - connection.execute("UPDATE game_configuration SET configuration_value = 'running' WHERE configuration_key = 'game_status'") - connection.execute("UPDATE player SET score = MAX(0, score - 3) WHERE is_alive = 1 AND is_approved = 1 AND player_identifier != ?", (victim_identifier,)) - - connection.commit() - has_game_state_changed = True - - elif action_type == "change_target": - player_identifier = form_data.get("player_identifier") - new_target_identifier = form_data.get("new_target_identifier") - if player_identifier and new_target_identifier and player_identifier != new_target_identifier: - player_record = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (player_identifier,)).fetchone() - other_player_record = connection.execute("SELECT player_identifier FROM player WHERE target_identifier = ?", (new_target_identifier,)).fetchone() - if player_record and other_player_record: - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (new_target_identifier, player_identifier)) - connection.execute("UPDATE player SET target_identifier = ? WHERE player_identifier = ?", (player_record["target_identifier"], other_player_record["player_identifier"])) - connection.commit() - has_game_state_changed = True + elif action_type == 'revoke_kill': + kill_identifier = form_data.get('kill_identifier') + kill_record = connection.execute('SELECT * FROM kill_log WHERE kill_identifier = ?', (kill_identifier,)).fetchone() + + if kill_record and kill_record['report_status'] == 'confirmed': + if kill_record['killer_identifier']: + connection.execute('UPDATE player SET score = MAX(0, score - 1) WHERE player_identifier = ?', (kill_record['killer_identifier'],)) + + victim_identifier = kill_record['victim_identifier'] + target_to_restore = kill_record['victim_previous_target'] + + player_pointing_to_target = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (target_to_restore,)).fetchone() + + connection.execute('UPDATE player SET is_alive = 1, target_identifier = ? WHERE player_identifier = ?', (target_to_restore, victim_identifier)) + if player_pointing_to_target: + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (victim_identifier, player_pointing_to_target['player_identifier'])) + + connection.execute('UPDATE kill_log SET report_status = "revoked" WHERE kill_identifier = ?', (kill_identifier,)) + + alive_count = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1').fetchone()['alive_count'] + if alive_count > 1: + status = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone()['configuration_value'] + if status == 'finished': + connection.execute('UPDATE game_configuration SET configuration_value = "running" WHERE configuration_key = "game_status"') + connection.execute('UPDATE player SET score = MAX(0, score - 3) WHERE is_alive = 1 AND is_approved = 1 AND player_identifier != ?', (victim_identifier,)) + + connection.commit() + has_game_state_changed = True + + elif action_type == 'change_target': + player_identifier = form_data.get('player_identifier') + new_target_identifier = form_data.get('new_target_identifier') + if player_identifier and new_target_identifier and player_identifier != new_target_identifier: + player_record = connection.execute('SELECT target_identifier FROM player WHERE player_identifier = ?', (player_identifier,)).fetchone() + other_player_record = connection.execute('SELECT player_identifier FROM player WHERE target_identifier = ?', (new_target_identifier,)).fetchone() + if player_record and other_player_record: + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (new_target_identifier, player_identifier)) + connection.execute('UPDATE player SET target_identifier = ? WHERE player_identifier = ?', (player_record['target_identifier'], other_player_record['player_identifier'])) + connection.commit() + has_game_state_changed = True - elif action_type == "generate_reset": - target_identifier = form_data.get("player_identifier") - reset_token = secrets.token_urlsafe(32) - connection.execute("UPDATE player SET reset_token = ? WHERE player_identifier = ?", (reset_token, target_identifier)) - connection.commit() - - elif action_type == "update_credentials": - new_password = form_data.get("password_string") - password_confirmation = form_data.get("password_string_confirmation") - if new_password and new_password == password_confirmation: - hashed_password = argon2.hash(new_password) - connection.execute("UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1", (hashed_password,)) - connection.commit() - - elif action_type == "enable_two_factor": - two_factor_input = form_data.get("two_factor_code") - administrator_record = connection.execute("SELECT two_factor_secret FROM administrator WHERE administrator_identifier = 1").fetchone() - time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"]) - if time_based_one_time_password_validator.verify(two_factor_input): - connection.execute("UPDATE administrator SET two_factor_enabled = 1 WHERE administrator_identifier = 1") - connection.commit() - - elif action_type == "disable_two_factor": - connection.execute("UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1") - connection.commit() - - connection.close() - - if has_game_state_changed: - websocket_manager.emit("game_state_changed") - else: - websocket_manager.emit("admin_state_changed", to="administrator_room") + elif action_type == 'generate_reset': + target_identifier = form_data.get('player_identifier') + reset_token = secrets.token_urlsafe(32) + connection.execute('UPDATE player SET reset_token = ? WHERE player_identifier = ?', (reset_token, target_identifier)) + connection.commit() + + elif action_type == 'update_credentials': + new_password = form_data.get('password_string') + password_confirmation = form_data.get('password_string_confirmation') + if new_password and new_password == password_confirmation: + hashed_password = argon2.hash(new_password) + connection.execute('UPDATE administrator SET password_hash = ? WHERE administrator_identifier = 1', (hashed_password,)) + connection.commit() + + elif action_type == 'enable_two_factor': + two_factor_input = form_data.get('two_factor_code') + administrator_record = connection.execute('SELECT two_factor_secret FROM administrator WHERE administrator_identifier = 1').fetchone() + time_based_one_time_password_validator = pyotp.TOTP(administrator_record['two_factor_secret']) + if time_based_one_time_password_validator.verify(two_factor_input): + connection.execute('UPDATE administrator SET two_factor_enabled = 1 WHERE administrator_identifier = 1') + connection.commit() + + elif action_type == 'disable_two_factor': + connection.execute('UPDATE administrator SET two_factor_enabled = 0 WHERE administrator_identifier = 1') + connection.commit() + + connection.close() + + if has_game_state_changed: + websocket_manager.emit('game_state_changed') + else: + websocket_manager.emit('admin_state_changed', to='administrator_room') def scheduled_action_wrapper(action_type, form_data): - with application.app_context(): - process_administrator_action(action_type, form_data) + with application.app_context(): + process_administrator_action(action_type, form_data) -@application.route("/service_worker.js") +@application.route('/service_worker.js') def serve_service_worker(): - return send_from_directory(application.static_folder, "service_worker.js") + return send_from_directory(application.static_folder, 'service_worker.js') -@application.route("/") +@application.route('/') @require_player_authentication def handle_index(): - connection = get_database_connection() - player = connection.execute("SELECT * FROM player WHERE player_identifier = ?", (session["player_identifier"],)).fetchone() - if not player: - session.clear() - return redirect(url_for("handle_login")) - status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - game_status = status_row["configuration_value"] - time_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'start_time'").fetchone() - start_time = time_row["configuration_value"] - - target_player = None - if player["target_identifier"]: - target_player = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["target_identifier"],)).fetchone() - - pending_attacker_name = None - if player["pending_attacker_identifier"]: - attacker = connection.execute("SELECT display_name FROM player WHERE player_identifier = ?", (player["pending_attacker_identifier"],)).fetchone() - if attacker: - pending_attacker_name = attacker["display_name"] - - all_players = connection.execute("SELECT * FROM player WHERE is_approved = 1 ORDER BY score DESC").fetchall() - total_eliminations = sum(player_record["score"] for player_record in all_players) - alive_count = sum(1 for player_record in all_players if player_record["is_alive"] == 1) - connection.close() - - document_markup = get_markdown_rules() - - return render_template( - "game.html", - player=player, - game_status=game_status, - start_time=start_time, - target_player=target_player, - pending_attacker_name=pending_attacker_name, - all_players=all_players, - total_eliminations=total_eliminations, - alive_count=alive_count, - rules_markup=document_markup - ) + connection = get_database_connection() + player = connection.execute('SELECT * FROM player WHERE player_identifier = ?', (session['player_identifier'],)).fetchone() + if not player: + session.clear() + return redirect(url_for('handle_login')) + status_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + game_status = status_row['configuration_value'] + time_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "start_time"').fetchone() + start_time = time_row['configuration_value'] + + target_player = None + if player['target_identifier']: + target_player = connection.execute('SELECT display_name FROM player WHERE player_identifier = ?', (player['target_identifier'],)).fetchone() + + pending_attacker_name = None + if player['pending_attacker_identifier']: + attacker = connection.execute('SELECT display_name FROM player WHERE player_identifier = ?', (player['pending_attacker_identifier'],)).fetchone() + if attacker: + pending_attacker_name = attacker['display_name'] + + pending_kill_record = connection.execute('SELECT kill_identifier FROM kill_log WHERE killer_identifier = ? AND report_status = "pending"', (player['player_identifier'],)).fetchone() + has_pending_kill_report = pending_kill_record is not None + + all_players = connection.execute('SELECT * FROM player WHERE is_approved = 1 ORDER BY score DESC').fetchall() + total_eliminations = sum(player_record['score'] for player_record in all_players) + alive_count = sum(1 for player_record in all_players if player_record['is_alive'] == 1) + connection.close() + + document_markup = get_markdown_rules() + + return render_template( + 'game.html', + player=player, + game_status=game_status, + start_time=start_time, + target_player=target_player, + pending_attacker_name=pending_attacker_name, + has_pending_kill_report=has_pending_kill_report, + all_players=all_players, + total_eliminations=total_eliminations, + alive_count=alive_count, + rules_markup=document_markup + ) -@application.route("/register", methods=["GET", "POST"]) +@application.route('/register', methods=['GET', 'POST']) def handle_registration(): - if request.method == "POST": - display_name = request.form.get("display_name") - password_input = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if password_input and password_input == password_confirmation: - new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) - hashed_password = argon2.hash(password_input) - connection = get_database_connection() - connection.execute("INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)", (new_identifier, hashed_password, display_name)) - connection.commit() - connection.close() - websocket_manager.emit("admin_state_changed", to="administrator_room") - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, registration_success=True, new_identifier=new_identifier) - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, register_mode=True) + if request.method == 'POST': + display_name = request.form.get('display_name') + password_input = request.form.get('password_string') + password_confirmation = request.form.get('password_string_confirmation') + if password_input and password_input == password_confirmation: + new_identifier = ''.join(secrets.choice(string.ascii_uppercase) for _ in range(IDENTIFIER_LENGTH)) + hashed_password = argon2.hash(password_input) + connection = get_database_connection() + connection.execute('INSERT INTO player (player_identifier, password_hash, display_name, is_approved) VALUES (?, ?, ?, 0)', (new_identifier, hashed_password, display_name)) + connection.commit() + connection.close() + websocket_manager.emit('admin_state_changed', to='administrator_room') + return render_template('login.html', identifier_length=IDENTIFIER_LENGTH, registration_success=True, new_identifier=new_identifier) + return render_template('login.html', identifier_length=IDENTIFIER_LENGTH, register_mode=True) -@application.route("/login", methods=["GET", "POST"]) +@application.route('/login', methods=['GET', 'POST']) def handle_login(): - if request.method == "POST": - identifier_input = request.form.get("player_identifier", "").strip().upper() - password_input = request.form.get("password_string", "") - connection = get_database_connection() - player = connection.execute("SELECT password_hash FROM player WHERE player_identifier = ?", (identifier_input,)).fetchone() - connection.close() - if player and argon2.verify(password_input, player["password_hash"]): - session["player_identifier"] = identifier_input - return redirect(url_for("handle_index")) - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH) + if request.method == 'POST': + identifier_input = request.form.get('player_identifier', '').strip().upper() + password_input = request.form.get('password_string', '') + connection = get_database_connection() + player = connection.execute('SELECT password_hash FROM player WHERE player_identifier = ?', (identifier_input,)).fetchone() + connection.close() + if player and argon2.verify(password_input, player['password_hash']): + session['player_identifier'] = identifier_input + return redirect(url_for('handle_index')) + return render_template('login.html', identifier_length=IDENTIFIER_LENGTH) -@application.route("/reset_password", methods=["GET", "POST"]) +@application.route('/reset_password', methods=['GET', 'POST']) def handle_password_reset(): - token = request.args.get("token") or request.form.get("token") - if not token: - abort(400) - connection = get_database_connection() - player = connection.execute("SELECT player_identifier FROM player WHERE reset_token = ?", (token,)).fetchone() - if not player: - connection.close() - abort(403) - if request.method == "POST": - new_password = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if new_password and new_password == password_confirmation: - hashed_password = argon2.hash(new_password) - connection.execute("UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?", (hashed_password, token)) - connection.commit() - connection.close() - return redirect(url_for("handle_login")) - connection.close() - return render_template("login.html", identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token) + token = request.args.get('token') or request.form.get('token') + if not token: + abort(400) + connection = get_database_connection() + player = connection.execute('SELECT player_identifier FROM player WHERE reset_token = ?', (token,)).fetchone() + if not player: + connection.close() + abort(403) + if request.method == 'POST': + new_password = request.form.get('password_string') + password_confirmation = request.form.get('password_string_confirmation') + if new_password and new_password == password_confirmation: + hashed_password = argon2.hash(new_password) + connection.execute('UPDATE player SET password_hash = ?, reset_token = NULL WHERE reset_token = ?', (hashed_password, token)) + connection.commit() + connection.close() + return redirect(url_for('handle_login')) + connection.close() + return render_template('login.html', identifier_length=IDENTIFIER_LENGTH, reset_mode=True, token=token) -@application.route("/player/action", methods=["POST"]) +@application.route('/player/action', methods=['POST']) @require_player_authentication def handle_player_action(): - action_type = request.form.get("action_type") - connection = get_database_connection() - if action_type == "request_name_change": - pending_name = request.form.get("pending_display_name") - connection.execute("UPDATE player SET pending_display_name = ? WHERE player_identifier = ?", (pending_name, session["player_identifier"])) - connection.commit() - websocket_manager.emit("admin_state_changed", to="administrator_room") - connection.close() - return redirect(url_for("handle_index")) + action_type = request.form.get('action_type') + connection = get_database_connection() + if action_type == 'request_name_change': + pending_name = request.form.get('pending_display_name') + connection.execute('UPDATE player SET pending_display_name = ? WHERE player_identifier = ?', (pending_name, session['player_identifier'])) + connection.commit() + websocket_manager.emit('admin_state_changed', to='administrator_room') + elif action_type == 'cancel_name_change': + connection.execute('UPDATE player SET pending_display_name = NULL WHERE player_identifier = ?', (session['player_identifier'],)) + connection.commit() + websocket_manager.emit('admin_state_changed', to='administrator_room') + connection.close() + return redirect(url_for('handle_index')) -@application.route("/logout") +@application.route('/logout') def handle_logout(): - session.clear() - return redirect(url_for("handle_login")) + session.clear() + return redirect(url_for('handle_login')) -@application.route("/admin", methods=["GET", "POST"]) +@application.route('/admin', methods=['GET', 'POST']) def handle_administrator(): - connection = get_database_connection() - administrator_record = connection.execute("SELECT * FROM administrator WHERE administrator_identifier = 1").fetchone() - - if not administrator_record: - if request.method == "POST": - password_input = request.form.get("password_string") - password_confirmation = request.form.get("password_string_confirmation") - if password_input and password_input == password_confirmation: - hashed_password = argon2.hash(password_input) - two_factor_secret = pyotp.random_base32() - connection.execute("INSERT INTO administrator (administrator_identifier, password_hash, two_factor_secret, two_factor_enabled) VALUES (1, ?, ?, 0)", (hashed_password, two_factor_secret)) - connection.commit() - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - connection.close() - return render_template("administrator.html", setup_required=True) - - if not session.get("administrator_authenticated"): - if request.method == "POST": - password_input = request.form.get("password_string") - if argon2.verify(password_input, administrator_record["password_hash"]): - if administrator_record["two_factor_enabled"]: - two_factor_input = request.form.get("two_factor_code") - time_based_one_time_password_validator = pyotp.TOTP(administrator_record["two_factor_secret"]) - if time_based_one_time_password_validator.verify(two_factor_input): - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - else: - session["administrator_authenticated"] = True - connection.close() - return redirect(url_for("handle_administrator")) - connection.close() - return render_template("administrator.html", login_required=True, two_factor_enabled=bool(administrator_record["two_factor_enabled"])) - - status_row = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - game_status = status_row["configuration_value"] - all_players = connection.execute("SELECT * FROM player ORDER BY is_approved ASC, display_name ASC").fetchall() - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1").fetchone() - can_start = alive_count_record["alive_count"] >= 2 - - kill_logs = connection.execute(""" - SELECT k.*, - killer.display_name as killer_name, - victim.display_name as victim_name - FROM kill_log k - LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier - LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier - ORDER BY k.kill_timestamp DESC - """).fetchall() - - two_factor_uri = pyotp.totp.TOTP(administrator_record["two_factor_secret"]).provisioning_uri(name="Administrator", issuer_name="AssassinGame") - qr_factory = qrcode.image.svg.SvgPathImage - qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory) - qr_stream = io.BytesIO() - qr_image.save(qr_stream) - qr_code_svg = qr_stream.getvalue().decode("utf-8") - - connection.close() - - return render_template( - "administrator.html", - dashboard_active=True, - game_status=game_status, - can_start=can_start, - all_players=all_players, - kill_logs=kill_logs, - two_factor_secret=administrator_record["two_factor_secret"], - two_factor_enabled=bool(administrator_record["two_factor_enabled"]), - qr_code_svg=qr_code_svg - ) + connection = get_database_connection() + administrator_record = connection.execute('SELECT * FROM administrator WHERE administrator_identifier = 1').fetchone() + + if not administrator_record: + if request.method == 'POST': + password_input = request.form.get('password_string') + password_confirmation = request.form.get('password_string_confirmation') + if password_input and password_input == password_confirmation: + hashed_password = argon2.hash(password_input) + two_factor_secret = pyotp.random_base32() + connection.execute('INSERT INTO administrator (administrator_identifier, password_hash, two_factor_secret, two_factor_enabled) VALUES (1, ?, ?, 0)', (hashed_password, two_factor_secret)) + connection.commit() + session['administrator_authenticated'] = True + connection.close() + return redirect(url_for('handle_administrator')) + connection.close() + return render_template('administrator.html', setup_required=True) + + if not session.get('administrator_authenticated'): + if request.method == 'POST': + password_input = request.form.get('password_string') + if argon2.verify(password_input, administrator_record['password_hash']): + if administrator_record['two_factor_enabled']: + two_factor_input = request.form.get('two_factor_code') + time_based_one_time_password_validator = pyotp.TOTP(administrator_record['two_factor_secret']) + if time_based_one_time_password_validator.verify(two_factor_input): + session['administrator_authenticated'] = True + connection.close() + return redirect(url_for('handle_administrator')) + else: + session['administrator_authenticated'] = True + connection.close() + return redirect(url_for('handle_administrator')) + connection.close() + return render_template('administrator.html', login_required=True, two_factor_enabled=bool(administrator_record['two_factor_enabled'])) + + status_row = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + game_status = status_row['configuration_value'] + all_players = connection.execute('SELECT * FROM player ORDER BY is_approved ASC, display_name ASC').fetchall() + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_approved = 1').fetchone() + can_start = alive_count_record['alive_count'] >= 2 + + kill_logs = connection.execute(''' + SELECT k.*, + killer.display_name as killer_name, + victim.display_name as victim_name + FROM kill_log k + LEFT JOIN player killer ON k.killer_identifier = killer.player_identifier + LEFT JOIN player victim ON k.victim_identifier = victim.player_identifier + ORDER BY k.kill_timestamp DESC + ''').fetchall() + + two_factor_uri = pyotp.totp.TOTP(administrator_record['two_factor_secret']).provisioning_uri(name='Administrator', issuer_name='AssassinGame') + qr_factory = qrcode.image.svg.SvgPathImage + qr_image = qrcode.make(two_factor_uri, image_factory=qr_factory) + qr_stream = io.BytesIO() + qr_image.save(qr_stream) + qr_code_svg = qr_stream.getvalue().decode('utf-8') + + connection.close() + + return render_template( + 'administrator.html', + dashboard_active=True, + game_status=game_status, + can_start=can_start, + all_players=all_players, + kill_logs=kill_logs, + two_factor_secret=administrator_record['two_factor_secret'], + two_factor_enabled=bool(administrator_record['two_factor_enabled']), + qr_code_svg=qr_code_svg + ) -@application.route("/admin/action", methods=["POST"]) +@application.route('/admin/action', methods=['POST']) @require_administrator_authentication def handle_administrator_action(): - form_data = request.form.to_dict() - action_type = form_data.get("action_type") - execution_time_string = form_data.get("execution_time") - - if execution_time_string: - execution_datetime = datetime.strptime(execution_time_string[:16], "%Y-%m-%dT%H:%M").replace(tzinfo=european_timezone) - scheduler_instance.add_job(scheduled_action_wrapper, DateTrigger(run_date=execution_datetime), args=[action_type, form_data]) - else: - process_administrator_action(action_type, form_data) - - return redirect(url_for("handle_administrator")) + form_data = request.form.to_dict() + action_type = form_data.get('action_type') + execution_time_string = form_data.get('execution_time') + + if execution_time_string: + execution_datetime = datetime.strptime(execution_time_string[:16], '%Y-%m-%dT%H:%M').replace(tzinfo=european_timezone) + scheduler_instance.add_job(scheduled_action_wrapper, DateTrigger(run_date=execution_datetime), args=[action_type, form_data]) + else: + process_administrator_action(action_type, form_data) + + return redirect(url_for('handle_administrator')) -@websocket_manager.on("connect") +@websocket_manager.on('connect') def handle_websocket_connection(): - if "player_identifier" in session: - join_room(session["player_identifier"]) - if session.get("administrator_authenticated"): - join_room("administrator_room") + if 'player_identifier' in session: + join_room(session['player_identifier']) + if session.get('administrator_authenticated'): + join_room('administrator_room') -@websocket_manager.on("request_elimination") +@websocket_manager.on('request_elimination') def handle_elimination_request(): - attacker_identifier = session.get("player_identifier") - if not attacker_identifier: - return - connection = get_database_connection() - attacker = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (attacker_identifier,)).fetchone() - if attacker and attacker["target_identifier"]: - victim_identifier = attacker["target_identifier"] - victim = connection.execute("SELECT target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - - connection.execute("UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?", (attacker_identifier, victim_identifier)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, 'pending')", (attacker_identifier, victim_identifier, victim["target_identifier"], timestamp)) - - connection.commit() - emit("elimination_verification_requested", to=victim_identifier) - websocket_manager.emit("admin_state_changed", to="administrator_room") - connection.close() + attacker_identifier = session.get('player_identifier') + if not attacker_identifier: + return + connection = get_database_connection() + attacker = connection.execute('SELECT target_identifier FROM player WHERE player_identifier = ?', (attacker_identifier,)).fetchone() + if attacker and attacker['target_identifier']: + victim_identifier = attacker['target_identifier'] + victim = connection.execute('SELECT target_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone() + + connection.execute('UPDATE player SET pending_attacker_identifier = ? WHERE player_identifier = ?', (attacker_identifier, victim_identifier)) + + timestamp = datetime.now(european_timezone).isoformat() + connection.execute('INSERT INTO kill_log (killer_identifier, victim_identifier, victim_previous_target, kill_timestamp, report_status) VALUES (?, ?, ?, ?, "pending")', (attacker_identifier, victim_identifier, victim['target_identifier'], timestamp)) + + connection.commit() + emit('elimination_verification_requested', to=victim_identifier) + websocket_manager.emit('game_state_changed', to=attacker_identifier) + websocket_manager.emit('admin_state_changed', to='administrator_room') + connection.close() -@websocket_manager.on("confirm_elimination") +@websocket_manager.on('revoke_elimination_request') +def handle_elimination_revocation(): + attacker_identifier = session.get('player_identifier') + if not attacker_identifier: + return + connection = get_database_connection() + victim = connection.execute('SELECT player_identifier FROM player WHERE pending_attacker_identifier = ?', (attacker_identifier,)).fetchone() + if victim: + connection.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim['player_identifier'],)) + connection.execute('DELETE FROM kill_log WHERE killer_identifier = ? AND report_status = "pending"', (attacker_identifier,)) + connection.commit() + emit('elimination_revoked', to=victim['player_identifier']) + websocket_manager.emit('game_state_changed', to=attacker_identifier) + websocket_manager.emit('admin_state_changed', to='administrator_room') + connection.close() + +@websocket_manager.on('confirm_elimination') def handle_elimination_confirmation(): - victim_identifier = session.get("player_identifier") - if not victim_identifier: - return - connection = get_database_connection() - victim = connection.execute("SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - if victim and victim["pending_attacker_identifier"]: - attacker_identifier = victim["pending_attacker_identifier"] - new_target = victim["target_identifier"] - - connection.execute("UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,)) - connection.execute("UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?", (new_target, attacker_identifier)) - - timestamp = datetime.now(european_timezone).isoformat() - connection.execute("UPDATE kill_log SET report_status = 'confirmed', kill_timestamp = ? WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = 'pending'", (timestamp, attacker_identifier, victim_identifier)) - - alive_count_record = connection.execute("SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1").fetchone() - if alive_count_record["alive_count"] <= 1: - status_record = connection.execute("SELECT configuration_value FROM game_configuration WHERE configuration_key = 'game_status'").fetchone() - current_status = status_record["configuration_value"] if status_record else "unknown" - connection.execute("UPDATE game_configuration SET configuration_value = 'finished' WHERE configuration_key = 'game_status'") - - if alive_count_record["alive_count"] == 1 and current_status != 'finished': - connection.execute("UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1") - - connection.commit() - websocket_manager.emit("game_state_changed") - websocket_manager.emit("admin_state_changed", to="administrator_room") - connection.close() + victim_identifier = session.get('player_identifier') + if not victim_identifier: + return + connection = get_database_connection() + victim = connection.execute('SELECT pending_attacker_identifier, target_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone() + if victim and victim['pending_attacker_identifier']: + attacker_identifier = victim['pending_attacker_identifier'] + new_target = victim['target_identifier'] + + connection.execute('UPDATE player SET is_alive = 0, target_identifier = NULL, pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim_identifier,)) + connection.execute('UPDATE player SET target_identifier = ?, score = score + 1 WHERE player_identifier = ?', (new_target, attacker_identifier)) + + timestamp = datetime.now(european_timezone).isoformat() + connection.execute('UPDATE kill_log SET report_status = "confirmed", kill_timestamp = ? WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = "pending"', (timestamp, attacker_identifier, victim_identifier)) + + alive_count_record = connection.execute('SELECT COUNT(*) as alive_count FROM player WHERE is_alive = 1 AND is_approved = 1').fetchone() + if alive_count_record['alive_count'] <= 1: + status_record = connection.execute('SELECT configuration_value FROM game_configuration WHERE configuration_key = "game_status"').fetchone() + current_status = status_record['configuration_value'] if status_record else 'unknown' + connection.execute('UPDATE game_configuration SET configuration_value = "finished" WHERE configuration_key = "game_status"') + + if alive_count_record['alive_count'] == 1 and current_status != 'finished': + connection.execute('UPDATE player SET score = score + 3 WHERE is_alive = 1 AND is_approved = 1') + + connection.commit() + websocket_manager.emit('game_state_changed') + websocket_manager.emit('admin_state_changed', to='administrator_room') + connection.close() -@websocket_manager.on("reject_elimination") +@websocket_manager.on('reject_elimination') def handle_elimination_rejection(): - victim_identifier = session.get("player_identifier") - if not victim_identifier: - return - connection = get_database_connection() - victim = connection.execute("SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?", (victim_identifier,)).fetchone() - if victim and victim["pending_attacker_identifier"]: - attacker_identifier = victim["pending_attacker_identifier"] - connection.execute("UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?", (victim_identifier,)) - connection.execute("UPDATE kill_log SET report_status = 'rejected' WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = 'pending'", (attacker_identifier, victim_identifier)) - connection.commit() - emit("elimination_rejected", to=attacker_identifier) - websocket_manager.emit("admin_state_changed", to="administrator_room") - connection.close() + victim_identifier = session.get('player_identifier') + if not victim_identifier: + return + connection = get_database_connection() + victim = connection.execute('SELECT pending_attacker_identifier FROM player WHERE player_identifier = ?', (victim_identifier,)).fetchone() + if victim and victim['pending_attacker_identifier']: + attacker_identifier = victim['pending_attacker_identifier'] + connection.execute('UPDATE player SET pending_attacker_identifier = NULL WHERE player_identifier = ?', (victim_identifier,)) + connection.execute('UPDATE kill_log SET report_status = "rejected" WHERE killer_identifier = ? AND victim_identifier = ? AND report_status = "pending"', (attacker_identifier, victim_identifier)) + connection.commit() + emit('elimination_rejected', to=attacker_identifier) + websocket_manager.emit('game_state_changed', to=victim_identifier) + websocket_manager.emit('admin_state_changed', to='administrator_room') + connection.close() -if __name__ == "__main__": - initialize_database() - scheduler_instance.start() - websocket_manager.run(application, host="0.0.0.0", port=5000) \ No newline at end of file +if __name__ == '__main__': + initialize_database() + scheduler_instance.start() + websocket_manager.run(application, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/bundle_project.py b/bundle_project.py new file mode 100644 index 0000000..eaa64c7 --- /dev/null +++ b/bundle_project.py @@ -0,0 +1,87 @@ +import pathlib + +def is_item_ignored(item_name, ignored_directories_list, ignored_extensions_list): + if item_name in ignored_directories_list: + return True + for extension_string in ignored_extensions_list: + if item_name.endswith(extension_string): + return True + return False + +def format_tree_line(entry_name, prefix_string, is_last_element): + connector_string = '└── ' if is_last_element else '├── ' + return prefix_string + connector_string + entry_name + +def format_file_section(relative_path_string, file_content_string): + header_string = '\n--- FILE START: ' + relative_path_string + ' ---\n' + footer_string = '\n--- FILE END: ' + relative_path_string + ' ---\n' + return header_string + file_content_string + footer_string + +def build_final_output_string(tree_lines_list, code_contents_list): + tree_string = '\n'.join(tree_lines_list) + code_string = ''.join(code_contents_list) + return 'PROJECT STRUCTURE:\n' + tree_string + '\n\n' + code_string + +def collect_file_system_nodes(root_path, ignored_directories_list, ignored_extensions_list): + valid_nodes_list = [] + try: + directory_items_list = list(root_path.iterdir()) + except PermissionError: + return valid_nodes_list + + for item_path in directory_items_list: + if not is_item_ignored(item_path.name, ignored_directories_list, ignored_extensions_list): + valid_nodes_list.append(item_path) + + valid_nodes_list.sort(key=lambda path_object: (not path_object.is_dir(), path_object.name.lower())) + return valid_nodes_list + +def read_file_content_safely(file_path_object): + try: + return file_path_object.read_text(encoding='utf-8') + except UnicodeDecodeError: + return '[BINARY_OR_UNSUPPORTED_ENCODING]' + except PermissionError: + return '[PERMISSION_DENIED]' + +def write_output_file(output_file_path, content_string): + output_file_path.write_text(content_string, encoding='utf-8') + +def process_directory_recursively(current_path, base_directory_path, prefix_string, ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list): + nodes_list = collect_file_system_nodes(current_path, ignored_directories_list, ignored_extensions_list) + total_nodes_count = len(nodes_list) + + for index_number, node_path in enumerate(nodes_list): + is_last_node = index_number == total_nodes_count - 1 + formatted_tree_line = format_tree_line(node_path.name, prefix_string, is_last_node) + tree_lines_list.append(formatted_tree_line) + + if node_path.is_dir(): + new_prefix_string = prefix_string + (' ' if is_last_node else '│ ') + process_directory_recursively(node_path, base_directory_path, new_prefix_string, ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list) + else: + relative_file_path_string = str(node_path.relative_to(base_directory_path)) + file_content_string = read_file_content_safely(node_path) + formatted_file_section = format_file_section(relative_file_path_string, file_content_string) + code_contents_list.append(formatted_file_section) + +def execute_bundler_script(): + target_output_file_name_string = 'output_fuer_ki.txt' + ignored_directories_list = ['.git', '__pycache__', 'venv', 'env', '.venv', 'node_modules', '.idea', '.vscode', 'instance'] + ignored_extensions_list = ['.pyc', '.sqlite', '.db', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.exe', '.pdf', '.zip', '.tar.gz', '.mp3', '.mp4', target_output_file_name_string] + + current_working_directory_path = pathlib.Path.cwd() + output_file_path = current_working_directory_path / target_output_file_name_string + + tree_lines_list = [current_working_directory_path.name] + code_contents_list = [] + + process_directory_recursively(current_working_directory_path, current_working_directory_path, '', ignored_directories_list, ignored_extensions_list, tree_lines_list, code_contents_list) + + final_output_string = build_final_output_string(tree_lines_list, code_contents_list) + write_output_file(output_file_path, final_output_string) + + print('Erfolg! Die Datei ' + target_output_file_name_string + ' wurde generiert.') + +if __name__ == '__main__': + execute_bundler_script() \ No newline at end of file diff --git a/game_database.sqlite b/game_database.sqlite index baf8ef262a32422e3cf2dbaba5d12899a22bee74..2f6a587ca93e304eb1ec5745dd8d12583bb19cb9 100644 GIT binary patch delta 1152 zcmb`F&yV6{9LC$CKq>I;#q7I+0(w(She z9yT*;^x#P)@uJ3LuikdDm%XfBy!$73T$7E5J@_Lrv6RD-=-#}%zrOGLd7kHe-_uj< z^c4GCS^Mn85cmV}5mymFg7eTM{A=)Q;!EP{`Na!A1?KHQ1NVA;{iHbmGx(NU*$zdiX9HAdO-AyGq|0M%1^Z3t)v9yI>KdjNDOLpT8ds0xxk9#C z79y0JZJFgiW1E_#*^+GxHGPE{3%~9fYKJ#3FAQnA3iZD93@v>mk?>!y>llaJJ5 zYF6k+Q-z_)WLhIDBW4fl8D+*dclYV+a6n(r)aip3)Ki1nO|e;?ruQ95gRkfJ8;j;P zwiH511aXAi^@Wg3D^{y-=Tdg2of^z?u#%I9nQWAu9*gO5hba{Xh9D?CtJ!E*(lz=> ztF?H8IaK-wySZFSp=(A%9*^bcT;gmQ$M1LIuf}QoPjLbc7XyzG@H=>Xe(}R!zzevC zL|tjj?;&yb{_fRo1Ju6=H<~7)z{3tF7nxZPrbqQ^KXW*ZCMGR@R2pz6%2*L&<$AHi z-cUOop&65lw>aKVZuP2_o4J$ghiN)lVNxR7zQG82UdVP@952?yc8L~6fytK(A;%F4 z9wOkO^P=}b=?dyYNRs3?T~n?fcl59SlBDCw48x|A%m30f^d2bYEiVb8Y)BY{G&b@d=b1Dzb|{xP+6 z>UOtqS~SyV}lKROFIWZfd+CDRREW*P6d3^Lu}$79D+fCS>k;hs{?Y z=z$=}0p391BY3kr=z9q!Fo*L5X0458HE1o+5!g8~Qnm)1w`xXH?=-jjeH4J1~Sead^KCN52zC_6?sUZUPwT*jAKms+q`%SAw$EYONO8<#IC0 zM*VAKZjA{G;pN5fYD-`_iU_5ob7u^_NyRR5nT2&aB`s`}VhmYM&t_vQX@Xfvo~7a( iHOE|vCzxy|orsakG#OvStVGa&mE}EnaDTYZe*Fo2+9ezS diff --git a/static/behavior.js b/static/behavior.js index 6c63521..03f6fff 100644 --- a/static/behavior.js +++ b/static/behavior.js @@ -1,330 +1,415 @@ -let websocket_connection; -let pending_administrator_form = null; -let stopwatch_interval_identifier = null; +const application_state = { + websocket_connection: null, + pending_administrator_form: null, + stopwatch_interval_identifier: null +}; -function initialize_lucide_icons() { - if (typeof lucide !== "undefined") { - lucide.createIcons(); - } +/* --- Deterministische Funktionen ("ehrliche" Funktionen) --- */ + +function calculate_time_difference(start_time_milliseconds, current_time_milliseconds) { + return current_time_milliseconds - start_time_milliseconds; } -function update_game_stopwatch() { - const stopwatch_element = document.getElementById("game-stopwatch"); - if (!stopwatch_element) { - return; - } +function format_stopwatch_string(difference_in_milliseconds) { + if (difference_in_milliseconds < 0) { + return '00:00 Minuten'; + } - const start_time_string = stopwatch_element.getAttribute("data-start-time"); - if (!start_time_string) { - return; - } + const total_seconds = Math.floor(difference_in_milliseconds / 1000); + const days_passed = Math.floor(total_seconds / 86400); + const hours_passed = Math.floor((total_seconds % 86400) / 3600); + const minutes_passed = Math.floor((total_seconds % 3600) / 60); + const seconds_passed = total_seconds % 60; - const start_date = new Date(start_time_string); - if (isNaN(start_date.getTime())) { - return; - } + let output_string = ''; - const current_date = new Date(); - const difference_in_milliseconds = current_date.getTime() - start_date.getTime(); + if (days_passed === 1) { + output_string += '1 Tag und '; + } else if (days_passed > 1) { + output_string += days_passed + ' Tage und '; + } - if (difference_in_milliseconds < 0) { - stopwatch_element.textContent = "00:00 Minuten"; - return; - } + const padded_minutes = String(minutes_passed).padStart(2, '0'); + const padded_seconds = String(seconds_passed).padStart(2, '0'); - const total_seconds = Math.floor(difference_in_milliseconds / 1000); - const days_passed = Math.floor(total_seconds / 86400); - const hours_passed = Math.floor((total_seconds % 86400) / 3600); - const minutes_passed = Math.floor((total_seconds % 3600) / 60); - const seconds_passed = total_seconds % 60; + if (hours_passed === 1) { + output_string += '1:' + padded_minutes + ':' + padded_seconds + ' Stunde'; + } else if (hours_passed > 1) { + output_string += hours_passed + ':' + padded_minutes + ':' + padded_seconds + ' Stunden'; + } else { + output_string += padded_minutes + ':' + padded_seconds + ' Minuten'; + } - let output_string = ""; - - if (days_passed === 1) { - output_string += "1 Tag und "; - } else if (days_passed > 1) { - output_string += days_passed + " Tage und "; - } - - const padded_minutes = String(minutes_passed).padStart(2, "0"); - const padded_seconds = String(seconds_passed).padStart(2, "0"); - - if (hours_passed === 1) { - output_string += "1:" + padded_minutes + ":" + padded_seconds + " Stunde"; - } else if (hours_passed > 1) { - output_string += hours_passed + ":" + padded_minutes + ":" + padded_seconds + " Stunden"; - } else { - output_string += padded_minutes + ":" + padded_seconds + " Minuten"; - } - - stopwatch_element.textContent = output_string; + return output_string; } -function initialize_application_behavior() { - initialize_lucide_icons(); - - if (stopwatch_interval_identifier) { - clearInterval(stopwatch_interval_identifier); - } - stopwatch_interval_identifier = setInterval(update_game_stopwatch, 1000); - update_game_stopwatch(); - - const tab_buttons = document.querySelectorAll(".tab-button"); - tab_buttons.forEach(function(current_button) { - current_button.addEventListener("click", function() { - const target_identifier = current_button.getAttribute("data-target"); - const parent_container = current_button.closest(".application-content"); - - parent_container.querySelectorAll(".tab-button").forEach(function(button_element) { - button_element.classList.remove("active-tab"); - }); - parent_container.querySelectorAll(".tab-content").forEach(function(content_element) { - content_element.classList.remove("active-content"); - }); - - current_button.classList.add("active-tab"); - const target_panel = document.getElementById(target_identifier); - if (target_panel) { - target_panel.classList.add("active-content"); - } - }); - }); +function synchronize_element_nodes(current_node, updated_node) { + if (current_node.nodeType !== updated_node.nodeType) { + current_node.replaceWith(updated_node.cloneNode(true)); + return; + } + if (current_node.nodeType === Node.TEXT_NODE) { + if (current_node.nodeValue !== updated_node.nodeValue) { + current_node.nodeValue = updated_node.nodeValue; + } + return; + } + if (current_node.nodeType !== Node.ELEMENT_NODE) { + return; + } - const player_forms = document.querySelectorAll(".asynchronous-form"); - player_forms.forEach(function(current_form) { - current_form.addEventListener("submit", function(form_submission_event) { - form_submission_event.preventDefault(); - execute_form_submission(current_form); - }); - }); + if (current_node.tagName !== updated_node.tagName) { + current_node.replaceWith(updated_node.cloneNode(true)); + return; + } + + if (current_node.namespaceURI !== updated_node.namespaceURI) { + current_node.replaceWith(updated_node.cloneNode(true)); + return; + } - const administrator_forms = document.querySelectorAll(".admin-asynchronous-form"); - administrator_forms.forEach(function(current_form) { - current_form.addEventListener("submit", function(form_submission_event) { - form_submission_event.preventDefault(); - pending_administrator_form = current_form; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - const time_input = document.getElementById("modal_execution_time"); - if (time_input) { - time_input.value = ""; - } - administrator_modal.classList.add("active-modal"); - } - }); - }); + const is_active_input = document.activeElement === current_node; + const is_input_element = ['INPUT', 'TEXTAREA', 'SELECT'].includes(current_node.tagName); - const confirm_administrator_button = document.getElementById("confirm-admin-action"); - if (confirm_administrator_button) { - confirm_administrator_button.addEventListener("click", function() { - if (pending_administrator_form) { - const time_input = document.getElementById("modal_execution_time"); - if (time_input && time_input.value) { - let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); - if (!hidden_time_input) { - hidden_time_input = document.createElement("input"); - hidden_time_input.type = "hidden"; - hidden_time_input.name = "execution_time"; - pending_administrator_form.appendChild(hidden_time_input); - } - hidden_time_input.value = time_input.value; - } else { - let hidden_time_input = pending_administrator_form.querySelector("input[name='execution_time']"); - if (hidden_time_input) { - hidden_time_input.remove(); - } - } - execute_form_submission(pending_administrator_form); - pending_administrator_form = null; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - administrator_modal.classList.remove("active-modal"); - } - } - }); - } + if (is_input_element && !is_active_input) { + if (current_node.value !== updated_node.value) { + current_node.value = updated_node.value; + } + } - const cancel_administrator_button = document.getElementById("cancel-admin-action"); - if (cancel_administrator_button) { - cancel_administrator_button.addEventListener("click", function() { - pending_administrator_form = null; - const administrator_modal = document.getElementById("admin-confirmation-modal"); - if (administrator_modal) { - administrator_modal.classList.remove("active-modal"); - } - }); - } + const current_attributes = current_node.attributes; + const updated_attributes = updated_node.attributes; - const trigger_elimination_button = document.getElementById("trigger-elimination"); - if (trigger_elimination_button) { - trigger_elimination_button.addEventListener("click", function() { - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.add("active-modal"); - } - }); - } + for (let index = 0; index < updated_attributes.length; index++) { + const attribute = updated_attributes[index]; + if (current_node.getAttribute(attribute.name) !== attribute.value) { + if (!(is_input_element && is_active_input && attribute.name === 'value')) { + current_node.setAttribute(attribute.name, attribute.value); + } + } + } - const proceed_elimination_button = document.getElementById("proceed-elimination"); - if (proceed_elimination_button) { - proceed_elimination_button.addEventListener("click", function() { - websocket_connection.emit("request_elimination"); - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.remove("active-modal"); - } - const waiting_modal = document.getElementById("waiting-modal"); - if (waiting_modal) { - waiting_modal.classList.add("active-modal"); - } - }); - } + for (let index = current_attributes.length - 1; index >= 0; index--) { + const attribute = current_attributes[index]; + if (!updated_node.hasAttribute(attribute.name)) { + current_node.removeAttribute(attribute.name); + } + } - const cancel_elimination_button = document.getElementById("cancel-elimination"); - if (cancel_elimination_button) { - cancel_elimination_button.addEventListener("click", function() { - const elimination_modal = document.getElementById("elimination-confirm-modal"); - if (elimination_modal) { - elimination_modal.classList.remove("active-modal"); - } - }); - } + const current_children = Array.from(current_node.childNodes); + const updated_children = Array.from(updated_node.childNodes); + const max_length = Math.max(current_children.length, updated_children.length); - const confirm_button = document.getElementById("confirm-elimination"); - if (confirm_button) { - confirm_button.addEventListener("click", function() { - websocket_connection.emit("confirm_elimination"); - const modal_element = document.getElementById("verification-modal"); - if (modal_element) { - modal_element.classList.remove("active-modal"); - } - }); - } + for (let index = 0; index < max_length; index++) { + const current_child = current_children[index]; + const updated_child = updated_children[index]; - const reject_button = document.getElementById("reject-elimination"); - if (reject_button) { - reject_button.addEventListener("click", function() { - websocket_connection.emit("reject_elimination"); - const modal_element = document.getElementById("verification-modal"); - if (modal_element) { - modal_element.classList.remove("active-modal"); - } - }); - } - - const acknowledge_rejection_button = document.getElementById("acknowledge-rejection"); - if (acknowledge_rejection_button) { - acknowledge_rejection_button.addEventListener("click", function() { - const rejection_modal = document.getElementById("rejection-modal"); - if (rejection_modal) { - rejection_modal.classList.remove("active-modal"); - } - }); - } - - const open_name_modal_button = document.getElementById("trigger-name-change-modal"); - if (open_name_modal_button) { - open_name_modal_button.addEventListener("click", function() { - const name_modal = document.getElementById("name-change-modal"); - if (name_modal) { - name_modal.classList.add("active-modal"); - } - }); - } - - const close_name_modal_button = document.getElementById("cancel-name-change"); - if (close_name_modal_button) { - close_name_modal_button.addEventListener("click", function() { - const name_modal = document.getElementById("name-change-modal"); - if (name_modal) { - name_modal.classList.remove("active-modal"); - } - }); - } + if (!current_child && updated_child) { + current_node.appendChild(updated_child.cloneNode(true)); + } else if (current_child && !updated_child) { + current_node.removeChild(current_child); + } else if (current_child && updated_child) { + synchronize_element_nodes(current_child, updated_child); + } + } } -function execute_form_submission(target_form) { - const active_tab_element = document.querySelector(".active-tab"); - let active_target = "control-panel"; - if (active_tab_element) { - active_target = active_tab_element.getAttribute("data-target"); - } - - fetch(target_form.action, { - method: target_form.method, - body: new FormData(target_form) - }) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - const new_tab_button = document.querySelector(".tab-button[data-target='" + active_target + "']"); - if (new_tab_button) { - new_tab_button.click(); - } - }); +/* --- Architektur & State-Management ("unehrliche" Funktionen) --- */ + +function trigger_dom_update(markup_text) { + const document_parser = new DOMParser(); + const parsed_document = document_parser.parseFromString(markup_text, 'text/html'); + const current_container = document.querySelector('.application-content'); + const new_container = parsed_document.querySelector('.application-content'); + + if (!current_container || !new_container) { + return; + } + + const current_active_tab = document.querySelector('.active-tab'); + const active_tab_target = current_active_tab ? current_active_tab.getAttribute('data-target') : null; + + const active_modals = []; + document.querySelectorAll('.active-modal').forEach(function(modal) { + active_modals.push(modal.id); + }); + + synchronize_element_nodes(current_container, new_container); + + if (active_tab_target) { + document.querySelectorAll('.tab-button').forEach(function(button) { button.classList.remove('active-tab'); }); + document.querySelectorAll('.tab-content').forEach(function(content) { content.classList.remove('active-content'); }); + + const restored_tab = document.querySelector('.tab-button[data-target="' + active_tab_target + '"]'); + const restored_content = document.getElementById(active_tab_target); + if (restored_tab && restored_content) { + restored_tab.classList.add('active-tab'); + restored_content.classList.add('active-content'); + } + } + + active_modals.forEach(function(modal_identifier) { + const restored_modal = document.getElementById(modal_identifier); + if (restored_modal) { + restored_modal.classList.add('active-modal'); + } + }); + + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } } -function replace_main_content(markup_text) { - const document_parser = new DOMParser(); - const parsed_document = document_parser.parseFromString(markup_text, "text/html"); - const current_container = document.querySelector(".application-content"); - const new_container = parsed_document.querySelector(".application-content"); - if (current_container && new_container) { - current_container.innerHTML = new_container.innerHTML; - initialize_application_behavior(); - } +function fetch_and_synchronize_dom() { + fetch(window.location.href) + .then(function(network_response) { + if (!network_response.ok) { + throw new Error('Network response failure'); + } + return network_response.text(); + }) + .then(function(markup_text) { + trigger_dom_update(markup_text); + }) + .catch(function(error_object) { + console.error('DOM Synchronization failed:', error_object); + }); } -document.addEventListener("DOMContentLoaded", function() { - if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("/service_worker.js"); - } +function execute_asynchronous_form_submission(target_form) { + fetch(target_form.action, { + method: target_form.method, + body: new FormData(target_form) + }) + .then(function(network_response) { + if (!network_response.ok) { + throw new Error('Form submission failure'); + } + return network_response.text(); + }) + .then(function(markup_text) { + trigger_dom_update(markup_text); + }) + .catch(function(error_object) { + console.error('Form execution failed:', error_object); + }); +} - if (typeof io !== "undefined") { - websocket_connection = io(); - - websocket_connection.on("game_state_changed", function() { - if (document.querySelector(".game-section") || document.querySelector(".administrator-section")) { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - }); - } - }); +function update_game_stopwatch_ui() { + const stopwatch_element = document.getElementById('game-stopwatch'); + if (!stopwatch_element) { + return; + } - websocket_connection.on("admin_state_changed", function() { - if (document.querySelector(".administrator-section")) { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - }); - } - }); + const start_time_string = stopwatch_element.getAttribute('data-start-time'); + if (!start_time_string) { + return; + } - websocket_connection.on("elimination_verification_requested", function() { - fetch(window.location.href) - .then(function(network_response) { return network_response.text(); }) - .then(function(markup_text) { - replace_main_content(markup_text); - const new_modal_element = document.getElementById("verification-modal"); - if (new_modal_element) { - new_modal_element.classList.add("active-modal"); - } - }); - }); + const start_date = new Date(start_time_string); + if (isNaN(start_date.getTime())) { + return; + } - websocket_connection.on("elimination_rejected", function() { - const waiting_modal = document.getElementById("waiting-modal"); - if (waiting_modal) { - waiting_modal.classList.remove("active-modal"); - } - const rejection_modal = document.getElementById("rejection-modal"); - if (rejection_modal) { - rejection_modal.classList.add("active-modal"); - } - }); - } - initialize_application_behavior(); + const current_date = new Date(); + const difference = calculate_time_difference(start_date.getTime(), current_date.getTime()); + stopwatch_element.textContent = format_stopwatch_string(difference); +} + +function modify_modal_state(modal_identifier, is_active) { + const modal = document.getElementById(modal_identifier); + if (modal) { + if (is_active) { + modal.classList.add('active-modal'); + } else { + modal.classList.remove('active-modal'); + } + } +} + +function write_text_to_clipboard(text_payload) { + if (!navigator.clipboard) { + console.error('Clipboard API unvailable.'); + return; + } + navigator.clipboard.writeText(text_payload).catch(function(error_object) { + console.error('Clipboard action failed:', error_object); + }); +} + +/* --- Globale Event-Delegation --- */ + +function handle_global_clicks(mouse_event) { + const target_element = mouse_event.target; + + const tab_button = target_element.closest('.tab-button'); + if (tab_button) { + const target_identifier = tab_button.getAttribute('data-target'); + const parent_container = tab_button.closest('.application-content'); + + parent_container.querySelectorAll('.tab-button').forEach(function(button) { + button.classList.remove('active-tab'); + }); + parent_container.querySelectorAll('.tab-content').forEach(function(content) { + content.classList.remove('active-content'); + }); + + tab_button.classList.add('active-tab'); + const target_panel = document.getElementById(target_identifier); + if (target_panel) { + target_panel.classList.add('active-content'); + } + return; + } + + const action_mapping = { + 'trigger-elimination': function() { modify_modal_state('elimination-confirm-modal', true); }, + 'cancel-elimination': function() { modify_modal_state('elimination-confirm-modal', false); }, + 'proceed-elimination': function() { + if (application_state.websocket_connection) { + application_state.websocket_connection.emit('request_elimination'); + } + modify_modal_state('elimination-confirm-modal', false); + }, + 'trigger-revoke-elimination': function() { + if (application_state.websocket_connection) { + application_state.websocket_connection.emit('revoke_elimination_request'); + } + }, + 'confirm-elimination': function() { + if (application_state.websocket_connection) { + application_state.websocket_connection.emit('confirm_elimination'); + } + modify_modal_state('verification-modal', false); + }, + 'reject-elimination': function() { + if (application_state.websocket_connection) { + application_state.websocket_connection.emit('reject_elimination'); + } + modify_modal_state('verification-modal', false); + }, + 'acknowledge-rejection': function() { modify_modal_state('rejection-modal', false); }, + 'trigger-name-change-modal': function() { modify_modal_state('name-change-modal', true); }, + 'cancel-name-change-modal': function() { modify_modal_state('name-change-modal', false); }, + 'trigger-help-modal': function() { modify_modal_state('help-modal', true); }, + 'close-help-modal': function() { modify_modal_state('help-modal', false); }, + 'copy-identifier-button': function(button_element) { + const identifier = button_element.getAttribute('data-identifier'); + write_text_to_clipboard(identifier); + const icon = button_element.querySelector('i, svg'); + if (icon) { + icon.setAttribute('data-lucide', 'check'); + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } + }, + 'confirm-admin-action': function() { + if (application_state.pending_administrator_form) { + const time_input = document.getElementById('modal_execution_time'); + if (time_input && time_input.value) { + let hidden_input = application_state.pending_administrator_form.querySelector('input[name="execution_time"]'); + if (!hidden_input) { + hidden_input = document.createElement('input'); + hidden_input.type = 'hidden'; + hidden_input.name = 'execution_time'; + application_state.pending_administrator_form.appendChild(hidden_input); + } + hidden_input.value = time_input.value; + } else { + const hidden_input = application_state.pending_administrator_form.querySelector('input[name="execution_time"]'); + if (hidden_input) { + hidden_input.remove(); + } + } + execute_asynchronous_form_submission(application_state.pending_administrator_form); + application_state.pending_administrator_form = null; + modify_modal_state('admin-confirmation-modal', false); + } + }, + 'cancel-admin-action': function() { + application_state.pending_administrator_form = null; + modify_modal_state('admin-confirmation-modal', false); + } + }; + + for (const [identifier, action_function] of Object.entries(action_mapping)) { + const button = target_element.closest('#' + identifier); + if (button) { + action_function(button); + return; + } + } +} + +function handle_global_submits(submit_event) { + const target_form = submit_event.target; + + if (target_form.classList.contains('asynchronous-form')) { + submit_event.preventDefault(); + execute_asynchronous_form_submission(target_form); + } else if (target_form.classList.contains('admin-asynchronous-form')) { + submit_event.preventDefault(); + application_state.pending_administrator_form = target_form; + const time_input = document.getElementById('modal_execution_time'); + if (time_input) { + time_input.value = ''; + } + modify_modal_state('admin-confirmation-modal', true); + } +} + +/* --- Initialisierung --- */ + +document.addEventListener('DOMContentLoaded', function() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/service_worker.js').catch(function(error_object) { + console.error('Service Worker Registration failed:', error_object); + }); + } + + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + + if (application_state.stopwatch_interval_identifier) { + clearInterval(application_state.stopwatch_interval_identifier); + } + application_state.stopwatch_interval_identifier = setInterval(update_game_stopwatch_ui, 1000); + update_game_stopwatch_ui(); + + document.addEventListener('click', handle_global_clicks); + document.addEventListener('submit', handle_global_submits); + + if (typeof io !== 'undefined') { + application_state.websocket_connection = io(); + + application_state.websocket_connection.on('game_state_changed', function() { + if (document.querySelector('.game-section') || document.querySelector('.administrator-section')) { + fetch_and_synchronize_dom(); + } + }); + + application_state.websocket_connection.on('admin_state_changed', function() { + if (document.querySelector('.administrator-section')) { + fetch_and_synchronize_dom(); + } + }); + + application_state.websocket_connection.on('elimination_verification_requested', function() { + fetch_and_synchronize_dom(); + setTimeout(function() { + modify_modal_state('verification-modal', true); + }, 50); + }); + + application_state.websocket_connection.on('elimination_rejected', function() { + modify_modal_state('rejection-modal', true); + }); + + application_state.websocket_connection.on('elimination_revoked', function() { + modify_modal_state('verification-modal', false); + fetch_and_synchronize_dom(); + }); + } }); \ No newline at end of file diff --git a/static/logo.svg b/static/logo.svg index b2f2548..bc1378a 100644 --- a/static/logo.svg +++ b/static/logo.svg @@ -1,2 +1,9 @@ - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/static/stylesheet.css b/static/stylesheet.css index 1003c6a..1925b34 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -1,443 +1,552 @@ :root { - --lightness-primary: 0.35; - --chroma-primary: 0.15; - --hue-primary: 15; + --fallback-primary: hsl(15, 80%, 45%); + --fallback-background: hsl(15, 20%, 95%); + --fallback-surface: hsl(0, 0%, 100%); + --fallback-text: hsl(250, 20%, 15%); + --fallback-static-white: hsl(0, 0%, 100%); + --fallback-static-black: hsl(250, 20%, 15%); + --fallback-text-inverted: hsl(0, 0%, 100%); + --fallback-success: hsl(140, 50%, 45%); + --fallback-danger: hsl(25, 80%, 50%); + --fallback-warning: hsl(50, 90%, 65%); + --fallback-border: hsla(250, 20%, 15%, 0.2); + --fallback-border-light: hsla(250, 20%, 15%, 0.1); + --fallback-primary-transparent: hsla(15, 80%, 45%, 0.1); + --fallback-shadow-primary: hsla(15, 80%, 45%, 0.3); + --fallback-overlay: rgba(0, 0, 0, 0.6); - --lightness-background: 0.9; - --chroma-background: 0.01; - --hue-background: var(--hue-primary); - - --lightness-surface: 1; - --chroma-surface: 0; - --hue-surface: 0; - - --lightness-text: 0.15; - --chroma-text: 0.05; - --hue-text: 250; - - --lightness-success: 0.60; - --chroma-success: 0.18; - --hue-success: 140; - - --lightness-danger: 0.50; - --chroma-danger: 0.22; - --hue-danger: 25; - - --lightness-warning: 0.75; - --chroma-warning: 0.18; - --hue-warning: 70; - - --dimension-multiplier: 1; - --spacing-small: calc(0.5rem * var(--dimension-multiplier)); - --spacing-medium: calc(1rem * var(--dimension-multiplier)); - --spacing-large: calc(1.5rem * var(--dimension-multiplier)); - - --border-radius-small: calc(0.5rem * var(--dimension-multiplier)); - --border-radius-large: calc(1.25rem * var(--dimension-multiplier)); - --border-radius-pill: calc(999rem * var(--dimension-multiplier)); + --lightness-primary: 0.35; + --chroma-primary: 0.15; + --hue-primary: 15; + + --lightness-background: 0.95; + --chroma-background: 0.01; + --hue-background: var(--hue-primary); + + --lightness-surface: 1; + --chroma-surface: 0; + --hue-surface: 0; + + --lightness-text: 0.15; + --chroma-text: 0.05; + --hue-text: 250; + + --lightness-static-white: 1; + --chroma-static-white: 0; + --hue-static-white: 0; + + --lightness-static-black: 0.15; + --chroma-static-black: 0.05; + --hue-static-black: 250; + + --lightness-text-inverted: 1; + --chroma-text-inverted: 0; + --hue-text-inverted: 0; + + --lightness-success: 0.60; + --chroma-success: 0.18; + --hue-success: 140; + + --lightness-danger: 0.50; + --chroma-danger: 0.22; + --hue-danger: 25; + + --lightness-warning: 0.85; + --chroma-warning: 0.15; + --hue-warning: 80; + + --dimension-multiplier: 1; + --spacing-scale-base: 0.25rem; + --spacing-small: calc(var(--spacing-scale-base) * 2 * var(--dimension-multiplier)); + --spacing-medium: calc(var(--spacing-scale-base) * 4 * var(--dimension-multiplier)); + --spacing-large: calc(var(--spacing-scale-base) * 6 * var(--dimension-multiplier)); + + --border-radius-small: calc(0.5rem * var(--dimension-multiplier)); + --border-radius-large: calc(1.25rem * var(--dimension-multiplier)); + --border-radius-pill: calc(999rem * var(--dimension-multiplier)); } @media (prefers-color-scheme: dark) { - :root { - --lightness-primary: 0.65; - --chroma-primary: 0.20; + :root { + --fallback-primary: hsl(15, 80%, 60%); + --fallback-background: hsl(250, 15%, 15%); + --fallback-surface: hsl(250, 15%, 20%); + --fallback-text: hsl(250, 20%, 90%); + --fallback-text-inverted: hsl(0, 0%, 0%); + --fallback-border: hsla(250, 20%, 90%, 0.2); + --fallback-border-light: hsla(250, 20%, 90%, 0.1); + --fallback-primary-transparent: hsla(15, 80%, 60%, 0.1); + --fallback-shadow-primary: hsla(15, 80%, 60%, 0.3); - --lightness-background: 0.2; - --chroma-background: 0.02; - - --lightness-surface: 0.18; - --chroma-surface: 0.02; - - --lightness-text: 0.90; - --chroma-text: 0.02; - } + --lightness-primary: 0.65; + --chroma-primary: 0.20; + + --lightness-background: 0.15; + --chroma-background: 0.02; + + --lightness-surface: 0.20; + --chroma-surface: 0.02; + + --lightness-text: 0.90; + --chroma-text: 0.02; + + --lightness-text-inverted: 0; + --chroma-text-inverted: 0; + --hue-text-inverted: 0; + } } @media (prefers-reduced-motion: reduce) { - * { - transition-duration: 0s !important; - animation-duration: 0s !important; - } + * { + transition-duration: 0s !important; + animation-duration: 0s !important; + } } body { - background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); - color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); - font-family: system-ui, -apple-system, sans-serif; - font-size: calc(1rem * var(--dimension-multiplier)); - line-height: 1.6; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - min-height: 100vh; + background-color: var(--fallback-background); + background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); + color: var(--fallback-text); + color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); + font-family: system-ui, -apple-system, sans-serif; + font-size: calc(1rem * var(--dimension-multiplier)); + line-height: 1.6; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + min-height: 100vh; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.brand-link { + text-decoration: none; + color: inherit; + display: block; + transition: opacity 0.2s ease; +} + +.brand-link:hover { + opacity: 0.8; } .application-header { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - color: oklch(1 0 0); - padding: var(--spacing-medium) var(--spacing-large); - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - position: sticky; - top: 0; - z-index: 100; + background-color: var(--fallback-primary); + background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + color: var(--fallback-text-inverted); + color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted)); + padding: var(--spacing-medium) var(--spacing-large); + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 12px var(--fallback-border-light); + box-shadow: 0 4px 12px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1); + position: sticky; + top: 0; + z-index: 100; } .brand-container { - display: flex; - align-items: center; - gap: var(--spacing-small); + display: flex; + align-items: center; + gap: var(--spacing-small); } .brand-logo { - width: calc(2rem * var(--dimension-multiplier)); - height: calc(2rem * var(--dimension-multiplier)); - color: currentColor; + width: calc(2rem * var(--dimension-multiplier)); + height: calc(2rem * var(--dimension-multiplier)); + color: currentColor; } .application-title { - margin: 0; - font-size: calc(1.25rem * var(--dimension-multiplier)); - font-weight: 800; - letter-spacing: -0.02em; + margin: 0; + font-size: calc(1.25rem * var(--dimension-multiplier)); + font-weight: 800; + letter-spacing: -0.02em; } .application-content { - flex: 1; - padding: var(--spacing-large); - width: 100%; - max-width: calc(48rem * var(--dimension-multiplier)); - margin: 0 auto; - box-sizing: border-box; + flex: 1; + padding: var(--spacing-large); + width: 100%; + max-width: calc(48rem * var(--dimension-multiplier)); + margin: 0 auto; + box-sizing: border-box; } .content-card { - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - padding: var(--spacing-large); - border-radius: var(--border-radius-large); - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01); - margin-bottom: var(--spacing-large); + background-color: var(--fallback-surface); + background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); + padding: var(--spacing-large); + border-radius: var(--border-radius-large); + box-shadow: 0 10px 25px -5px var(--fallback-border-light), 0 8px 10px -6px var(--fallback-border-light); + box-shadow: 0 10px 25px -5px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.05), 0 8px 10px -6px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.01); + margin-bottom: var(--spacing-large); + transition: background-color 0.3s ease, box-shadow 0.3s ease; } .section-title { - margin-top: 0; - margin-bottom: var(--spacing-medium); - display: flex; - align-items: center; - gap: var(--spacing-small); + margin-top: 0; + margin-bottom: var(--spacing-medium); + display: flex; + align-items: center; + gap: var(--spacing-small); } .card-subtitle { - margin-top: 0; - margin-bottom: var(--spacing-small); - opacity: 0.7; - text-transform: uppercase; - font-size: calc(0.875rem * var(--dimension-multiplier)); - letter-spacing: 0.05em; + margin-top: 0; + margin-bottom: var(--spacing-small); + opacity: 0.7; + text-transform: uppercase; + font-size: calc(0.875rem * var(--dimension-multiplier)); + letter-spacing: 0.05em; } .input-form { - display: flex; - flex-direction: column; - gap: var(--spacing-medium); + display: flex; + flex-direction: column; + gap: var(--spacing-medium); } .inline-form { - display: inline-block; + display: inline-block; } .flexible_form_container { - display: flex; - gap: calc(0.5rem * var(--dimension-multiplier)); - align-items: center; - flex-wrap: wrap; + display: flex; + gap: calc(0.5rem * var(--dimension-multiplier)); + align-items: center; + flex-wrap: wrap; } .form-group { - display: flex; - flex-direction: column; - gap: calc(0.25rem * var(--dimension-multiplier)); + display: flex; + flex-direction: column; + gap: calc(0.25rem * var(--dimension-multiplier)); } .input-label { - font-weight: bold; - font-size: calc(0.875rem * var(--dimension-multiplier)); + font-weight: bold; + font-size: calc(0.875rem * var(--dimension-multiplier)); } .text-input { - padding: var(--spacing-medium); - border: 2px solid oklch(0.5 0 0 / 0.2); - border-radius: var(--border-radius-small); - background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); - color: inherit; - font-size: inherit; - transition: border-color 0.2s; + padding: var(--spacing-medium); + border: 2px solid var(--fallback-border); + border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2); + border-radius: var(--border-radius-small); + background-color: var(--fallback-background); + background-color: oklch(var(--lightness-background) var(--chroma-background) var(--hue-background)); + color: inherit; + font-size: inherit; + transition: border-color 0.2s ease, background-color 0.3s ease; } .text-input:focus { - outline: none; - border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + outline: none; + border-color: var(--fallback-primary); + border-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); } .small-input { - width: calc(12rem * var(--dimension-multiplier)); - padding: var(--spacing-small); + width: calc(12rem * var(--dimension-multiplier)); + padding: var(--spacing-small); } .time-input { - width: calc(13rem * var(--dimension-multiplier)); + width: calc(13rem * var(--dimension-multiplier)); } .action-button { - padding: var(--spacing-medium) var(--spacing-large); - border: none; - border-radius: var(--border-radius-pill); - font-size: inherit; - font-weight: bold; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-small); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - color: oklch(1 0 0); + padding: var(--spacing-medium) var(--spacing-large); + border: none; + border-radius: var(--border-radius-pill); + font-size: inherit; + font-weight: bold; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-small); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + color: var(--fallback-text-inverted); + color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted)); } .action-button:hover, .action-button:focus { - filter: brightness(1.1); - transform: translateY(-1px); + filter: brightness(1.1); + transform: translateY(-1px); } .icon-button { - padding: calc(0.5rem * var(--dimension-multiplier)); - border-radius: var(--border-radius-small); + padding: calc(0.5rem * var(--dimension-multiplier)); + border-radius: var(--border-radius-small); } .primary-button { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3); + background-color: var(--fallback-primary); + background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + box-shadow: 0 4px 12px var(--fallback-shadow-primary); + box-shadow: 0 4px 12px oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.3); } .success-button { - background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); + background-color: var(--fallback-success); + background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); } .danger-button { - background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); + background-color: var(--fallback-danger); + background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); } .warning-button { - background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); - color: oklch(0.2 0 0); + background-color: var(--fallback-warning); + background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); + color: var(--fallback-static-black); + color: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black)); } .secondary-button { - background-color: transparent; - color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); - border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2); + background-color: transparent; + color: var(--fallback-text); + color: oklch(var(--lightness-text) var(--chroma-text) var(--hue-text)); + border: 2px solid var(--fallback-border); + border: 2px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.2); } .large-button { - padding: var(--spacing-large) calc(2.5rem * var(--dimension-multiplier)); - font-size: calc(1.125rem * var(--dimension-multiplier)); - width: 100%; + padding: var(--spacing-large) calc(2.5rem * var(--dimension-multiplier)); + font-size: calc(1.125rem * var(--dimension-multiplier)); + width: 100%; } .small-button { - padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.75rem * var(--dimension-multiplier)); - font-size: calc(0.875rem * var(--dimension-multiplier)); + padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.75rem * var(--dimension-multiplier)); + font-size: calc(0.875rem * var(--dimension-multiplier)); } .tab-container { - display: flex; - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - border-radius: var(--border-radius-pill); - padding: calc(0.375rem * var(--dimension-multiplier)); - gap: calc(0.25rem * var(--dimension-multiplier)); - box-shadow: 0 4px 12px rgba(0,0,0,0.05); - margin-bottom: var(--spacing-large); - overflow-x: auto; + display: flex; + background-color: var(--fallback-surface); + background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); + border-radius: var(--border-radius-pill); + padding: calc(0.375rem * var(--dimension-multiplier)); + gap: calc(0.25rem * var(--dimension-multiplier)); + box-shadow: 0 4px 12px var(--fallback-border-light); + box-shadow: 0 4px 12px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.05); + margin-bottom: var(--spacing-large); + overflow-x: auto; } .tab-button { - flex: 1; - border-radius: var(--border-radius-pill); - padding: calc(0.625rem * var(--dimension-multiplier)) var(--spacing-medium); - border: none; - background: transparent; - color: inherit; - font-size: inherit; - font-weight: bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - gap: calc(0.5rem * var(--dimension-multiplier)); - white-space: nowrap; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + flex: 1; + border-radius: var(--border-radius-pill); + padding: calc(0.625rem * var(--dimension-multiplier)) var(--spacing-medium); + border: none; + background: transparent; + color: inherit; + font-size: inherit; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: calc(0.5rem * var(--dimension-multiplier)); + white-space: nowrap; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .active-tab { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - color: oklch(1 0 0); - box-shadow: 0 2px 8px rgba(0,0,0,0.15); + background-color: var(--fallback-primary); + background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + color: var(--fallback-text-inverted); + color: oklch(var(--lightness-text-inverted) var(--chroma-text-inverted) var(--hue-text-inverted)); + box-shadow: 0 2px 8px var(--fallback-border-light); + box-shadow: 0 2px 8px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.15); } .tab-content { - display: none; + display: none; } .active-content { - display: block; + display: block; + animation: fade-in 0.3s ease-out; +} + +@keyframes fade-in { + from { opacity: 0; transform: translateY(calc(0.25rem * var(--dimension-multiplier))); } + to { opacity: 1; transform: translateY(0); } } .status-pill { - display: inline-flex; - align-items: center; - gap: calc(0.375rem * var(--dimension-multiplier)); - padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.875rem * var(--dimension-multiplier)); - border-radius: var(--border-radius-pill); - font-size: calc(0.875rem * var(--dimension-multiplier)); - font-weight: bold; - color: oklch(1 0 0); - margin-left: var(--spacing-small); + display: inline-flex; + align-items: center; + gap: calc(0.375rem * var(--dimension-multiplier)); + padding: calc(0.25rem * var(--dimension-multiplier)) calc(0.875rem * var(--dimension-multiplier)); + border-radius: var(--border-radius-pill); + font-size: calc(0.875rem * var(--dimension-multiplier)); + font-weight: bold; + color: var(--fallback-static-white); + color: oklch(var(--lightness-static-white) var(--chroma-static-white) var(--hue-static-white)); + margin-left: var(--spacing-small); } .warning-pill { - background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); - color: oklch(0.2 0 0); + background-color: var(--fallback-warning); + background-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); + color: var(--fallback-static-black); + color: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black)); } .success-pill { - background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); + background-color: var(--fallback-success); + background-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); } .danger-pill { - background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); + background-color: var(--fallback-danger); + background-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); } .notice-card { - border-left: 6px solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + border-left: calc(0.375rem * var(--dimension-multiplier)) solid var(--fallback-primary); + border-left: calc(0.375rem * var(--dimension-multiplier)) solid oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); } .danger-card { - border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); + border-left-color: var(--fallback-danger); + border-left-color: oklch(var(--lightness-danger) var(--chroma-danger) var(--hue-danger)); } .success-card { - border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); + border-left-color: var(--fallback-success); + border-left-color: oklch(var(--lightness-success) var(--chroma-success) var(--hue-success)); } .warning-card { - border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); + border-left-color: var(--fallback-warning); + border-left-color: oklch(var(--lightness-warning) var(--chroma-warning) var(--hue-warning)); } .target-name { - font-size: calc(2.5rem * var(--dimension-multiplier)); - text-align: center; - margin: var(--spacing-large) 0; - color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); - font-weight: 800; + font-size: calc(2.5rem * var(--dimension-multiplier)); + text-align: center; + margin: var(--spacing-large) 0; + color: var(--fallback-primary); + color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + font-weight: 800; } .data-table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } .data-table th, .data-table td { - padding: var(--spacing-medium); - border-bottom: 1px solid oklch(0.5 0 0 / 0.1); - text-align: left; + padding: var(--spacing-medium); + border-bottom: 1px solid var(--fallback-border-light); + border-bottom: 1px solid oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1); + text-align: left; } .data-table th { - font-weight: 800; - text-transform: uppercase; - font-size: calc(0.75rem * var(--dimension-multiplier)); - letter-spacing: 0.05em; - opacity: 0.7; + font-weight: 800; + text-transform: uppercase; + font-size: calc(0.75rem * var(--dimension-multiplier)); + letter-spacing: 0.05em; + opacity: 0.7; } .highlighted-row { - background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1); + background-color: var(--fallback-primary-transparent); + background-color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary) / 0.1); } .highlighted-row td { - font-weight: bold; + font-weight: bold; } .unregistered-row { - opacity: 0.6; + opacity: 0.6; } .markdown-container h1, .markdown-container h2, .markdown-container h3 { - color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); + color: var(--fallback-primary); + color: oklch(var(--lightness-primary) var(--chroma-primary) var(--hue-primary)); } .markdown-container ul { - padding-left: var(--spacing-large); + padding-left: var(--spacing-large); } .modal-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.6); - backdrop-filter: blur(4px); - justify-content: center; - align-items: center; - z-index: 1000; + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--fallback-overlay); + background-color: oklch(0 0 0 / 0.6); + backdrop-filter: blur(4px); + justify-content: center; + align-items: center; + z-index: 1000; } .active-modal { - display: flex; + display: flex; } .modal-dialog { - background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); - padding: var(--spacing-large); - border-radius: var(--border-radius-large); - max-width: calc(32rem * var(--dimension-multiplier)); - width: 90%; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); + background-color: var(--fallback-surface); + background-color: oklch(var(--lightness-surface) var(--chroma-surface) var(--hue-surface)); + padding: var(--spacing-large); + border-radius: var(--border-radius-large); + max-width: calc(32rem * var(--dimension-multiplier)); + width: 90%; + box-shadow: 0 20px 40px var(--fallback-border); + box-shadow: 0 20px 40px oklch(0 0 0 / 0.2); } .modal-title { - margin-top: 0; - display: flex; - align-items: center; - gap: var(--spacing-small); + margin-top: 0; + display: flex; + align-items: center; + gap: var(--spacing-small); } .modal-actions { - display: flex; - gap: var(--spacing-medium); - justify-content: flex-end; - margin-top: var(--spacing-large); + display: flex; + gap: var(--spacing-medium); + justify-content: flex-end; + margin-top: var(--spacing-large); } .qr-code-container { - background-color: oklch(1 0 0); - padding: var(--spacing-medium); - border-radius: var(--border-radius-large); - display: inline-block; - margin-bottom: var(--spacing-medium); - box-shadow: inset 0 0 0 2px oklch(0.5 0 0 / 0.1); + background-color: var(--fallback-static-white); + background-color: oklch(var(--lightness-static-white) var(--chroma-static-white) var(--hue-static-white)); + padding: var(--spacing-medium); + border-radius: var(--border-radius-large); + display: inline-block; + margin-bottom: var(--spacing-medium); + box-shadow: inset 0 0 0 2px var(--fallback-border-light); + box-shadow: inset 0 0 0 2px oklch(var(--lightness-text) var(--chroma-text) var(--hue-text) / 0.1); } .qr-code-container svg { - display: block; - width: calc(14rem * var(--dimension-multiplier)); - height: calc(14rem * var(--dimension-multiplier)); + display: block; + width: calc(14rem * var(--dimension-multiplier)); + height: calc(14rem * var(--dimension-multiplier)); +} + +.qr-code-container svg path { + fill: var(--fallback-static-black); + fill: oklch(var(--lightness-static-black) var(--chroma-static-black) var(--hue-static-black)); } .monospace-identifier { - font-family: monospace; + font-family: monospace; } \ No newline at end of file diff --git a/templates/administrator.html b/templates/administrator.html index 88f9c3f..138c7f5 100644 --- a/templates/administrator.html +++ b/templates/administrator.html @@ -1,348 +1,348 @@ {% extends "layout.html" %} {% block content %}
- {% if setup_required %} -
-

Administrator Einrichtung

-
-
- - -
-
- - -
- -
-
- {% elif login_required %} -
-

Administrator Anmeldung

-
-
- - -
- {% if two_factor_enabled %} -
- - -
- {% endif %} - -
-
- {% elif dashboard_active %} -
- - - -
- -
-
-

- Spielstatus: - {% if game_status == 'pending' %} - Ausstehend - {% elif game_status == 'running' %} - Läuft - {% elif game_status == 'paused' %} - Pausiert - {% elif game_status == 'finished' %} - Beendet - {% endif %} -

- - {% if game_status == 'pending' %} - {% if can_start %} -
- - -
- {% else %} -
-

Es werden mindestens zwei freigegebene Spieler benötigt, um das Spiel zu starten.

-
- {% endif %} - {% elif game_status == 'running' %} -
- - -
-
- - -
- {% elif game_status == 'paused' %} -
- - -
-
- - -
- {% elif game_status == 'finished' %} -
- - -
- {% endif %} -
- -
-

Sicherheit

- - {% if two_factor_enabled %} -
-

Zwei-Faktor-Authentifizierung ist aktiv.

-
- - -
-
- {% else %} -
-

Zwei-Faktor-Authentifizierung ist inaktiv. Bitte scanne den QR-Code und bestätige zur Aktivierung.

-

Manuelle Eingabe: {{ two_factor_secret }}

-
- {{ qr_code_svg | safe }} -
-
- -
- - -
- -
-
- {% endif %} - -
- -
- - -
-
- - -
- -
-
-
- -
-
-

Spieler manuell erstellen

-
- -
- - -
- -
-
- -
- - - - - - - - - - - - {% for current_player in all_players %} - - - - - - - - {% endfor %} - -
KennungName & ÄnderungStatusZielAktionen
- {{ current_player.player_identifier }} - {% if not current_player.is_approved %} -
Wartet - {% elif current_player.late_joiner %} -
Späteinstieg - {% endif %} -
-
- - - - -
- {% if current_player.pending_display_name %} -
-

- Wünscht: {{ current_player.pending_display_name }} -

-
- - - -
-
- - - -
-
- {% endif %} -
- {% if not current_player.is_approved %} -
- - - -
- {% else %} - {% if current_player.is_alive %} - Lebend -
- - - -
- {% else %} - Tot -
- - - -
- {% endif %} - {% endif %} -
- {% if current_player.target_identifier %} - {{ current_player.target_identifier }} -
- - - - -
- {% else %} - - - {% endif %} -
-
- - - -
- {% if current_player.reset_token %} - - {% endif %} -
- - - -
-
-
-
- -
-
- - - - - - - - - - - - {% for report in kill_logs %} - - - - - - - - {% else %} - - - - {% endfor %} - -
ZeitpunktKillerOpferStatusAktionen
{{ report.kill_timestamp[:16].replace('T', ' ') if report.kill_timestamp else '-' }}{{ report.killer_name or 'Unbekannt' }} ({{ report.killer_identifier }}){{ report.victim_name or 'Unbekannt' }} ({{ report.victim_identifier }}) - {% if report.report_status == 'confirmed' %} - Bestätigt - {% elif report.report_status == 'pending' %} - Ausstehend - {% elif report.report_status == 'rejected' %} - Abgelehnt - {% elif report.report_status == 'revoked' %} - Widerrufen - {% else %} - {{ report.report_status }} - {% endif %} - - {% if report.report_status == 'confirmed' %} -
- - - -
- {% endif %} - - {% if report.report_status == 'pending' %} -
- - - -
- {% endif %} - - {% if report.report_status in ['pending', 'rejected', 'revoked'] %} -
- - - -
- {% endif %} -
Bisher keine Meldungen im System.
-
-
- - - {% endif %} + {% if setup_required %} +
+

Einrichtung

+
+
+ + +
+
+ + +
+ +
+
+ {% elif login_required %} +
+

Admin-Login

+
+
+ + +
+ {% if two_factor_enabled %} +
+ + +
+ {% endif %} + +
+
+ {% elif dashboard_active %} +
+ + + +
+ +
+
+

+ Spielstatus: + {% if game_status == 'pending' %} + Ausstehend + {% elif game_status == 'running' %} + Läuft + {% elif game_status == 'paused' %} + Pausiert + {% elif game_status == 'finished' %} + Beendet + {% endif %} +

+ + {% if game_status == 'pending' %} + {% if can_start %} +
+ + +
+ {% else %} +
+

Zu wenige Spieler zum Starten.

+
+ {% endif %} + {% elif game_status == 'running' %} +
+ + +
+
+ + +
+ {% elif game_status == 'paused' %} +
+ + +
+
+ + +
+ {% elif game_status == 'finished' %} +
+ + +
+ {% endif %} +
+ +
+

Sicherheit

+ + {% if two_factor_enabled %} +
+

2FA ist aktiv.

+
+ + +
+
+ {% else %} +
+

2FA inaktiv. QR-Code scannen.

+

Code: {{ two_factor_secret }}

+
+ {{ qr_code_svg | safe }} +
+
+ +
+ + +
+ +
+
+ {% endif %} + +
+ +
+ + +
+
+ + +
+ +
+
+
+ +
+
+

Spieler erstellen

+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + {% for current_player in all_players %} + + + + + + + + {% endfor %} + +
KennungNameStatusZielAktionen
+ {{ current_player.player_identifier }} + {% if not current_player.is_approved %} +
Wartet + {% elif current_player.late_joiner %} +
Späteinstieg + {% endif %} +
+
+ + + + +
+ {% if current_player.pending_display_name %} +
+

+ -> {{ current_player.pending_display_name }} +

+
+ + + +
+
+ + + +
+
+ {% endif %} +
+ {% if not current_player.is_approved %} +
+ + + +
+ {% else %} + {% if current_player.is_alive %} + Lebend +
+ + + +
+ {% else %} + Gekillt +
+ + + +
+ {% endif %} + {% endif %} +
+ {% if current_player.target_identifier %} + {{ current_player.target_identifier }} +
+ + + + +
+ {% else %} + - + {% endif %} +
+
+ + + +
+ {% if current_player.reset_token %} + + {% endif %} +
+ + + +
+
+
+
+ +
+
+ + + + + + + + + + + + {% for report in kill_logs %} + + + + + + + + {% else %} + + + + {% endfor %} + +
ZeitpunktKillerOpferStatusAktionen
{{ report.kill_timestamp[:16].replace('T', ' ') if report.kill_timestamp else '-' }}{{ report.killer_name or 'Unbekannt' }} ({{ report.killer_identifier }}){{ report.victim_name or 'Unbekannt' }} ({{ report.victim_identifier }}) + {% if report.report_status == 'confirmed' %} + Bestätigt + {% elif report.report_status == 'pending' %} + Ausstehend + {% elif report.report_status == 'rejected' %} + Abgelehnt + {% elif report.report_status == 'revoked' %} + Widerrufen + {% else %} + {{ report.report_status }} + {% endif %} + + {% if report.report_status == 'confirmed' %} +
+ + + +
+ {% endif %} + + {% if report.report_status == 'pending' %} +
+ + + +
+ {% endif %} + + {% if report.report_status in ['pending', 'rejected', 'revoked'] %} +
+ + + +
+ {% endif %} +
Keine Meldungen.
+
+
+ + + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/templates/game.html b/templates/game.html index 52283e1..c3e6e5d 100644 --- a/templates/game.html +++ b/templates/game.html @@ -1,187 +1,194 @@ {% extends "layout.html" %} {% block content %}
-
-

Willkommen, {{ player.display_name }} ({{ player.player_identifier }})!

-
- - {% if not player.is_approved %} -
- -
- {% elif game_status == 'pending' %} -
- -
- {% elif game_status == 'finished' %} -
-

Spiel beendet – Rangliste

- - - - - - - - - - {% for current_player in all_players %} - - - - - - {% endfor %} - -
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
-
- {% elif game_status in ['running', 'paused'] %} -
- - - -
- -
- {% if game_status == 'paused' %} -
- -

Das Spiel wurde pausiert. Eliminierungen sind aktuell nicht möglich.

-
- {% endif %} - - {% if player.is_alive %} -
-

Dein Ziel

-

{{ target_player.display_name if target_player else 'Niemand' }}

- {% if game_status == 'running' %} - - {% else %} - - {% endif %} -
- {% elif player.late_joiner %} -
- -

Du bist dem Spiel während einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.

-
- {% else %} -
- -
- {% endif %} - -
-

Spielübersicht

-

Überlebende: {{ alive_count }}

-

Deine Kills: {{ player.score }}

-

Gesamte Kills: {{ total_eliminations }}

-

Laufzeit: Wird berechnet...

-
- -
-

Profil

- {% if player.pending_display_name %} -
-

Der Name {{ player.pending_display_name }} wird geprüft…

-
- {% else %} - - {% endif %} -
-
- -
-
- - - - - - - - - - {% for current_player in all_players %} - - - - - - {% endfor %} - -
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
-
-
- -
-
- {{ rules_markup | safe }} -
-
- - - - - - - - - - - {% endif %} +
+

Hallo, {{ player.display_name }}!

+
+ + {% if not player.is_approved %} +
+ +

Die Spielleitung muss deinen Account noch aktivieren.

+
+ {% elif game_status == 'pending' %} +
+ +
+ {% elif game_status == 'finished' %} +
+

Rangliste

+ + + + + + + + + + {% for current_player in all_players %} + + + + + + {% endfor %} + +
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
+
+ {% elif game_status in ['running', 'paused'] %} +
+ + + +
+ +
+ {% if game_status == 'paused' %} +
+ +

Kills sind vorübergehend deaktiviert.

+
+ {% endif %} + + {% if player.is_alive %} +
+

Ziel

+

{{ target_player.display_name if target_player else 'Niemand' }}

+ + {% if game_status == 'running' %} + {% if has_pending_kill_report %} +
+

Kill gemeldet

+

Warte auf Bestätigung…

+ +
+ {% else %} + + {% endif %} + {% else %} + + {% endif %} +
+ {% elif player.late_joiner %} +
+ +

Die Spielleitung wechselt dich bald ein.

+
+ {% else %} +
+ +
+ {% endif %} + +
+

Übersicht

+

Lebend: {{ alive_count }}

+

Kills: {{ player.score }}

+

Gesamt: {{ total_eliminations }}

+

Zeit: Lade...

+
+ +
+

Profil

+ {% if player.pending_display_name %} +
+

{{ player.pending_display_name }} wird geprüft…

+
+ + +
+
+ {% else %} + + {% endif %} +
+
+ +
+
+ + + + + + + + + + {% for current_player in all_players %} + + + + + + {% endfor %} + +
PlatzNamePunkte
{{ loop.index }}{{ current_player.display_name }}{{ current_player.score }}
+
+
+ +
+
+ {{ rules_markup | safe }} +
+
+ + + + + + + + + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index a28e45f..e820d0b 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,33 +1,35 @@ - - - SCOicide Squad - - - - - - - + + + SCOicide Squad + + + + + + + -
-
- -

SCOicide Squad

-
- -
-
- {% block content %}{% endblock %} -
+
+ +
+ +

SCOicide Squad

+
+
+ +
+
+ {% block content %}{% endblock %} +
\ No newline at end of file diff --git a/templates/login.html b/templates/login.html index d589505..c0c2e3f 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,72 +1,85 @@ {% extends "layout.html" %} {% block content %}
- {% if reset_mode %} -
-

Passwort zurücksetzen

-
- -
- - -
-
- - -
- -
-
- {% elif register_mode %} -
-

Neues Konto erstellen

-
-
- - -
-
- - -
-
- - -
- -
-
- - {% elif registration_success %} -
-

Registrierung erfolgreich

-

Deine ID für die Anmeldung lautet:

-

{{ new_identifier }}

-

Bitte speichere oder merke sie dir!

- -
- {% else %} -
-

Anmeldung

-
-
- - -
-
- - -
- -
-
- - {% endif %} + {% if reset_mode %} +
+

Passwort zurücksetzen

+
+ +
+ + +
+
+ + +
+ +
+
+ {% elif register_mode %} +
+

Registrieren

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + {% elif registration_success %} +
+

Erfolgreich

+

Wichtig!

+

Deine ID:

+

{{ new_identifier }}

+ +

Gut aufbewahren!

+ +
+ {% else %} +
+

Login

+
+
+ + +
+
+ + +
+ +
+
+
+ Registrieren + +
+ + + {% endif %}
{% endblock %} \ No newline at end of file