Adds the _recent_players_modal_body.html partial for the full recent-players
list (no 10-item cap), the route branch in live_state_fragment that renders it
when ?view=recent-modal is requested, and the .recent-modal-list CSS rule that
forces single-column layout inside the modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop .limit(20) from the recent_rows query so the full history window is
available for the future recent-players modal; derive recent_players_overview
(first 10) and recent_players_total_count from the unbounded result and pass
both into _live_state.html alongside the existing recent_players key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
load_current_user now treats a session whose pw_changed_at marker
is missing, malformed, or older than the user's current
password_changed_at as logged-out. Same shape as the existing
user.active check.
Forced fan-out updates to every test fixture that forges a session
via session_transaction(): each now stamps a current pw_changed_at
marker. test_deactivated_user_existing_session_invalidated keeps
its meaning — the deactivation still flips the user to inactive,
and load_current_user rejects the session via the user.active
branch before reaching the freshness branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stream_command used a blocking proc.stdout.readline() that never woke
when the underlying journalctl was silent, so Flask never delivered
GeneratorExit on client disconnect — the worker thread and the journalctl
child both leaked permanently and pinned the gunicorn thread pool.
Switch to a select-based read loop with a 15s heartbeat tick (yielded as
""), and translate the tick to an SSE keepalive comment in the log route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>