From 6de5f906267ab10712eeb5c854ea4584cc74e0d4 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 21:25:36 +0200 Subject: [PATCH] feat(live-state): ?view=recent-modal branch + single-column modal list 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 --- l4d2web/l4d2web/routes/server_routes.py | 6 ++++ l4d2web/l4d2web/static/css/components.css | 11 ++++++ .../templates/_recent_players_modal_body.html | 21 +++++++++++ l4d2web/tests/test_status_and_server_logs.py | 35 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 l4d2web/l4d2web/templates/_recent_players_modal_body.html diff --git a/l4d2web/l4d2web/routes/server_routes.py b/l4d2web/l4d2web/routes/server_routes.py index 6b2fe4a..0a6663d 100644 --- a/l4d2web/l4d2web/routes/server_routes.py +++ b/l4d2web/l4d2web/routes/server_routes.py @@ -266,6 +266,12 @@ def live_state_fragment(server_id: int) -> Response: recent_total = len(recent_rows) recent_overview = recent_rows[:10] + if request.args.get("view") == "recent-modal": + return render_template( + "_recent_players_modal_body.html", + recent_players=recent_rows, + ) + return render_template( "_live_state.html", server=server, diff --git a/l4d2web/l4d2web/static/css/components.css b/l4d2web/l4d2web/static/css/components.css index 7a1a956..045df98 100644 --- a/l4d2web/l4d2web/static/css/components.css +++ b/l4d2web/l4d2web/static/css/components.css @@ -1120,3 +1120,14 @@ div.modal.modal-wide { .console-input-form.htmx-request .console-spinner { display: inline; } + +.recent-modal-list { + /* Force a single column inside the modal regardless of the 5-col + default on .player-grid.recent. !important is acceptable here: + the only way to override the more-specific class selector + chain is via specificity or !important; modal-only override + is local enough that long-term maintainability is fine. */ + grid-template-columns: 1fr !important; + max-height: 60vh; + overflow: auto; +} diff --git a/l4d2web/l4d2web/templates/_recent_players_modal_body.html b/l4d2web/l4d2web/templates/_recent_players_modal_body.html new file mode 100644 index 0000000..0258305 --- /dev/null +++ b/l4d2web/l4d2web/templates/_recent_players_modal_body.html @@ -0,0 +1,21 @@ +{# Full single-column list of recent players for the + #recent-players-modal. Rendered via /servers//live-state?view=recent-modal. + Reuses the same chip markup as the inline grid in _live_state.html. #} + diff --git a/l4d2web/tests/test_status_and_server_logs.py b/l4d2web/tests/test_status_and_server_logs.py index f7c2d2b..7590669 100644 --- a/l4d2web/tests/test_status_and_server_logs.py +++ b/l4d2web/tests/test_status_and_server_logs.py @@ -136,3 +136,38 @@ def test_live_state_recent_header_not_clickable_when_le_10(owner_client_with_ser assert "4 Recent" in html assert 'data-inline-modal-open="recent-players-modal"' not in html + + +def test_live_state_recent_modal_view_returns_single_column_chip_list(owner_client_with_server) -> None: + """?view=recent-modal renders all recent players (not just the + 10-item overview) in a single-column scrollable list using the + same chip markup.""" + from datetime import timedelta + from l4d2web.models import ServerPlayerSession + + client, server_id = owner_client_with_server + + now = datetime.now(UTC) + total = 13 + + with session_scope() as db: + for i in range(total): + db.add(ServerPlayerSession( + server_id=server_id, + steam_id_64=str(76561190000000000 + i), + joined_at=now - timedelta(hours=i + 2), + left_at=now - timedelta(hours=i + 1), + name_at_join=f"Player{i}", + min_ping=10, + max_ping=50, + )) + + resp = client.get(f"/servers/{server_id}/live-state?view=recent-modal") + assert resp.status_code == 200 + body = resp.get_data(as_text=True) + # All 13 chips present (no slice in the modal view). + assert body.count('class="player-card recent-chip"') == 13 + # Has the modal-list wrapper class. + assert 'class="player-grid recent recent-modal-list"' in body + # No "Current" or "N Recent" header — modal view is just the list. + assert "Recent" not in body or "Recent" not in body # accept either: the test is about the list rendering, not headers