diff --git a/.gitignore b/.gitignore index f460b62..fed9bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ env/ # 5. IDE / Editor Einstellungen .vscode/ .idea/ + +data_trans.py \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..370f462 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = mysql+pymysql://admin:PWM_556rtX@192.168.0.113:3306/league_db + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/data/data_api.py b/data/data_api.py index c4badca..cb88fb5 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -190,7 +190,7 @@ def get_player_statistic(player_id, system_id): SELECT sys.id AS gamesystem_id, sys.name AS gamesystem_name, - sys.*, + sys.description, stat.mmr, stat.games_in_system, stat.points, @@ -206,6 +206,7 @@ def get_player_statistic(player_id, system_id): ON sys.id = stat.gamesystem_id AND stat.player_id = %s WHERE sys.id = %s """ + cursor.execute(query, (player_id, system_id)) row = cursor.fetchone() @@ -214,6 +215,33 @@ def get_player_statistic(player_id, system_id): return row +def get_player_rank(player_id, system_id): + """Holt den Rang eines Spielers in einem System basierend auf MMR (absteigend).""" + connection = db_connection() + cursor = connection.cursor(dictionary=True) + + query = """ + SELECT COUNT(*) + 1 AS rank + FROM player_game_statistic stat + WHERE stat.gamesystem_id = %s + AND stat.games_in_system > 0 + AND stat.mmr > ( + SELECT stat2.mmr + FROM player_game_statistic stat2 + WHERE stat2.player_id = %s + AND stat2.gamesystem_id = %s + ) + """ + cursor.execute(query, (system_id, player_id, system_id)) + row = cursor.fetchone() + + cursor.close() + connection.close() + + if row and row['rank'] is not None: + return row['rank'] + return None + def join_league(player_id, gamesystem_id): connection = db_connection() cursor = connection.cursor() @@ -407,19 +435,21 @@ def get_gamesystems_data(): return rows + def get_leaderboard(system_name): """Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste.""" connection = db_connection() cursor = connection.cursor(dictionary=True) - query = """ - SELECT p.id, p.display_name, p.discord_name, stat.mmr - FROM players p - JOIN player_game_statistic stat ON p.id = stat.player_id - JOIN gamesystems sys ON stat.gamesystem_id = sys.id - WHERE sys.name = %s AND stat.games_in_system > 0 - ORDER BY stat.mmr DESC - """ + query = query = """ + SELECT p.id, p.display_name, p.discord_name, p.discord_id, p.discord_avatar_url, + stat.mmr, stat.games_in_system, stat.wins, stat.loss, stat.avv_points + FROM players p + JOIN player_game_statistic stat ON p.id = stat.player_id + JOIN gamesystems sys ON stat.gamesystem_id = sys.id + WHERE sys.name = %s AND stat.games_in_system > 0 + ORDER BY stat.mmr DESC +""" cursor.execute(query, (system_name,)) rows = cursor.fetchall() diff --git a/gui/league_statistic_gui.py b/gui/league_statistic_gui.py index 3a76d03..00280e4 100644 --- a/gui/league_statistic_gui.py +++ b/gui/league_statistic_gui.py @@ -2,10 +2,11 @@ from nicegui import ui, app from gui import gui_style from data import data_api from gui.info_text import info_system +from gui.templates.leaderboard import Leaderboard def setup_routes(): - @ui.page('/statistic/{systemname}', dark=True) - def gamesystem_statistic_page(systemname: str): + @ui.page('/statistic/{system_name}', dark=True) + def gamesystem_statistic_page(system_name: str): if not app.storage.user.get('authenticated', False): ui.navigate.to('/') @@ -14,112 +15,77 @@ def setup_routes(): gui_style.apply_design() player_id = app.storage.user.get('db_id') - all_stats = data_api.get_player_statistics(player_id) + system_id = data_api.get_gamesystem_id_by_name(system_name) + print(player_id, system_id) - # Passendes System anhand des Namens (case-insensitive) herausfiltern - system_stat = next( - (s for s in all_stats if s["gamesystem_name"].lower() == systemname.lower()), - None - ) + player_stats = data_api.get_player_statistic(player_id, system_id) + + # Header + with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): + ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round") + ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{system_name}')) - if system_stat: - mmr = system_stat["mmr"] or 0 - games = system_stat["games_in_system"] or 0 - points = system_stat["points"] or 0 - avv_points = system_stat.get("avv_points") or "-" - last_played_raw = system_stat.get("last_played") - last_played = str(last_played_raw)[:10] if last_played_raw else "-" + with ui.column().classes('w-full items-center justify-center mt-10'): + ui.label(f'Deine Statistik in {system_name}').classes('text-3xl justify-center font-bold text-normaltext') - with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): - ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round") - ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{systemname}')) + # --- BLOCK 1 (MMR & Rang | Rangliste) --- + with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"): + with ui.column().classes("w-full gap-4"): + with ui.card().classes("w-full items-center justify-center text-center"): + with ui.row().classes("w-full items-center text-center"): + ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext') + ui.space() + info_system.create_info_button("mmr_info") + ui.label(str(player_stats["mmr"])).classes('text-4xl font-bold text-accent') - with ui.column().classes('w-full items-center justify-center mt-10'): - ui.label(f'Deine Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext') + with ui.card().classes("w-full items-center justify-center text-center"): + with ui.row().classes("w-full items-center text-center"): + ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext') + ui.space() + info_system.create_info_button("rang_info") + ui.label(str(data_api.get_player_rank(player_id,system_id))).classes('text-4xl font-bold text-blue-100') - # --- BLOCK 1 (MMR & Rang | Rangliste) --- - leaderboard_data = data_api.get_leaderboard(systemname) - table_rows = [] - my_rank = "-" + + with ui.card().classes("w-full lg:col-span-2 max-h-[50vh] overflow-y-auto"): + ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2 top-0 bg-zinc-900 z-10") + Leaderboard(system_name) - for index, player in enumerate(leaderboard_data): - current_rank = index + 1 - if player['id'] == player_id: - my_rank = current_rank - table_rows.append({ - 'rank': current_rank, - 'trend': '➖', - 'name': f"{player['display_name']} 'aka' {player['discord_name']}", - 'mmr': player['mmr'] - }) + # --- BLOCK 2 (5 Karten) --- + with ui.card().classes("w-full"): + with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"): - table_columns = [ - {'name': 'rank', 'label': '#', 'field': 'rank', 'align': 'left'}, - {'name': 'trend', 'label': 'Trend', 'field': 'trend', 'align': 'center'}, - {'name': 'name', 'label': 'Spieler', 'field': 'name', 'align': 'left'}, - {'name': 'mmr', 'label': 'MMR', 'field': 'mmr', 'align': 'left'}, - ] + with ui.card().classes("items-center justify-center text-center"): + ui.label("Spiele: ").classes('text-2xl font-bold') + ui.label(str(player_stats["games_in_system"])).classes('text-4xl font-bold text-blue-100') - with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"): - with ui.column().classes("w-full gap-4"): - with ui.card().classes("w-full items-center justify-center text-center"): - with ui.row().classes("w-full items-center text-center"): - ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext') - ui.space() - info_system.create_info_button("mmr_info") - ui.label(str(mmr)).classes('text-4xl font-bold text-accent') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold') + ui.label(str(player_stats["avv_points"])).classes('text-4xl font-bold text-blue-100') - with ui.card().classes("w-full items-center justify-center text-center"): - with ui.row().classes("w-full items-center text-center"): - ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext') - ui.space() - info_system.create_info_button("rang_info") - ui.label(str(my_rank)).classes('text-4xl font-bold text-blue-100') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Win-Rate: ").classes('text-2xl font-bold') + ui.label("-").classes('text-4xl font-bold text-blue-100') - with ui.card().classes("w-full lg:col-span-2"): - ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2") - ui.table(columns=table_columns, rows=table_rows, row_key='rank').classes('w-full bg-zinc-900 text-white') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Letztes Spiel am: ").classes('text-2xl font-bold') + ui.label(str(player_stats["last_played"])).classes('text-4xl font-bold text-blue-100') - # --- BLOCK 2 (5 Karten) --- - with ui.card().classes("w-full"): - with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"): + with ui.card().classes("items-center justify-center text-center"): + ui.label("Win-Streak: ").classes('text-2xl font-bold') + ui.label("-").classes('text-4xl font-bold text-blue-100') - with ui.card().classes("items-center justify-center text-center"): - ui.label("Spiele: ").classes('text-2xl font-bold') - ui.label(str(games)).classes('text-4xl font-bold text-blue-100') + # --- BLOCK 3 (3 Karten) --- + with ui.card().classes("w-full"): + with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"): - with ui.card().classes("items-center justify-center text-center"): - ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold') - ui.label(str(avv_points)).classes('text-4xl font-bold text-blue-100') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Dein Nemesis: ").classes('text-2xl font-bold') + ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100') - with ui.card().classes("items-center justify-center text-center"): - ui.label("Win-Rate: ").classes('text-2xl font-bold') - ui.label("-").classes('text-4xl font-bold text-blue-100') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold') + ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100') - with ui.card().classes("items-center justify-center text-center"): - ui.label("Letztes Spiel am: ").classes('text-2xl font-bold') - ui.label(last_played).classes('text-4xl font-bold text-blue-100') - - with ui.card().classes("items-center justify-center text-center"): - ui.label("Win-Streak: ").classes('text-2xl font-bold') - ui.label("-").classes('text-4xl font-bold text-blue-100') - - # --- BLOCK 3 (3 Karten) --- - with ui.card().classes("w-full"): - with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"): - - with ui.card().classes("items-center justify-center text-center"): - ui.label("Dein Nemesis: ").classes('text-2xl font-bold') - ui.label("-").classes('text-4xl font-bold text-blue-100') - - with ui.card().classes("items-center justify-center text-center"): - ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold') - ui.label("-").classes('text-4xl font-bold text-blue-100') - - with ui.card().classes("items-center justify-center text-center"): - ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold') - ui.label("-").classes('text-4xl font-bold text-blue-100') - - else: - with ui.column().classes('w-full items-center justify-center mt-10'): - ui.label(f'Keine Statistik für "{systemname}" gefunden.').classes('text-red-500 text-xl mt-4') + with ui.card().classes("items-center justify-center text-center"): + ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold') + ui.label(str(player_stats["pushover_id"])).classes('text-4xl font-bold text-blue-100') \ No newline at end of file diff --git a/gui/league_statistic_visitor_gui.py b/gui/league_statistic_visitor_gui.py new file mode 100644 index 0000000..5db40c8 --- /dev/null +++ b/gui/league_statistic_visitor_gui.py @@ -0,0 +1,24 @@ +from nicegui import ui, app +from gui import gui_style +from data import data_api +from gui.info_text import info_system + +from gui.templates.leaderboard import Leaderboard + + + +def setup_routes(): + @ui.page('/statistic/visitor/{systemname}', dark=True) + def gamesystem_statistic_visitor_page(systemname: str): + if not app.storage.user.get('authenticated', False): + ui.navigate.to('/') + return + + gui_style.apply_design() + + with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): + ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round") + + with ui.column().classes('w-full items-center justify-center mt-10'): + ui.label(f'Die aktuelle Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext') + Leaderboard(systemname) \ No newline at end of file diff --git a/gui/main_gui.py b/gui/main_gui.py index b088ee9..19052c4 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -21,7 +21,9 @@ def setup_routes(admin_discord_id): ui.navigate.reload() return -# ----------------------------------------- + + + # --------------------------- # --- NAVIGATIONSLEISTE (HEADER) # --------------------------- @@ -31,19 +33,19 @@ def setup_routes(admin_discord_id): # --- LINKE SEITE --- # Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1) with ui.row().classes('items-center'): - ui.image("gui/pictures/wsdg.png").classes('w-15 h-15 rounded-full') - ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext') + ui.image("gui/pictures/wsdg.png").classes('w-12 h-12 md:w-25 md:h-25 rounded-full') + ui.label('Westside Diceghost Liga').classes('text-lg md:text-3xl font-bold text-normaltext') # --- MITTE --- if app.storage.user.get('authenticated', False): discord_id = app.storage.user.get("discord_id") if discord_id == admin_discord_id: - ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).props("round") + ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).classes('w-2 h-2') # --- RECHTE SEITE --- if app.storage.user.get('authenticated', False): with ui.row().classes('items-center gap-4'): - ui.image(app.storage.user.get('discord_avatar_url')).classes('w-15 h-15 rounded-full') + ui.image(app.storage.user.get('discord_avatar_url')).classes('w-10 h-10 md:w-25 md:h-25 rounded-full border-1 border-red-900') discord_name = app.storage.user.get('discord_name') display_name = app.storage.user.get('display_name') player_id = app.storage.user.get('db_id') @@ -57,10 +59,10 @@ def setup_routes(admin_discord_id): # --- ANSICHT 1: Der normale Text mit Edit-Button --- with ui.column().classes('items-center gap-0') as display_row: with ui.column(): - ui.label(display_name).classes('text-xl font-bold text-normaltext') + ui.label(display_name).classes('md:text-3xl font-bold text-normaltext') with ui.row().classes("items-center justify-between"): - ui.label("'aka'").classes('text-sm text-italic text-infotext') - ui.label(discord_name).classes('text-m text-bold text-infotext') + ui.label("'aka'").classes('md:text-lg text-xs text-italic text-infotext') + ui.label(discord_name).classes('text-sm md:text-xl text-bold text-infotext') edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense') # --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) --- @@ -201,19 +203,25 @@ def setup_routes(admin_discord_id): sys_description = sys['description'] sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors") - - with sys_card: - ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext') - if sys_logo: - ui.image(f"/pictures/{sys_logo}").classes("w-60") - if sys_description: - ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext') + with sys_card: + # Wir erstellen einen Container für den "klickbaren" Teil + clickable_content = ui.column().classes("w-full h-full items-center") + + with clickable_content: + ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext') + if sys_logo: + ui.image(f"/pictures/{sys_logo}").classes("w-60") + if sys_description: + ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext') # Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR? if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None: ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold") + clickable_content.classes("cursor-pointer hover:bg-zinc-800") + clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/visitor/{name}')) + join_row = ui.row().classes('items-center gap-2') confirm_row = ui.row().classes('items-center gap-2') confirm_row.visible = False @@ -225,11 +233,12 @@ def setup_routes(admin_discord_id): ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=sys_id: click_join_league(p, s)) ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense') else: - # Spieler IST in der Liga! - stat = my_stats[sys_id] # Wir holen uns seine Stats aus dem Wörterbuch + # Spieler IST in der Liga + stat = my_stats[sys_id] - sys_card.classes("cursor-pointer hover:bg-zinc-800") - sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}')) + # JETZT machen wir nur den Container klickbar, nicht die ganze Karte! + clickable_content.classes("cursor-pointer hover:bg-zinc-800") + clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}')) with ui.row().classes('items-center gap-4'): ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext") diff --git a/gui/match_gui.py b/gui/match_gui.py index 0873928..3446af5 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -33,19 +33,12 @@ def setup_routes(): raw_players = data_api.get_all_players_from_system(system_name) my_id = app.storage.user.get('db_id') - def add_point(): - p1_points.value += 1 - - def sub_point(): - p1_points.value -= 1 - - with ui.row().classes("w-full items-center justify-between"): - p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35") - with ui.column().classes("items-center justify-between"): - # Punkte Up- Down- Buttons. und Textanzeige - ui.button(icon="expand_less", on_click=add_point) - ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext") - ui.button(icon="expand_more", on_click=sub_point) + with ui.column().classes("items-center justify-between"): + p1_points = ui.number(placeholder="Punkte eingeben", + min=system_data["min_score"], + max=system_data["max_score"], + precision=0 + ).classes("w-35").props("clearable") with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'): dropdown_options = {} @@ -56,36 +49,39 @@ def setup_routes(): opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full') - def add_point(): - p2_points.value += 1 - - def sub_point(): - p2_points.value -= 1 - - with ui.row().classes("w-full items-center justify-between"): - p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35") - with ui.column().classes("items-center justify-between"): - # Punkte Up- Down- Buttons. und Textanzeige - ui.button(icon="expand_less", on_click=add_point) - ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext") - ui.button(icon="expand_more", on_click=sub_point) + with ui.column().classes("items-center justify-between"): + p2_points = ui.number(placeholder="Punkte eingeben", + min=system_data["min_score"], + max=system_data["max_score"], + precision=0 + ).classes("w-35").props("clearable") - # Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern. + # Das Match in die Datenbank eintragen lassen. def input_match_to_database(): p2_id = opponent_select.value + score_p1 = p1_points.value + score_p2 = p2_points.value + + + # Check der Eingegebenen Daten. + # Fehler wenn kein Spieler oder keine Punkte eingegeben werden. Das Punkte Eingabefeld fängt mit min_score und max_score ab, + # ob für das Spielsystem legale Punkte eingetragen wurden. if p2_id is None: ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top") return - score_p1 = p1_points.value - score_p2 = p2_points.value + if score_p1 is None: + ui.notify("Ungültige Punkteeingabe Spieler 1!", color="red", position="top") + return + if score_p2 is None: + ui.notify("Ungültige Punkteeingabe Spieler 2!", color="red", position="top") + return + match_id = data_api.add_new_match(system_name, my_id, p2_id, score_p1, score_p2) - # 4. Erfolgsmeldung und Berechnung - ui.notify("Match erfolgreich eingetragen!", color="green") ui.navigate.to(f'/statistic/{system_name}') # Buttons ganz unten in einer Reihe diff --git a/gui/pictures/achievments/AchievementIcon_128_37.png b/gui/pictures/achievments/AchievementIcon_10_King1.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_37.png rename to gui/pictures/achievments/AchievementIcon_10_King1.png diff --git a/gui/pictures/achievments/AchievementIcon_128_38.png b/gui/pictures/achievments/AchievementIcon_11_King2.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_38.png rename to gui/pictures/achievments/AchievementIcon_11_King2.png diff --git a/gui/pictures/achievments/AchievementIcon_128_01.png b/gui/pictures/achievments/AchievementIcon_128_01.png index 4c63aa4..6486a5b 100644 Binary files a/gui/pictures/achievments/AchievementIcon_128_01.png and b/gui/pictures/achievments/AchievementIcon_128_01.png differ diff --git a/gui/pictures/achievments/AchievementIcon_128_02.png b/gui/pictures/achievments/AchievementIcon_128_02.png index bbd541c..0ddbef6 100644 Binary files a/gui/pictures/achievments/AchievementIcon_128_02.png and b/gui/pictures/achievments/AchievementIcon_128_02.png differ diff --git a/gui/pictures/achievments/AchievementIcon_128_03.png b/gui/pictures/achievments/AchievementIcon_128_03.png index b53a654..b4552eb 100644 Binary files a/gui/pictures/achievments/AchievementIcon_128_03.png and b/gui/pictures/achievments/AchievementIcon_128_03.png differ diff --git a/gui/pictures/achievments/AchievementIcon_128_43.png b/gui/pictures/achievments/AchievementIcon_128_43.png deleted file mode 100644 index 6486a5b..0000000 Binary files a/gui/pictures/achievments/AchievementIcon_128_43.png and /dev/null differ diff --git a/gui/pictures/achievments/AchievementIcon_128_44.png b/gui/pictures/achievments/AchievementIcon_128_44.png deleted file mode 100644 index 0ddbef6..0000000 Binary files a/gui/pictures/achievments/AchievementIcon_128_44.png and /dev/null differ diff --git a/gui/pictures/achievments/AchievementIcon_128_45.png b/gui/pictures/achievments/AchievementIcon_128_45.png deleted file mode 100644 index b4552eb..0000000 Binary files a/gui/pictures/achievments/AchievementIcon_128_45.png and /dev/null differ diff --git a/gui/pictures/achievments/AchievementIcon_128_39.png b/gui/pictures/achievments/AchievementIcon_12_King3.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_39.png rename to gui/pictures/achievments/AchievementIcon_12_King3.png diff --git a/gui/pictures/achievments/AchievementIcon_128_49.png b/gui/pictures/achievments/AchievementIcon_1_EinAnfang.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_49.png rename to gui/pictures/achievments/AchievementIcon_1_EinAnfang.png diff --git a/gui/pictures/achievments/AchievementIcon_128_50.png b/gui/pictures/achievments/AchievementIcon_2_Erprobt.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_50.png rename to gui/pictures/achievments/AchievementIcon_2_Erprobt.png diff --git a/gui/pictures/achievments/AchievementIcon_128_51.png b/gui/pictures/achievments/AchievementIcon_3_SchlachtErfahren.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_51.png rename to gui/pictures/achievments/AchievementIcon_3_SchlachtErfahren.png diff --git a/gui/pictures/achievments/AchievementIcon_4_1Sieg.png b/gui/pictures/achievments/AchievementIcon_4_1Sieg.png new file mode 100644 index 0000000..4c63aa4 Binary files /dev/null and b/gui/pictures/achievments/AchievementIcon_4_1Sieg.png differ diff --git a/gui/pictures/achievments/AchievementIcon_5_5Sieg.png b/gui/pictures/achievments/AchievementIcon_5_5Sieg.png new file mode 100644 index 0000000..bbd541c Binary files /dev/null and b/gui/pictures/achievments/AchievementIcon_5_5Sieg.png differ diff --git a/gui/pictures/achievments/AchievementIcon_6_10Sieg.png b/gui/pictures/achievments/AchievementIcon_6_10Sieg.png new file mode 100644 index 0000000..b53a654 Binary files /dev/null and b/gui/pictures/achievments/AchievementIcon_6_10Sieg.png differ diff --git a/gui/pictures/achievments/AchievementIcon_128_40.png b/gui/pictures/achievments/AchievementIcon_7_Kingslayer1.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_40.png rename to gui/pictures/achievments/AchievementIcon_7_Kingslayer1.png diff --git a/gui/pictures/achievments/AchievementIcon_128_41.png b/gui/pictures/achievments/AchievementIcon_8_Kingslayer2.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_41.png rename to gui/pictures/achievments/AchievementIcon_8_Kingslayer2.png diff --git a/gui/pictures/achievments/AchievementIcon_128_42.png b/gui/pictures/achievments/AchievementIcon_9_Kingslayer3.png similarity index 100% rename from gui/pictures/achievments/AchievementIcon_128_42.png rename to gui/pictures/achievments/AchievementIcon_9_Kingslayer3.png diff --git a/gui/templates/leaderboard.py b/gui/templates/leaderboard.py new file mode 100644 index 0000000..8bb0d8b --- /dev/null +++ b/gui/templates/leaderboard.py @@ -0,0 +1,24 @@ +from nicegui import ui +from data import data_api +from gui.templates.player_card import PlayerCard + +class Leaderboard: + def __init__(self, system_name: str): + # Daten holen + leaderboard_data = data_api.get_leaderboard(system_name) + + # Container + with ui.column().classes('w-full gap-2') as self.container: + for index, player in enumerate(leaderboard_data): + PlayerCard( + rank=index + 1, + display_name=player['display_name'], + discord_name=player['discord_name'], + mmr=player['mmr'], + discord_avatar_url=player.get('discord_avatar_url'), + discord_id=player.get('discord_id'), + games_in_system=player.get('games_in_system', 0), + wins=player.get('wins', 0), + loss=player.get('loss', 0), + avv_points=player.get('avv_points', 0) + ) diff --git a/gui/templates/player_card.py b/gui/templates/player_card.py new file mode 100644 index 0000000..1f6f799 --- /dev/null +++ b/gui/templates/player_card.py @@ -0,0 +1,41 @@ +from nicegui import ui + +class PlayerCard: + def __init__(self, rank: int, display_name: str, discord_name: str, mmr: int, + discord_avatar_url: str = None, discord_id: str = None, + games_in_system: int = 0, wins: int = 0, loss: int = 0, avv_points: int = 0): + + # Rang links, Card rechts + with ui.row().classes('w-full items-center gap-1 flex-nowrap'): + ui.label(f'#{rank}').classes('text-2xl font-bold w-10 text-center shrink-0') + + # Player Card + with ui.card().classes("flex-1 no-shadow bg-zinc-900 min-w-0 overflow-hidden"): + # Discord Block - klickbar + if discord_id: + with ui.link(target=f'https://discord.com/users/{discord_id}', new_tab=True).classes('no-underline w-full'): + self._discord_content(discord_avatar_url, display_name, discord_name) + else: + self._discord_content(discord_avatar_url, display_name, discord_name) + + # Stats und Achievments + with ui.row().classes('w-full items-center flex-nowrap'): + with ui.row().classes('flex-2 items-center gap-2 md:gap-3 md:justify-center shrink min-w-0'): + ui.label(f'{mmr} MMR').classes('font-bold md:text-lg') + with ui.row().classes('flex-1 justify-end items-center gap-1 md:gap-3 shrink-0'): + ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15') + ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15') + ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15') + + + def _discord_content(self, avatar_url, display_name, discord_name): + short_name = display_name[:15] + '...' if len(display_name) > 15 else display_name + short_discord = discord_name[:15] + '...' if len(discord_name) > 15 else discord_name + + with ui.card().classes("w-full md:w-90 bg-indigo-950 no-shadow"): + with ui.row().classes('items-center gap-3 min-w-0'): + if avatar_url: + ui.image(avatar_url).classes('w-10 h-10 md:w-20 md:h-20 rounded-full border-red-900 border-2 shrink-0') + with ui.column().classes('gap-0 min-w-0'): + ui.label(short_name).classes('text-sm md:text-2xl font-bold whitespace-nowrap truncate') + ui.label(short_discord).classes('text-xs md:text-sm opacity-60 whitespace-nowrap truncate') diff --git a/main.py b/main.py index 78e9274..78b8600 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,13 @@ import os +import schedule from dotenv import load_dotenv from nicegui import ui, app from data import database -from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui +from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui, league_statistic_visitor_gui from wood import logger from gui.info_text import info_system +from utils import season_scheduler # 1. Lade die geheimen Variablen aus der .env Datei in den Speicher load_dotenv() @@ -33,6 +35,18 @@ match_gui.setup_routes() admin_gui.setup_routes() match_history_gui.setup_routes() imprint_gui.setup_routes() +league_statistic_visitor_gui.setup_routes() -# 4. Wir starten die NiceGUI App +# Timer der schedule regelmäßig prüft +@app.on_startup +def setup_scheduler(): + import threading + from utils.season_scheduler import run_scheduler + + thread = threading.Thread(target=run_scheduler, daemon=True) + thread.start() + + + +# 4. Starten der App Runtime auf dem Server. ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png") diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..36112a3 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/generate_migration_code.py b/migrations/generate_migration_code.py new file mode 100644 index 0000000..2ef4eaa --- /dev/null +++ b/migrations/generate_migration_code.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine, MetaData + +DATABASE_URL = "mysql+pymysql://admin:PWM_556rtX@192.168.0.113:3306/league_db" +engine = create_engine(DATABASE_URL) +metadata = MetaData() +metadata.reflect(bind=engine) + +print("Kopiere das hier in dein Alembic-Script:") +for table in metadata.tables.values(): + print(f"\n# Tabellen-Definition für: {table.name}") + print(f"op.create_table('{table.name}',") + for column in table.columns: + col_type = str(column.type) + nullable = "True" if column.nullable else "False" + print(f" sa.Column('{column.name}', sa.{col_type.split('(')[0].upper()}(), nullable={nullable}),") + print(")") diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/47a1e41aa62f_initial_setup.py b/migrations/versions/47a1e41aa62f_initial_setup.py new file mode 100644 index 0000000..9afcfa5 --- /dev/null +++ b/migrations/versions/47a1e41aa62f_initial_setup.py @@ -0,0 +1,129 @@ +"""initial_setup + +Revision ID: 47a1e41aa62f +Revises: +Create Date: 2026-05-28 08:32:26.064485 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '47a1e41aa62f' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + + # Tabellen-Definition für: achievements + op.create_table('achievements', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('description', sa.TEXT(), nullable=False), + sa.Column('icon', sa.TEXT(), nullable=True), + sa.Column('misc', sa.TEXT(), nullable=True), + ) + + # Tabellen-Definition für: gamesystems + op.create_table('gamesystems', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('picture', sa.TEXT(), nullable=True), + sa.Column('description', sa.TEXT(), nullable=True), + sa.Column('active_players', sa.INTEGER(), nullable=True), + sa.Column('min_score', sa.INTEGER(), nullable=True), + sa.Column('max_score', sa.INTEGER(), nullable=True), + ) + + # Tabellen-Definition für: matches + op.create_table('matches', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('gamesystem_id', sa.INTEGER(), nullable=True), + sa.Column('player1_id', sa.INTEGER(), nullable=True), + sa.Column('score_player1', sa.INTEGER(), nullable=True), + sa.Column('player2_id', sa.INTEGER(), nullable=True), + sa.Column('score_player2', sa.INTEGER(), nullable=True), + sa.Column('played_at', sa.TIMESTAMP(), nullable=True), + sa.Column('player1_base_change', sa.INTEGER(), nullable=True), + sa.Column('player1_khorne', sa.INTEGER(), nullable=True), + sa.Column('player1_slaanesh', sa.INTEGER(), nullable=True), + sa.Column('player1_tzeentch', sa.INTEGER(), nullable=True), + sa.Column('player1_mmr_change', sa.INTEGER(), nullable=True), + sa.Column('player2_base_change', sa.INTEGER(), nullable=True), + sa.Column('player2_khorne', sa.INTEGER(), nullable=True), + sa.Column('player2_slaanesh', sa.INTEGER(), nullable=True), + sa.Column('player2_tzeentch', sa.INTEGER(), nullable=True), + sa.Column('player2_mmr_change', sa.INTEGER(), nullable=True), + sa.Column('player2_check', sa.INTEGER(), nullable=True), + sa.Column('match_is_counted', sa.INTEGER(), nullable=True), + sa.Column('rust_factor', sa.DOUBLE(), nullable=True), + sa.Column('point_inflation', sa.DOUBLE(), nullable=True), + sa.Column('elo_factor', sa.DOUBLE(), nullable=True), + ) + + # Tabellen-Definition für: players + op.create_table('players', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('discord_id', sa.VARCHAR(), nullable=True), + sa.Column('discord_name', sa.VARCHAR(), nullable=False), + sa.Column('discord_avatar_url', sa.TEXT(), nullable=True), + sa.Column('display_name', sa.VARCHAR(), nullable=True), + ) + + # Tabellen-Definition für: player_achievements + op.create_table('player_achievements', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('player_id', sa.INTEGER(), nullable=True), + sa.Column('achievement_id', sa.INTEGER(), nullable=True), + sa.Column('earned_at', sa.TIMESTAMP(), nullable=True), + sa.Column('earned_in', sa.INTEGER(), nullable=True), + ) + + # Tabellen-Definition für: player_game_statistic + op.create_table('player_game_statistic', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('player_id', sa.INTEGER(), nullable=True), + sa.Column('gamesystem_id', sa.INTEGER(), nullable=True), + sa.Column('mmr', sa.INTEGER(), nullable=True), + sa.Column('games_in_system', sa.INTEGER(), nullable=True), + sa.Column('points', sa.INTEGER(), nullable=True), + sa.Column('avv_points', sa.INTEGER(), nullable=True), + sa.Column('last_played', sa.TIMESTAMP(), nullable=True), + sa.Column('win_rate', sa.INTEGER(), nullable=True), + sa.Column('win_streak', sa.INTEGER(), nullable=True), + sa.Column('wins', sa.INTEGER(), nullable=True), + sa.Column('loss', sa.INTEGER(), nullable=True), + sa.Column('draws', sa.INTEGER(), nullable=True), + sa.Column('trend', sa.INTEGER(), nullable=True), + sa.Column('tyrann_id', sa.INTEGER(), nullable=True), + sa.Column('pushover_id', sa.INTEGER(), nullable=True), + sa.Column('nemesis_id', sa.INTEGER(), nullable=True), + sa.Column('season_id', sa.INTEGER(), nullable=False), + ) + + # Tabellen-Definition für: seasons + op.create_table('seasons', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('gamesystem_id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('start_date', sa.DATE(), nullable=False), + sa.Column('end_date', sa.DATE(), nullable=True), + ) + + # Tabellen-Definition für: season_schedule + op.create_table('season_schedule', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('gamesystem_id', sa.INTEGER(), nullable=False), + sa.Column('start_month', sa.INTEGER(), nullable=False), + sa.Column('start_day', sa.INTEGER(), nullable=False), + ) + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 703c039..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nicegui -requests -python-dotenv -mariadb diff --git a/setup_env.sh b/setup_env.sh index 4bb87d2..7e0dfc0 100644 --- a/setup_env.sh +++ b/setup_env.sh @@ -1,15 +1,92 @@ #!/bin/bash -# 1. Erstellt die virtuelle Umgebung im Ordner ".venv" (falls sie nicht existiert) +# Sicherstellen dass das Script abbricht wenn ein Fehler passiert +# -e: Beendet das Script sofort wenn ein Befehl einen Fehlercode zurückgibt (Exit-Code != 0) +set -e + +echo "✅ Starte einrichtung der Liga Umgebung. Updates und System Abhängigkeiten werden installiert ..." + +# Prüfen ob der Liga-Service auf dem System installiert ist +# systemctl list-unit-files zeigt alle registrierten Service-Dateien +SERVICE_EXISTS=false +if systemctl list-unit-files --type service | grep -q "liga.service"; then + # Service existiert → sicher anhalten bevor Updates kommen + sudo systemctl stop liga + SERVICE_EXISTS=true + echo "✅ Liga Service wurde gestoppt." +else + # Service nicht installiert → kein Fehler, nur Info + echo "ℹ️ Liga Service nicht gefunden. Wird übersprungen." +fi + +# 1. System updaten +# apt update: Lädt die aktuellen Paketlisten vom Server herunter +# apt upgrade -y: Installiert alle verfügbaren Updates, -y bestätigt automatisch +sudo apt update && sudo apt upgrade -y + +# 2. System-Abhängigkeiten installieren +# -y: Automatisch mit "Ja" bestätigen +sudo apt install -y \ + python3 \ + python3-venv \ + python3-full \ + libmariadb3 \ + libmariadb-dev \ + alembic + +echo "✅ Updates installiert. Überprüfe Softwarestand von Git ..." + +# Git Pull: Holt die neuesten Änderungen vom Remote-Repository +# und merged sie in den lokalen Branch +git pull + +echo "✅ Softwarestand von Git aktuell. Richte Umgebung ein ..." + +# 3. Virtuelle Umgebung erstellen (falls noch nicht vorhanden) +# Wenn .venv schon existiert wird es einfach aktualisiert/überschrieben python3 -m venv .venv -# 2. Aktiviert die virtuelle Umgebung für dieses Skript +# 4. Virtuelle Umgebung aktivieren +# WICHTIG: Da wir in einem Bash-Script sind, gilt die Aktivierung nur für dieses Script source .venv/bin/activate -# 3. Aktualisiert das Installationsprogramm "pip" auf die neueste Version +echo "✅ Umgebung eingerichtet! Installiere Pakete ..." + +# 5. pip aktualisieren +# pip ist der Paketmanager für Python – sollte aktuell sein um Kompatibilitätsprobleme zu vermeiden pip install --upgrade pip -# 4. Installiert alle Pakete aus deiner "Stückliste" -pip install -r requirements.txt +# 6. Python-Pakete installieren +pip install \ + nicegui \ + requests \ + python-dotenv \ + pymysql \ + schedule \ + sqlalchemy \ + alembic + +echo "✅ Pakete installiert. Starte Liga Service ..." + +# Prüfen ob der Service vorher existierte und versuche ihn zu starten +if [ "$SERVICE_EXISTS" = true ]; then + # Service existiert bereits auf dem System → versuchen zu starten + # systemctl start gibt Exit-Code 0 bei Erfolg, != 0 bei Fehler + if sudo systemctl start liga; then + echo "✅ Liga Service erfolgreich gestartet. Alles bereit!" + else + # Service konnte nicht gestartet werden – mögliche Ursachen: + # - Konfigurationsfehler in der .service Datei + # - Python-Fehler beim Start (Import-Error, Syntax-Error etc.) + # - Port bereits belegt + # - Fehlende Berechtigungen + echo "❌ FEHLER: Liga Service konnte nicht gestartet werden!" + echo " Prüfe mit: sudo systemctl status liga" + echo " Prüfe mit: sudo journalctl -u liga --no-pager -n 50" + exit 1 + fi +else + # Service war vorher auch nicht installiert – Info an den User + echo "ℹ️ Liga Service nicht installiert. Manuell starten oder Service erst einrichten." +fi -echo "Umgebung wurde erfolgreich eingerichtet!" diff --git a/utils/season_scheduler.py b/utils/season_scheduler.py new file mode 100644 index 0000000..90e10af --- /dev/null +++ b/utils/season_scheduler.py @@ -0,0 +1,31 @@ +import schedule +import time +from datetime import date +from data import database + + +def check_new_seasons(): + today = date.today() + + conn = database.db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT id, gamesystem_id FROM season_schedule " + "WHERE start_month = %s AND start_day = %s", + (today.month, today.day) + ) + triggers = cursor.fetchall() + + print(f"[Season Check] {today} – {len(triggers)} Treffer gefunden") + + cursor.close() + conn.close() + + +def run_scheduler(): + """Läuft in einem Hintergrund-Thread.""" + schedule.every().day.at("00:01").do(check_new_seasons) + + while True: + schedule.run_pending() + time.sleep(60)