feat(live-state): compact 4-col current + 5-col recent chips + N Recent trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-17 21:07:15 +02:00
parent 9554661e5a
commit 20fb564246
No known key found for this signature in database
2 changed files with 56 additions and 19 deletions

View file

@ -1,4 +1,9 @@
<h2 class="section-title">Live state</h2>
{# Live-state partial — HTMX-polled into the state cluster on server_detail.html.
The parent .state-cluster section provides the heading context, so there is
no <h2> here. Current players have no sub-header; they sit directly under
the summary line. Recent players' header is "N Recent" and doubles as the
modal trigger when N > 10. #}
{% if not snapshot or not snapshot_fresh %}
<p class="muted">No data — server is not currently reporting.</p>
{% else %}
@ -6,17 +11,14 @@
{{ snapshot.players }}/{{ snapshot.max_players }}
{% if snapshot.hibernating %}· idle{% endif %}
· {{ snapshot.map }}
<small class="muted">
polled {{ snapshot.last_seen_at | timeago }}
</small>
<small class="muted">polled {{ snapshot.last_seen_at | timeago }}</small>
</p>
{% endif %}
{% if current_players %}
<h3 class="section-subtitle">Current players</h3>
<ul class="player-grid">
<ul class="player-grid current">
{% for session, profile in current_players %}
<li class="player-card">
<li class="player-card current-card">
<a class="player-link"
href="https://steamcommunity.com/profiles/{{ session.steam_id_64 }}"
target="_blank" rel="noopener noreferrer">
@ -25,22 +27,30 @@
{% else %}
<span class="avatar placeholder" aria-hidden="true"></span>
{% endif %}
<span class="name">{{ (profile and profile.persona_name) or session.name_at_join }}</span>
<span class="name" title="{{ (profile and profile.persona_name) or session.name_at_join }}">{{ (profile and profile.persona_name) or session.name_at_join }}</span>
</a>
<span class="meta">
joined {{ session.joined_at | timeago }}
· ping {{ session.min_ping }}-{{ session.max_ping }}ms
{{ session.joined_at | timeago }} · {{ session.min_ping }}-{{ session.max_ping }}ms
</span>
</li>
{% endfor %}
</ul>
{% endif %}
{% if recent_players %}
<h3 class="section-subtitle">Recent players</h3>
{% if recent_players_overview %}
<h3 class="recent-header">
{% if recent_players_total_count > 10 %}
<button type="button" class="recent-header-trigger"
data-inline-modal-open="recent-players-modal">
{{ recent_players_total_count }} Recent
</button>
{% else %}
{{ recent_players_total_count }} Recent
{% endif %}
</h3>
<ul class="player-grid recent">
{% for row in recent_players %}
<li class="player-card">
{% for row in recent_players_overview %}
<li class="player-card recent-chip">
<a class="player-link"
href="https://steamcommunity.com/profiles/{{ row.steam_id_64 }}"
target="_blank" rel="noopener noreferrer">
@ -49,11 +59,9 @@
{% else %}
<span class="avatar placeholder" aria-hidden="true"></span>
{% endif %}
<span class="name">{{ row.persona_name or row.name_at_join }}</span>
<span class="name" title="{{ row.persona_name or row.name_at_join }}">{{ row.persona_name or row.name_at_join }}</span>
</a>
<span class="meta">
last seen {{ row.last_seen | timeago }}
</span>
<span class="meta">· {{ row.last_seen | timeago }}</span>
</li>
{% endfor %}
</ul>

View file

@ -73,7 +73,6 @@ def test_status_precedence() -> None:
assert compute_display_state("start", "stopped") == "starting"
@pytest.mark.xfail(reason="template change in Task 3 will satisfy this", strict=True)
def test_live_state_exposes_recent_overview_and_total_count(owner_client_with_server) -> None:
"""Route must expose recent_players_overview (≤10) and recent_players_total_count.
@ -107,3 +106,33 @@ def test_live_state_exposes_recent_overview_and_total_count(owner_client_with_se
# These assertions depend on the Task 3 template changes.
assert "13 Recent" in html
assert html.count("recent-chip") <= 10
def test_live_state_recent_header_not_clickable_when_le_10(owner_client_with_server) -> None:
"""When recent player count is ≤ 10, the header must be plain text, not a modal trigger."""
from datetime import timedelta
from l4d2web.models import ServerPlayerSession
client, server_id = owner_client_with_server
now = datetime.now(UTC)
total = 4
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,
))
res = client.get(f"/servers/{server_id}/live-state")
assert res.status_code == 200
html = res.get_data(as_text=True)
assert "4 Recent" in html
assert 'data-inline-modal-open="recent-players-modal"' not in html