diff --git a/src/ccma/assets/CHANGELOG.json b/src/ccma/assets/CHANGELOG.json index 5831580..513fcce 100644 --- a/src/ccma/assets/CHANGELOG.json +++ b/src/ccma/assets/CHANGELOG.json @@ -29,6 +29,7 @@ "Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.", "Konfigurierbare Mindestanzeigezeit des Splash-Screens mit fünf Sekunden Standardwert ergänzt.", "Theme-unabhängigen Splash-Fortschrittsbalken in Bild-Blau mit Silberrahmen und zeitbasiertem Fortschritt eingeführt.", + "Splash-Start verteilt seine Mindestanzeigezeit als optionales Mitglieder-Delay auf den Hausmeisterlauf, um insbesondere Netzwerk-Dateisysteme zu entlasten.", "Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.", "Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.", "Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.", diff --git a/src/ccma/services/housekeeper.py b/src/ccma/services/housekeeper.py index 2aee33e..a10cc0f 100644 --- a/src/ccma/services/housekeeper.py +++ b/src/ccma/services/housekeeper.py @@ -2,7 +2,9 @@ from __future__ import annotations import copy import json +import math import os +import time from contextlib import contextmanager from dataclasses import dataclass, field from datetime import date, datetime @@ -54,8 +56,14 @@ class Housekeeper: self.state_path = repository.root / "housekeeper.json" self.lock_path = repository.root / ".housekeeper.lock" - def run(self, today: date | None = None) -> list[HousekeeperFinding]: + def run( + self, + today: date | None = None, + *, + member_delay: float = 0.0, + ) -> list[HousekeeperFinding]: current_date = today or date.today() + delay = _non_negative_delay(member_delay) with _exclusive_lock(self.lock_path): original = self._load_state() working = copy.deepcopy(original) @@ -69,7 +77,9 @@ class Housekeeper: rules = load_rules(self.repository.root) repository_config = self.repository.get_configuration() - for member_id in sorted(member_ids): + for index, member_id in enumerate(sorted(member_ids)): + if index and delay: + time.sleep(delay) try: member, contributions = self.repository.preflight_member_record(member_id) except RepositoryError as exc: @@ -405,6 +415,14 @@ def _remove_orphaned_member_items(items: dict[str, dict[str, Any]], member_ids: del items[key] +def _non_negative_delay(value: float) -> float: + try: + delay = float(value) + except (TypeError, ValueError): + return 0.0 + return max(0.0, delay) if math.isfinite(delay) else 0.0 + + @contextmanager def _exclusive_lock(path: Path): path.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/ccma/ui/splash.py b/src/ccma/ui/splash.py index 1826856..a1ff6de 100644 --- a/src/ccma/ui/splash.py +++ b/src/ccma/ui/splash.py @@ -119,10 +119,16 @@ class SplashScreen(tk.Toplevel): errors = self.repository.validate() self._messages.put(("status", "Baue Suchindex …")) self.repository.list_members() + member_count = len(self.repository.list_member_ids()) findings = [] if self.run_housekeeper: self._messages.put(("status", "Starte Hausmeister …")) - findings = Housekeeper(self.repository, self.housekeeper_settings).run() + findings = Housekeeper(self.repository, self.housekeeper_settings).run( + member_delay=_member_delay_for_splash( + self.minimum_display_seconds, + member_count, + ) + ) result = StartupResult(self.repository, errors, findings) self._messages.put(("result", result)) except Exception as exc: @@ -265,3 +271,9 @@ def _progress_value(elapsed: float, minimum_seconds: float, startup_finished: bo if startup_finished: return elapsed_ratio * 100.0 return elapsed_ratio * 95.0 + + +def _member_delay_for_splash(minimum_seconds: float, member_count: int) -> float: + if minimum_seconds <= 0 or member_count <= 0: + return 0.0 + return minimum_seconds / member_count diff --git a/tests/test_rules.py b/tests/test_rules.py index 01a1ab4..9283f26 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -4,11 +4,25 @@ from datetime import date import pytest +import ccma.services.housekeeper as housekeeper_module from ccma.rules.loader import RuleLoadError from ccma.services.housekeeper import Housekeeper from ccma.storage.repository import MemberRepository +def test_housekeeper_optionally_waits_between_members(tmp_path, monkeypatch) -> None: + repository = MemberRepository(tmp_path) + repository.initialize() + repository.create_member(first_name="First", last_name="Member") + repository.create_member(first_name="Second", last_name="Member") + delays: list[float] = [] + monkeypatch.setattr(housekeeper_module.time, "sleep", delays.append) + + Housekeeper(repository).run(today=date(2026, 6, 21), member_delay=0.25) + + assert delays == [0.25] + + def test_store_rule_overrides_builtin_rule_with_same_filename(tmp_path) -> None: repository = MemberRepository(tmp_path) repository.initialize() diff --git a/tests/test_ui_imports.py b/tests/test_ui_imports.py index 67a6eb5..38c1e42 100644 --- a/tests/test_ui_imports.py +++ b/tests/test_ui_imports.py @@ -32,7 +32,11 @@ def test_splash_position_centers_on_pointer_and_stays_on_screen() -> None: def test_splash_minimum_time_only_waits_for_remaining_duration() -> None: - from ccma.ui.splash import _progress_value, _remaining_minimum_ms + from ccma.ui.splash import ( + _member_delay_for_splash, + _progress_value, + _remaining_minimum_ms, + ) assert _remaining_minimum_ms(100.0, 5.0, 102.25) == 2750 assert _remaining_minimum_ms(100.0, 5.0, 106.0) == 0 @@ -41,6 +45,9 @@ def test_splash_minimum_time_only_waits_for_remaining_duration() -> None: assert _progress_value(5.0, 5.0, startup_finished=False) == 95.0 assert _progress_value(2.5, 5.0, startup_finished=True) == 50.0 assert _progress_value(5.0, 5.0, startup_finished=True) == 100.0 + assert _member_delay_for_splash(5.0, 10) == 0.5 + assert _member_delay_for_splash(5.0, 0) == 0.0 + assert _member_delay_for_splash(0.0, 10) == 0.0 def test_event_labels_hide_board_actor_but_keep_automatic_marker() -> None: