7 Commits

Author SHA1 Message Date
David Rodenkirchen 68c51a09f8 Analyze mem leak 2026-04-15 09:29:56 +02:00
dusker a53e7100da Fix mariadb health check by adding the root password 2026-04-03 22:09:39 +02:00
David Rodenkirchen 2902c6a58c add sanitized production export 2026-02-24 00:54:24 +01:00
David Rodenkirchen 4541d3763f Hotfix: Remove Session override 2026-02-24 00:33:40 +01:00
David Rodenkirchen ce45c389ef fix login not working after registration 2026-02-23 23:50:13 +01:00
David Rodenkirchen edf1d70b54 Bump to Version 0.3.5 (Sessioning Rework / Catering UI Improvement) 2026-02-23 21:46:47 +01:00
David Rodenkirchen 8b02390bee Make disabled catering items clearer 2026-02-23 21:46:06 +01:00
8 changed files with 697 additions and 104 deletions
+1 -1
View File
@@ -1 +1 @@
0.3.4
0.3.6
+2 -1
View File
@@ -21,7 +21,7 @@ services:
MARIADB_USER: ezgg_lan_manager
MARIADB_PASSWORD: Alkohol1
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-pAlkohol1"]
interval: 5s
timeout: 3s
retries: 5
@@ -30,6 +30,7 @@ services:
volumes:
- database:/var/lib/mysql
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
- ./sql:/sql
volumes:
File diff suppressed because one or more lines are too long
+50
View File
@@ -1,8 +1,15 @@
import logging
import tracemalloc
import sys
from pathlib import Path
from uuid import uuid4
import gc
import time
import threading
from collections import Counter
from datetime import datetime, UTC
from rio import App, Theme, Color, Font, ComponentPage, Session
from from_root import from_root
@@ -10,10 +17,51 @@ from from_root import from_root
from src.ezgg_lan_manager import pages, init_services, LocalDataService
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ezgg_lan_manager.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.UserSession import UserSession
tracemalloc.start(25)
logger = logging.getLogger("EzggLanManager")
def log_object_summary():
gc.collect()
objs = gc.get_objects()
counter = Counter(type(obj).__name__ for obj in objs)
timestamp = datetime.now(UTC).isoformat()
with open("memory_objects.log", "a") as f:
f.write(f"\n=== {timestamp} ===\n")
f.write(f"Total objects: {len(objs)}\n")
for name, count in counter.most_common(25):
f.write(f"{name}: {count}\n")
def log_top_allocations():
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
timestamp = datetime.now(UTC).isoformat()
with open("memory_allocations.log", "a") as f:
f.write(f"\n=== {timestamp} ===\n")
for stat in top_stats[:10]:
f.write(str(stat) + "\n")
def start_hourly_logger():
def loop():
while True:
log_object_summary()
log_top_allocations()
time.sleep(3) # 1 hour
t = threading.Thread(target=loop, daemon=True)
t.start()
if __name__ == "__main__":
start_hourly_logger()
theme = Theme.from_colors(
primary_color=Color.from_hex("ffffff"),
secondary_color=Color.from_hex("018786"),
@@ -33,6 +81,8 @@ if __name__ == "__main__":
lan_info = default_attachments[3].get_lan_info()
async def on_session_start(session: Session) -> None:
# Use this line to fake being any user without having to log in
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
await session.set_title(lan_info.name)
if session[LocalData].stored_session_token:
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
@@ -1,38 +0,0 @@
from asyncio import sleep
from rio import Text, Component, TextStyle
class AnimatedText(Component):
def __post_init__(self) -> None:
self._display_printing: list[bool] = [False]
self.text_comp = Text("")
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
if self._display_printing[0]:
return
else:
self._display_printing[0] = True
self.text_comp.text = ""
if success:
self.text_comp.style = TextStyle(
fill=self.session.theme.success_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
else:
self.text_comp.style = TextStyle(
fill=self.session.theme.danger_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
self._display_printing[0] = False
def build(self) -> Component:
return self.text_comp
@@ -46,10 +46,10 @@ class CateringSelectionItem(Component):
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(
icon="material/add",
icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
min_size=2,
color=self.session.theme.success_color,
style="plain-text",
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
style="colored-text",
on_press=lambda: self.on_add_callback(self.article_id),
is_sensitive=self.is_sensitive
),
+84 -60
View File
@@ -1,10 +1,10 @@
import logging
from asyncio import sleep, create_task
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.AnimatedText import AnimatedText
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6
@@ -13,125 +13,154 @@ logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component):
pw_1: str = ""
pw_2: str = ""
email: str = ""
user_name: str = ""
pw_1_valid: bool = True
pw_2_valid: bool = True
email_valid: bool = True
submit_button_loading: bool = False
display_text: str = ""
display_text_style: TextStyle = TextStyle()
def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
self.pw_1.is_valid = False
self.pw_2.is_valid = False
if not (self.pw_1 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
self.pw_1_valid = False
self.pw_2_valid = False
return
self.pw_1.is_valid = True
self.pw_2.is_valid = True
self.pw_1_valid = True
self.pw_2_valid = True
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True
self.email_valid = True
except EmailNotValidError:
self.email_input.is_valid = False
self.email_valid = False
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name_input.text
current_text = self.user_name
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True
self.submit_button.force_refresh()
self.submit_button_loading = True
if len(self.user_name_input.text) < 1:
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!")
self.submit_button.is_loading = False
if len(self.user_name) < 1:
await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
self.submit_button_loading = False
return
if not (self.pw_1.text == self.pw_2.text):
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!")
self.submit_button.is_loading = False
if not (self.pw_1 == self.pw_2):
await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
self.submit_button_loading = False
return
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button.is_loading = False
if len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
await self.display_animated_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button_loading = False
return
if not self.email_input.is_valid or len(self.email_input.text) < 3:
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!")
self.submit_button.is_loading = False
if not self.email_valid or len(self.email) < 3:
await self.display_animated_text(False, "E-Mail Adresse ungültig!")
self.submit_button_loading = False
return
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None:
await self.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!")
self.submit_button.is_loading = False
if await user_service.get_user(self.email) is not None or await user_service.get_user(self.user_name) is not None:
await self.display_animated_text(False, "Benutzername oder E-Mail bereits registriert!")
self.submit_button_loading = False
return
try:
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
if not new_user:
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe
logger.error(f"create_user returned: {new_user}")
raise Exception(f"create_user returned: {new_user}")
except Exception as e:
logger.error(f"Unknown error during new user registration: {e}")
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button.is_loading = False
await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button_loading = False
return
await mailing_service.send_email(
subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name_input.text},\n\n"
body=f"Hallo {self.user_name},\n\n"
f"Du hast dich erfolgreich beim EZGG-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text
receiver=self.email
)
self.submit_button.is_loading = False
await self.animated_text.display_text(True, "Erfolgreich registriert!")
self.submit_button_loading = False
await self.display_animated_text(True, "Erfolgreich registriert!")
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
async def display_animated_text(self, success: bool, text: str) -> None:
self.display_text = ""
style = TextStyle(
fill=self.session.theme.success_color if success else self.session.theme.danger_color,
font_size=0.9
)
self.display_text_style = style
_ = create_task(self._animate_text(text))
async def _animate_text(self, text: str) -> None:
for c in text:
self.display_text += c
await sleep(0.06)
def build(self) -> Component:
self.user_name_input = TextInput(
user_name_input = TextInput(
label="Benutzername",
text="",
text=self.bind().user_name,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_user_name_focus_loss
)
self.email_input = TextInput(
email_input = TextInput(
label="E-Mail Adresse",
text="",
text=self.bind().email,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_email_focus_loss
on_lose_focus=self.on_email_focus_loss,
is_valid=self.email_valid
)
self.pw_1 = TextInput(
pw_1_input = TextInput(
label="Passwort",
text="",
text=self.bind().pw_1,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_1_valid
)
self.pw_2 = TextInput(
pw_2_input = TextInput(
label="Passwort wiederholen",
text="",
text=self.bind().pw_2,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_2_valid
)
self.submit_button = Button(
submit_button = Button(
content=Text(
"Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
@@ -145,13 +174,8 @@ class RegisterPage(Component):
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed
)
self.animated_text = AnimatedText(
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2
on_press=self.on_submit_button_pressed,
is_loading=self.submit_button_loading
)
return Column(
MainViewContentBox(
@@ -166,12 +190,12 @@ class RegisterPage(Component):
margin_bottom=2,
align_x=0.5
),
self.user_name_input,
self.email_input,
self.pw_1,
self.pw_2,
self.submit_button,
self.animated_text
user_name_input,
email_input,
pw_1_input,
pw_2_input,
submit_button,
Text(self.display_text, margin_top=2, margin_left=1, margin_right=1, margin_bottom=2, style=self.display_text_style)
)
),
align_y=0,
@@ -62,7 +62,8 @@ class DatabaseService:
password=self._database_config.db_password,
db=self._database_config.db_name,
minsize=1,
maxsize=40
maxsize=40,
autocommit=True
)
except aiomysql.OperationalError:
return False