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 baf8ef2..2f6a587 100644
Binary files a/game_database.sqlite and b/game_database.sqlite differ
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
-
-
- {% elif dashboard_active %}
-
- Steuerung
- Spieler
- Meldungen
-
-
-
-
-
- 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
-
-
-
-
-
-
-
- Kennung
- Name & Änderung
- Status
- Ziel
- Aktionen
-
-
-
- {% for current_player in all_players %}
-
-
- {{ 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 %}
-
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
- Zeitpunkt
- Killer
- Opfer
- Status
- Aktionen
-
-
-
- {% for report in kill_logs %}
-
- {{ 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 %}
-
-
- {% else %}
-
- Bisher keine Meldungen im System.
-
- {% endfor %}
-
-
-
-
-
-
-
-
Aktion planen
-
Führe die Aktion sofort aus oder plane sie für die Zukunft.
-
- Zeitpunkt (Optional)
-
-
-
- Bestätigen
- Abbrechen
-
-
-
- {% endif %}
+ {% if setup_required %}
+
+ {% elif login_required %}
+
+ {% elif dashboard_active %}
+
+ Steuerung
+ Spieler
+ Meldungen
+
+
+
+
+
+ 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 %}
+
+ {% else %}
+
+
2FA inaktiv. QR-Code scannen.
+
Code: {{ two_factor_secret }}
+
+ {{ qr_code_svg | safe }}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Kennung
+ Name
+ Status
+ Ziel
+ Aktionen
+
+
+
+ {% for current_player in all_players %}
+
+
+ {{ 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 %}
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ Zeitpunkt
+ Killer
+ Opfer
+ Status
+ Aktionen
+
+
+
+ {% for report in kill_logs %}
+
+ {{ 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 %}
+
+
+ {% else %}
+
+ Keine Meldungen.
+
+ {% endfor %}
+
+
+
+
+
+
+
+
Aktion planen
+
Sofort ausführen oder planen.
+
+ Zeitpunkt (Optional)
+
+
+
+ Bestätigen
+ Abbrechen
+
+
+
+ {% 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 %}
-
-
Konto wird geprüft…
-
- {% elif game_status == 'pending' %}
-
-
Startet bald…
-
- {% elif game_status == 'finished' %}
-
-
Spiel beendet – Rangliste
-
-
-
- Platz
- Name
- Punkte
-
-
-
- {% for current_player in all_players %}
-
- {{ loop.index }}
- {{ current_player.display_name }}
- {{ current_player.score }}
-
- {% endfor %}
-
-
-
- {% elif game_status in ['running', 'paused'] %}
-
- Ziel
- Punkte
- Regeln
-
-
-
- {% if game_status == 'paused' %}
-
-
Spiel pausiert
-
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' %}
- Kill melden
- {% else %}
- Pausiert
- {% endif %}
-
- {% elif player.late_joiner %}
-
-
Warte auf Einstieg…
-
Du bist dem Spiel während einer laufenden Runde beigetreten. Du wirst aktiv, sobald du eingewechselt wirst.
-
- {% else %}
-
-
Eliminiert
-
- {% 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 %}
-
Name ändern
- {% endif %}
-
-
-
-
-
-
-
-
- Platz
- Name
- Punkte
-
-
-
- {% for current_player in all_players %}
-
- {{ loop.index }}
- {{ current_player.display_name }}
- {{ current_player.score }}
-
- {% endfor %}
-
-
-
-
-
-
-
- {{ rules_markup | safe }}
-
-
-
-
-
-
Kill
-
Möchtest du melden, dass du dein Ziel gekillt hast?
-
- Ja, melden
- Abbrechen
-
-
-
-
-
-
-
Bitte warten
-
Warte auf Bestätigung vom Ziel...
-
-
-
-
-
-
Abgelehnt
-
Das Ziel hat deinen Kill abgelehnt.
-
- Verstanden
-
-
-
-
-
-
-
Wurdest du eliminiert?
-
- {% if pending_attacker_name %}
- {{ pending_attacker_name }} hat gemeldet, dass er/sie dich gekillt hat.
- {% else %}
- Dein Angreifer hat gemeldet, dass er dich gekillt hat.
- {% endif %}
-
-
- Bestätigen
- Ablehnen
-
-
-
-
-
-
-
Name ändern
-
Dein neuer Name muss erst geprüft werden.
-
-
-
- {% endif %}
+
+
Hallo, {{ player.display_name }}!
+
+
+ {% if not player.is_approved %}
+
+
Warten auf Freigabe
+
Die Spielleitung muss deinen Account noch aktivieren.
+
+ {% elif game_status == 'pending' %}
+
+
Spiel startet in Kürze
+
+ {% elif game_status == 'finished' %}
+
+
Rangliste
+
+
+
+ Platz
+ Name
+ Punkte
+
+
+
+ {% for current_player in all_players %}
+
+ {{ loop.index }}
+ {{ current_player.display_name }}
+ {{ current_player.score }}
+
+ {% endfor %}
+
+
+
+ {% elif game_status in ['running', 'paused'] %}
+
+ Ziel
+ Punkte
+ Regeln
+
+
+
+ {% if game_status == 'paused' %}
+
+
Pausiert
+
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…
+
Zurückziehen
+
+ {% else %}
+
Kill melden
+ {% endif %}
+ {% else %}
+
Gesperrt
+ {% endif %}
+
+ {% elif player.late_joiner %}
+
+
Warten auf Einwechslung
+
Die Spielleitung wechselt dich bald ein.
+
+ {% else %}
+
+
Gekillt
+
+ {% 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 %}
+
Name ändern
+ {% endif %}
+
+
+
+
+
+
+
+
+ Platz
+ Name
+ Punkte
+
+
+
+ {% for current_player in all_players %}
+
+ {{ loop.index }}
+ {{ current_player.display_name }}
+ {{ current_player.score }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+ {{ rules_markup | safe }}
+
+
+
+
+
+
Kill melden
+
Sicher, dass du den Kill melden möchtest?
+
+ Melden
+ Abbrechen
+
+
+
+
+
+
+
Abgelehnt
+
Das Ziel hat die Meldung abgelehnt. Bitte klären.
+
+ Okay
+
+
+
+
+
+
+
Gekillt?
+
+ {% if pending_attacker_name %}
+ Wurdest du von {{ pending_attacker_name }} gekillt?
+ {% else %}
+ Wurdest du gekillt?
+ {% endif %}
+
+
+ Ja
+ Nein
+
+
+
+
+
+
+
Name ändern
+
Der Name wird vorher von der Spielleitung geprüft.
+
+
+
+ {% 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
+
+
+
+
+
+
+
-
-
- {% block content %}{% endblock %}
-
+
+
+ {% 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 %}
-
-
- {% endif %}
+ {% if reset_mode %}
+
+
Passwort zurücksetzen
+
+
+ {% elif register_mode %}
+
+
+ {% elif registration_success %}
+
+
Erfolgreich
+
Wichtig!
+
Deine ID:
+
{{ new_identifier }}
+
ID kopieren
+
Gut aufbewahren!
+
+
+ {% else %}
+
+
+
Registrieren
+
Probleme beim Login?
+
+
+
+
+
Hilfe
+
Wende dich an die Spielleitung, falls du deine ID oder dein Passwort vergessen hast.
+
+ Okay
+
+
+
+ {% endif %}
{% endblock %}
\ No newline at end of file