Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel Nagel
106c8bb5e3 Merge branch 'master' of 192.168.0.105:WestsideDiceghost/Liga-System 2026-04-07 08:44:06 +00:00
Daniel Nagel
7b119a085f Hab wieder irgendwas falsch gemacht mit git ...
Merge branch 'dev'
2026-04-07 08:39:15 +00:00
Daniel Nagel
c905464924 Bugfix im Logger bei Match Eintragen. 2026-04-07 08:36:32 +00:00
Daniel Nagel
b7f9e7a6bb Impressums Seite. Datenschutzerklärung. Servitor Basic Test funktion. 2026-04-07 08:30:53 +00:00
Daniel
b707dd96d6 Infotext 2026-03-27 18:21:50 +01:00
Daniel
397d454ebc gitignore anggepasst damit die storage-user Chache Daten ignoriert werden. 2026-03-27 18:21:24 +01:00
Daniel
f9ed925643 gitignore geändert dass die Nutzer Metadaten ignoriert werden. 2026-03-27 17:36:08 +01:00
21 changed files with 238 additions and 24 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
# 2. Python Caches
__pycache__/
*.py[cod]
.nicegui/storage*
# 3. Virtuelle Umgebungen
venv/

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N.","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Meta-Chaser","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"113708052485636100","discord_name":"staelwulf","db_id":4,"display_name":"Max","discord_avatar_url":"https://cdn.discordapp.com/avatars/113708052485636100/d53339dd6a6659231c5c16645ba258df.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Blinder Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":1,"display_name":"Daniel N.","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

15
bot/servitor.py Normal file
View File

@ -0,0 +1,15 @@
import requests
webhook_url = "https://liga-n8n.au-fab.eu/webhook/21066d30-2757-4d64-9c72-e439ecd70f94"
discord_tokken = "MTQ3NTg1ODU5OTQwNTQyNDcwMQ.GRo63W.1erAk0janBqS8NlHB6FHEbXM1bolIOPH2Uc4vs"
def send_message(event, text):
data = {
event: "Neuer Spieler",
text: "mr_teels"
}
response = requests.post(webhook_url, json=data)

View File

@ -258,7 +258,7 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2))
new_match_id = cursor.lastrowid
logger.log(f" New Match ID{new_match_id} in {system_name}. {get_player_name(player1_id)}:({score_p1}) -VS- {get_player_name(player2_id)}:({score_p2})")
logger.log(f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})")
connection.commit()
connection.close()

124
gui/imprint.json Normal file
View File

@ -0,0 +1,124 @@
{
"version": 1,
"default_locale": "de",
"locales": {
"de": {
"page": {
"title": "Info"
},
"sections": [
{
"id": "rules",
"order": 10,
"title": "Allgemeine Regeln",
"blocks": [
{
"type": "markdown",
"content": [
"Mit der Anmeldung in der Liga stimmst du folgenden Richtlinien und Regeln zu:\n",
"* Freundliches und Respektvolles Verhalten gegenüber anderen Mitspielern.",
"* Kein Cheating / absichtliches Falschmelden in die Liga! Fehler können passieren.",
"* Es giltet ein Gentleman's Agreement: 'Wir schreiben keine absichtlichen Konterlisten. Schlecht geschriebene Regeln werden nicht zu unserem Vorteil ausgenutzt!'.",
"* Wir spielen mit **Intent**. Sprich: Es werden keine Regeln der eigenen Armee dem Gegner verheimlicht um Überraschungen zu produzieren."
]
}
]
},
{
"id": "imprint",
"order": 20,
"title": "Impressum",
"blocks": [
{
"type": "markdown",
"content": "**Verantwortlich:** Daniel Nagel (Privatperson) \n**Adresse:** PRIVAT \n**Kontakt:** admin@danielnagel.at"
}
]
},
{
"id": "privacy",
"order": 30,
"title": "Datenschutz",
"blocks": [
{
"type": "markdown",
"content": [
"Die Daten die für den Betrieb der Liga nötig sind werden auf einem privaten Server in Österreich gespeichert. Das bezieht ein: \n",
"* Spieler Namen (Automatisch generiert oder Selbst eingetragen)",
"* Spieler ID in form der Discord Nutzer ID.",
"* Discord Nutzernamen",
"* Discord url Pfad zum Profilfoto",
"\nDiese Daten werden **keinem** anderen Dienst, Service, Anbieter oder Drittem zur Verfügung gestellt!",
"\nEine Löschung der eigenen Daten kann jederzeit beantragt werden wenn man nicht mehr an der Liga teilnehmen will. \n",
"Bestimmte Ereignisse oder Eingaben in die App werden in eine Log Funktion geschrieben (mit Zeitstempel) für die Qualitätssicherung und störungsfreie Funktion der App. Unter anderem: \n",
"* Alle Eingaben vom Match Formular. Inkl. Spieler der es eingegeben hat.",
"* Wenn ein Match abgelehnt oder gelöscht wird. Inkl. Spieler der es gelöscht/abgelehnt hat.",
"* Genaue Aufschlüsselung der MMR Punkte Berechnung pro eingetragenem Match.",
"* Neu angelegte Spieler und ihren Discord Namen."
]
}
]
}
]
},
"en": {
"page": {
"title": "Info"
},
"sections": [
{
"id": "rules",
"order": 10,
"title": "General Rules",
"blocks": [
{
"type": "markdown",
"content": [
"By registering for the league you agree to the following guidelines and rules:\n",
"* Friendly and respectful behaviour towards all other players.",
"* No cheating or intentional misreporting of match results! Honest mistakes can happen.",
"* A Gentleman's Agreement applies: 'We do not write intentional counter-lists. Poorly written rules are not exploited for our own advantage!'.",
"* We play with **Intent**. This means: no rules of your own army are hidden from your opponent in order to create surprises."
]
}
]
},
{
"id": "imprint",
"order": 20,
"title": "Imprint",
"blocks": [
{
"type": "markdown",
"content": "**Responsible:** Daniel Nagel (Private individual) \n**Address:** PRIVATE \n**Contact:** admin@danielnagel.at"
}
]
},
{
"id": "privacy",
"order": 30,
"title": "Privacy Policy",
"blocks": [
{
"type": "markdown",
"content": [
"Data required to operate the league is stored on a private server located in Austria. This includes: \n",
"* Player names (automatically generated or manually entered).",
"* Player ID in the form of the Discord user ID.",
"* Discord usernames.",
"* Discord URL path to the profile picture.",
"\nThis data is **not** shared with any other service, provider, or third party.",
"\nDeletion of your own data can be requested at any time if you no longer wish to participate in the league. \n",
"Certain events or inputs within the app are written to a log function (with timestamp) to ensure quality and stable operation of the app. This includes: \n",
"* All inputs from the match form, including the player who submitted it.",
"* When a match is rejected or deleted, including the player who rejected/deleted it.",
"* Detailed breakdown of the MMR point calculation per submitted match.",
"* Newly created players and their Discord names."
]
}
]
}
]
}
}
}

75
gui/imprint_gui.py Normal file
View File

@ -0,0 +1,75 @@
import json
from pathlib import Path
from nicegui import ui
from fastapi import Request
from gui import gui_style
# JSON laden - liegt laut Screenshot direkt in gui/
_JSON_PATH = Path(__file__).resolve().parent / 'imprint.json'
with open(_JSON_PATH, encoding='utf-8') as f:
imprint_data: dict = json.load(f)
# -------------------------------------------------------
SECTION_TITLES_FALLBACK = {
"howto": "Kurzanleitung",
"rules": "Allgemeine Regeln",
"imprint": "Impressum",
"privacy": "Datenschutz",
}
def _get_locale(request: Request) -> str:
default = imprint_data.get('default_locale', 'de')
lang = request.query_params.get('lang', default)
if lang not in imprint_data.get('locales', {}):
lang = default
return lang
def _get_locale_options() -> list[str]:
return list(imprint_data.get('locales', {}).keys())
def setup_routes():
@ui.page('/info', dark=True)
def info_page(request: Request):
gui_style.apply_design()
lang = _get_locale(request)
locale_data = imprint_data['locales'][lang]
page_title = locale_data.get('page', {}).get('title', 'Info')
sections = sorted(locale_data.get('sections', []), key=lambda s: s.get('order', 0))
with ui.row().classes("w-full items-center justify-between"):
ui.button(text="Zurück", on_click=lambda: ui.navigate.to('/'))
ui.label(page_title).classes("text-xl font-bold")
ui.select(
options=_get_locale_options(),
value=lang,
label='Sprache',
on_change=lambda e: ui.navigate.to(f'/info?lang={e.value}'),
).classes("min-w-[8rem]")
ui.separator().classes("my-4")
with ui.column().classes("w-full gap-4"):
for section in sections:
title = section.get('title', SECTION_TITLES_FALLBACK.get(section.get('id', ''), ''))
blocks = section.get('blocks', [])
with ui.card().classes("w-full"):
if title:
ui.label(title).classes("text-lg font-semibold mb-2")
for block in blocks:
btype = block.get('type', 'markdown')
if btype == 'markdown':
raw = block.get('content', '')
# Unterstützt sowohl String als auch Array
if isinstance(raw, list):
content = '\n'.join(raw)
else:
content = raw
ui.markdown(content).classes("prose max-w-none")
elif btype == 'divider':
ui.separator().classes("my-2")

View File

@ -12,10 +12,18 @@
"**ACHTUNG:** Ein Spieler ist nur als Gegner auswählbar wenn er sich in der Liga angemeldet hat!",
"Solltest du einen Fehler machen kannst du das 'falsche' Match auf der Hauptseite noch löschen bevor es bestätigt wurde."
],
"khorne": ["Khorne will Blut und Schädel!"],
"tzeentch": ["tzeentch Pläne gehen auf!"],
"basis_mmr": ["Berechnet mit dem MMR Unterschied."],
"mmr_calc": [
"Die Berechnung und das System dahinter sind ein wenig komplex. Ich versuche trotzdem es so einfach wie möchlich zu erklären.",
"**Khorne**: Khorne interresiert es nicht woher die Schädel kommen! hat man in den letzten 16Tagen (~2 Wochen) in dieser Liga schon gespielt, bekommt man 8 Punkte",
"**Tzeentch**: Der Herr der Intriegen mag es wenn Pläne aufgehen. Ausgehend von den Maximalen Siegpunkten gewährt er einen Bonus wenn der Gegner mit vielen Punkten besiegt wird. 0-9 Punkte",
"**Slaanesh**: Noch in Arbeit. Irgendwas mit dem ***Tyrann***, ***Prügelknabe***, ***Nemesis***. Das System steht noch nicht.",
"**Nurgle**: Der Meister der Entrophie wird den Spielern ein wenig die MMR Punkte ***verotten***. Das System steht noch nicht!",
"**Elo Faktor**: Elo beschreibt das verhältnis zwischen den Sieger MMR und Verlierer MMR. Damit wird festgestellt wer stärker und wer schwächer ist. Ein Stärkerer Spieler der einen Schwächeren besiegt, kriegt weniger MMR Punkte als umgekehrt. Die Berechnung ist etwas komplex. Führ mehr googelt bitte 'elo schach'. Der Elo Faktor bestimmt die ***Basis MMR***.",
"**Rost Faktor**: Der Rostfaktor ist ein Punktefaktor der zum Einsatz kommt wenn ein Spieler eine Weile nicht mehr gespielt hat. Ab 30 Tagen ist er 0.8 . Von da an wird er graduell weniger bis 90 Tage (0.1). Verhindert das ***eingerostete*** Spieler fertigemacht werden oder gelegentliche gute Spieler zu weit hoch schießen.",
"**Gesamt Berechnung**:",
"Sieger (w_base + w_khorne + slaanesh + tzeentch) * rust_factor).",
"Verlierer: (-l_base + l_khorne - slaanesh - tzeentch) * rust_factor)"
],
"tyrann_info": [],
"prügelknabe_info": [],
"rang_info": [

View File

@ -16,19 +16,17 @@ def setup_routes(admin_discord_id):
# Fehlt die Discord-ID (altes Cookie) ODER sagt die Datenbank, dass da was nicht stimmt?
if not discord_id or not data_api.validate_user_session(db_id, discord_id):
# Ausweis ungültig! Wir vernichten das Cookie sofort.
app.storage.user.clear()
ui.notify("Deine Sitzung ist ungültig oder abgelaufen. Bitte neu einloggen!", color="negative")
ui.navigate.reload()
return
# -----------------------------------------
# ---------------------------
# --- NAVIGATIONSLEISTE (HEADER) ---
# --- NAVIGATIONSLEISTE (HEADER)
# ---------------------------
with ui.header().classes('items-center justify-between bg-zinc-900 shadow-lg').props("reveal reveal-offset=1"):
with ui.header(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
# --- LINKE SEITE ---
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
@ -291,3 +289,5 @@ def setup_routes(admin_discord_id):
else:
ui.label("Noch keine Spiele absolviert.").classes("text-infotext italic")
with ui.footer(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
ui.button(icon="description", on_click=lambda: ui.navigate.to('/info'))

View File

@ -5,10 +5,9 @@ from match_calculations import calc_match
from gui.info_text import info_system
def setup_routes():
# 1. Die {}-Klammern definieren eine dynamische Variable in der URL
@ui.page('/add-match/{system_name}', dark=True)
def match_form_page(system_name: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf!
def match_form_page(system_name: str): # <-- Hier wird der Name des Spielsystems gefiltert.
gui_style.apply_design()
# --- SICHERHEITS-CHECK ---

View File

@ -1,6 +1,7 @@
from nicegui import ui, app
from data import data_api
from gui import gui_style
from gui.info_text.info_system import create_info_button
def setup_routes():
@ -19,6 +20,7 @@ def setup_routes():
with ui.row().classes('w-full items-center justify-between mb-6'):
ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white")
create_info_button("mmr_calc")
ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white')
raw_matches = data_api.get_match_history_log(player_id)

View File

@ -3,7 +3,7 @@ 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, admin_gui, match_history_gui
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui
from wood import logger
from gui.info_text import info_system
@ -30,11 +30,11 @@ logger.setup_log_db()
# 3. Seitenrouten aufbauen
main_gui.setup_routes(admin_discord_id)
discord_login.setup_login_routes()
league_statistic.setup_routes()
league_statistic_gui.setup_routes()
match_gui.setup_routes()
admin_gui.setup_routes()
match_history_gui.setup_routes()
imprint_gui.setup_routes()
# 4. Wir starten die NiceGUI App
ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png")