From 70b80d4cebfc2855ebe1ae827156cb94f738a6eb Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 21:40:20 +0200 Subject: [PATCH] fix(server-detail): tall modal heights, true recent count, re-fetch on reopen, drop dead macro + arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 1: add .modal .log-stream.tall / .console-transcript.tall → max-height 60vh so log and console modals render taller than the compact inline tab - Fix 2: replace len(recent_rows) with a select(func.count(func.distinct(...))) so recent_players_total_count reflects all matching players, not the .limit(50) cap; add test_live_state_total_count_reflects_truth_above_limit (60 sessions → "60 Recent") - Fix 3: dispatch custom modal:opened event after showModal() in both openInline and fetchAndShowRouted; switch recent-players-modal hx-trigger from "revealed" to "modal:opened from:closest dialog" so HTMX re-fetches on every open, not just first. Manual smoke-test not performed — relies on JS event dispatch + test suite; no JS test framework in repo. - Fix 4: remove dead config_field macro (value-form, never called; config_field_block is the one actually used) - Fix 5: drop unused editable parameter from config_field_block macro definition and the editable=True call on the Hostname field Co-Authored-By: Claude Sonnet 4.6 --- ...5-17-server-detail-page-redesign-design.md | 2 +- l4d2web/l4d2web/routes/server_routes.py | 10 +++++- l4d2web/l4d2web/static/css/components.css | 9 +++++ l4d2web/l4d2web/static/js/modals.js | 6 +++- l4d2web/l4d2web/templates/_macros.html | 13 +------ l4d2web/l4d2web/templates/server_detail.html | 5 ++- l4d2web/tests/test_servers.py | 34 +++++++++++++++++++ 7 files changed, 61 insertions(+), 18 deletions(-) diff --git a/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md b/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md index f8bbf24..f5e7d2f 100644 --- a/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md +++ b/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md @@ -236,7 +236,7 @@ chrome, only the body content differs. | `l4d2web/l4d2web/static/css/components.css` | `.state-cluster`, `.inspection-strip`, player-grid/chip, tabbar | | `l4d2web/l4d2web/static/js/tabs.js` (new) | Tab activation + expand-to-modal handler | | `l4d2web/l4d2web/templates/base.html` | Include `tabs.js` if scripts are listed centrally | -| `l4d2web/l4d2web/routes/page_routes.py` (`server_detail`) | Add `recent_players_overview` (sliced to 10) + total count | +| `l4d2web/l4d2web/routes/server_routes.py` (`live_state_fragment`) | Add `recent_players_overview` (sliced to 10) + total count | ## Reused, do not modify diff --git a/l4d2web/l4d2web/routes/server_routes.py b/l4d2web/l4d2web/routes/server_routes.py index 0a6663d..f427fc3 100644 --- a/l4d2web/l4d2web/routes/server_routes.py +++ b/l4d2web/l4d2web/routes/server_routes.py @@ -263,7 +263,15 @@ def live_state_fragment(server_id: int) -> Response: .limit(50) ).all() - recent_total = len(recent_rows) + recent_total = db.scalar( + select(func.count(func.distinct(ServerPlayerSession.steam_id_64))) + .where( + ServerPlayerSession.server_id == server.id, + ServerPlayerSession.left_at.is_not(None), + ServerPlayerSession.left_at >= recent_cutoff, + ~ServerPlayerSession.steam_id_64.in_(current_ids) if current_ids else True, + ) + ) recent_overview = recent_rows[:10] if request.args.get("view") == "recent-modal": diff --git a/l4d2web/l4d2web/static/css/components.css b/l4d2web/l4d2web/static/css/components.css index 045df98..2c588ee 100644 --- a/l4d2web/l4d2web/static/css/components.css +++ b/l4d2web/l4d2web/static/css/components.css @@ -1131,3 +1131,12 @@ div.modal.modal-wide { max-height: 60vh; overflow: auto; } + +/* Modal-specific overrides — the log/console modals are meant to give + the user *more* room than the inline tab. The .tall modifier opts + into that extra height when the same element is rendered inside a + .modal. */ +.modal .log-stream.tall, +.modal .console-transcript.tall { + max-height: 60vh; +} diff --git a/l4d2web/l4d2web/static/js/modals.js b/l4d2web/l4d2web/static/js/modals.js index fdc2e85..499bcf5 100644 --- a/l4d2web/l4d2web/static/js/modals.js +++ b/l4d2web/l4d2web/static/js/modals.js @@ -32,6 +32,7 @@ const dialog = typeof idOrEl === "string" ? document.getElementById(idOrEl) : idOrEl; if (dialog && typeof dialog.showModal === "function" && !dialog.open) { dialog.showModal(); + dialog.dispatchEvent(new CustomEvent("modal:opened", { bubbles: true })); } } @@ -70,7 +71,10 @@ // this swap; the newer click will win. if (currentRoutedPath !== path) return; const dlg = document.getElementById("modal-container"); - if (dlg && !dlg.open) dlg.showModal(); + if (dlg && !dlg.open) { + dlg.showModal(); + dlg.dispatchEvent(new CustomEvent("modal:opened", { bubbles: true })); + } }).catch((err) => { console.error("[modals] routed fetch failed", err); }); diff --git a/l4d2web/l4d2web/templates/_macros.html b/l4d2web/l4d2web/templates/_macros.html index 5473387..5850a71 100644 --- a/l4d2web/l4d2web/templates/_macros.html +++ b/l4d2web/l4d2web/templates/_macros.html @@ -1,17 +1,6 @@ -{# Reusable field-cell for the server-detail config grid and any future - key/value layouts. `value` is rendered as-is (caller can pass safe - HTML via `|safe` if needed). `editable` is a flag the caller may - use to switch rendering — currently informational only. #} -{% macro config_field(label, value, editable=False) %} -
-
{{ label }}
-
{{ value }}
-
-{% endmacro %} - {# Block form of config_field: lets the caller write the value as a Jinja template body, which keeps auto-escaping active for interpolated values. #} -{% macro config_field_block(label, editable=False) %} +{% macro config_field_block(label) %}
{{ label }}
{{ caller() }}
diff --git a/l4d2web/l4d2web/templates/server_detail.html b/l4d2web/l4d2web/templates/server_detail.html index 78094fc..9932680 100644 --- a/l4d2web/l4d2web/templates/server_detail.html +++ b/l4d2web/l4d2web/templates/server_detail.html @@ -38,7 +38,7 @@ {% endcall %} - {% call macros.config_field_block("Hostname", editable=True) %} + {% call macros.config_field_block("Hostname") %}
@@ -143,9 +143,8 @@
diff --git a/l4d2web/tests/test_servers.py b/l4d2web/tests/test_servers.py index 0f99fbc..b01d1b4 100644 --- a/l4d2web/tests/test_servers.py +++ b/l4d2web/tests/test_servers.py @@ -571,6 +571,40 @@ def test_live_state_fragment_renders_current_and_recent(user_client_with_bluepri assert "steamcommunity.com/profiles/76561198021234567" in html +def test_live_state_total_count_reflects_truth_above_limit(user_client_with_blueprints) -> None: + """recent_players_total_count must reflect all matching distinct steam IDs, + not just the 50 returned by the capped query.""" + from datetime import timedelta + from l4d2web.models import Server, ServerPlayerSession + + client, data = user_client_with_blueprints + now = datetime.now(UTC) + with session_scope() as db: + srv = Server( + user_id=data["user_id"], blueprint_id=data["blueprint_id"], + name="bigsrv", port=27820, rcon_password="x", actual_state="running", + ) + db.add(srv); db.flush() + srv_id = srv.id + for i in range(60): + steam_id = f"7656119800000{i:04d}" + db.add(ServerPlayerSession( + server_id=srv_id, + steam_id_64=steam_id, + 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, + )) + + res = client.get(f"/servers/{srv_id}/live-state") + assert res.status_code == 200 + html = res.get_data(as_text=True) + assert "60 Recent" in html, "expected true count (60), not capped count (50)" + assert "50 Recent" not in html + + def test_reset_operation_enqueues_job_and_stops_desired_state(user_client_with_blueprints) -> None: from sqlalchemy import select