Commit graph

524 commits

Author SHA1 Message Date
mwiegand
aabe57b767
test(pages): update assertions stale from prior UI refactors
Two pre-existing failures unrelated to autoscroll, both from earlier
template redesigns that the corresponding tests never tracked:

test_server_actions_fragment_polls_while_running
  Asserted data-sse-url="/jobs/<id>/stream" in the actions fragment.
  That streaming <pre> was removed when the inline job-log moved into
  #job-log-modal (see test_server_detail_no_inline_job_log_pre).
  Replace with the modal-open trigger assertion.

test_workshop_overlay_refresh_button_hidden_during_build
  Used "building…" label as the positive companion that the
  build-running guard fired. The workshop overlay path now includes
  _overlay_build_status.html with omit_badge=True, so that label
  isn't rendered. Switch to hx-trigger="every 2s", which the partial
  emits unconditionally while a build is running.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:56:28 +02:00
mwiegand
8f5306db09
fix(server-detail): scroll the actual container, not the autoscroll target
The inline Log tab uses .tab-pane (height:18rem, overflow:auto) as its
scroll container. .log-stream has overflow:auto too but max-height:none
in tab-pane context, so it grows to fit and scrollHeight === clientHeight
— setting scrollTop on the <pre> was a no-op.

scrollAutoscrollTargets now walks up from each [data-autoscroll] target
until it finds an element whose CSS allows scrolling AND whose content
is actually overflowing (scrollHeight > clientHeight). sse.js delegates
to the same helper so per-line log appends scroll the right container.

e2e: new test asserts the .tab-pane is pinned to its bottom after 200
log lines are injected and the helper runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:53:06 +02:00
mwiegand
0307416b92
test(e2e): console transcript pinned to bottom on tab + submit
Adds server_with_console_history fixture (30 seeded CommandHistory rows)
and two Playwright tests that verify the inline Console transcript is
scrolled to its bottom when the Console tab is activated and after a
command is submitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:29:05 +02:00
mwiegand
06a358943e
feat(server-detail): pin Console-modal transcript on modal:opened
The console-modal transcript was not autoscrolled to the bottom on open
because tabs.js called dlg.showModal() directly, bypassing modals.js's
openInline() which dispatches the modal:opened CustomEvent. Fixed by
routing the expand-button open through window.modals.openInline() when
available.

Added inline script in server_detail.html that listens for modal:opened
on #console-modal and calls scrollAutoscrollTargets via requestAnimationFrame
so the browser has committed dialog layout before scrollHeight is read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:00:16 +02:00
mwiegand
c50b6bff29
feat(server-detail): pin transcripts/logs to bottom on tab activation
Call scrollAutoscrollTargets on the newly-visible pane when a tab is
activated, so console transcripts and log streams scroll to the bottom
after being revealed (their scrollHeight was 0 while hidden). Also adds
data-autoscroll to the three log-stream <pre> elements so they
participate in the same anchor logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:52:28 +02:00
mwiegand
02e44a04d3
feat(console): scrollAutoscrollTargets walks ancestors; expose on window
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:50:22 +02:00
mwiegand
35dfb6dd1f
feat(server-detail): cap inline console to 20 newest; modal keeps 50
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:49:12 +02:00
mwiegand
39963db2e3
docs(server-detail): implementation plan for console/log autoscroll
5 tasks, TDD style:
 1. server-side slice console_history_overview = console_history[-20:]
 2. generalise scrollConsolesToBottom -> scrollAutoscrollTargets (ancestor walk)
 3. tabs.js pins autoscroll targets on tab activation
 4. console-modal pin on modal:opened event
 5. e2e: pin-to-bottom on tab + on command submit

Bug confirmed in live browser before plan was written: after switching
to the Console tab on /servers/1, transcript scrollTop=0 with
bottomDistance=1873px (top-of-history visible, not newest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:41:42 +02:00
mwiegand
2415885d30
docs(server-detail): spec — console/log autoscroll + inline-history cap
Three usability fixes for the inspection strip on server_detail:
1. Pin transcripts/logs to bottom on tab activation.
2. Cap inline Console to 20 entries; modal keeps 50.
3. Pin to bottom after a console-line is appended via HTMX.

Approach B: single data-autoscroll opt-in attribute + one helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:29:51 +02:00
mwiegand
058acb9c5c
feat(files-overlay): recursive directory delete + fix nested-file save misroute
Two related fixes to the overlay file manager, found in the same session.

1. Nested-file save was silently moving the file (fix).
   The Filename input is pre-filled with the full relative path
   (intentional: phone-friendly move-via-edit when drag-and-drop
   isn't an option). The save handler compared it against the
   basename only, so every save of a file in a subfolder built
   newPath = parent + "/" + editedFilename and re-routed the
   file into a doubly-nested path (e.g. cfg/tick60.cfg ->
   cfg/cfg/tick60.cfg). All three sites in editor.js now compare
   against relPath. Two e2e tests pin both directions: save-without-
   edit leaves the file untouched, edit-the-path performs the
   intended move.

2. Recursive directory delete + visible 409 errors (feature).
   GET /files/delete_preview enumerates what a recursive delete
   would remove (files + dirs + symlinks, capped at 500 entries,
   followlinks=False). POST /files/delete accepts an optional
   recursive=1 form param that uses shutil.rmtree (default still
   refuses non-empty dirs, preserving the historical safety
   guard). The delete confirm modal now opens an inline preview
   for non-empty folders, with a scrollable list and a count
   summary. The error handler falls back to r.rawText so the
   server's text bodies (like "directory is not empty") finally
   surface to the user instead of "HTTP 409".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:49:45 +02:00
mwiegand
122e0abddd
fix(log-streaming): point logaddress at non-loopback IP
Reversed the wrong conclusion in 46bba0d. The vanilla L4D2 logaddress
UDP path is NOT broken — proved by retesting with destination
172.30.0.5:28000 (wireguard IP) which yielded 8 properly framed HL
Log Standard packets in 12s including real game events.

Root cause: the Source engine silently drops logaddress destinations
in 127.0.0.0/8. Registration succeeds and the cvar API reports
"logging to: udp" but sendto is never called for loopback. Every
other L4D2 stats deployment (multiple production HLstatsX:CE
instances) puts the collector on a separate host or interface IP
and never hits this.

Defaults: LOG_LISTENER_BIND=0.0.0.0:28000 (accept on any interface);
LOG_LISTENER_ADDR="" (production must set via web.env to the host's
non-loopback IP). Empty default = safe no-op for dev. The kernel's
same-host routing optimization keeps the traffic on lo internally
but the packet's destination IP must not literally be in 127/8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 00:31:45 +02:00
mwiegand
46bba0d134
docs(log-streaming): record that L4D2 logaddress UDP emit is dead
Investigation 2026-05-20: deployed listener captures nothing in
production. Diagnostics (strace + tcpdump) prove srcds makes ZERO
sendto calls toward registered logaddress destinations even though
the cvar API reports "logging to udp" and logaddress_list shows
the entry. File and console sinks work fine; the UDP path is
silently stubbed at the engine level (same family as broken L4D2
SourceTV). Listener and cfg injection retained for a future
SourceMod bridge that uses LogMessage() — that path does reach
UDP destinations on other Source 1 games.

Also drops mp_logdetail 3 (CS-only; L4D2 prints "Unknown command").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 00:19:50 +02:00
mwiegand
730ef09967
feat(log-streaming): enable srcds log streaming + temp UDP capture listener
Every managed server now auto-injects log on / mp_logdetail 3 / logaddress_add
into its generated server.cfg, streaming HL Log Standard events to a UDP
listener bundled with l4d2web. The listener is deliberately capture-only —
raw packets land in flat files per source address — so we can observe what
L4D2 actually emits on our servers before committing to a schema or event
vocabulary. Match/round/event model is a Phase 2 plan informed by that data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:22:00 +02:00
mwiegand
188fe546ed
style(overlays): inline build-status badge in workshop actions row
Moves the build-status badge (e.g. "ok" / "never built") into the
workshop section's .table-actions row, paired with the Refresh from
Steam button via a new .table-actions-end flex grouping. Drops the
now-redundant "Add items" field-label (aria-label preserves a11y).
The _overlay_build_status.html partial gains an omit_badge flag so
the workshop block can render the badge inline without it duplicating
inside the partial below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:54:01 +02:00
mwiegand
9763b8980c
style(overlays): refresh button as secondary; show disabled state
Two polish fixes caught in browser smoke after fa394c1:

1. Refresh from Steam is now class="button-secondary" — the brainstorm
   consensus was "normal-styled button" so it doesn't compete with the
   Add CTA, but left4me's default <button> is primary-blue, so the
   class is required to opt out.

2. Global button:disabled rule added: opacity 0.5 + cursor not-allowed.
   Was missing entirely, so the disabled attribute on Refresh-when-empty
   produced no visible change. Generic, helps any future <button disabled>
   too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:42:08 +02:00
mwiegand
a18e96eec9
fix(css): zero padding on custom radio + switch inputs
The global input rule (components.css:53-61) applies padding to ALL
inputs, including type=radio and type=checkbox. That inflated the new
.radio-row and .switch-row controls from 16x16px boxes into 40x32px
rectangles, which border-radius then rendered as ovals.

Adds padding: 0 + box-sizing: border-box to both control rules, and
border: 0 to the switch (the global border rule was squaring off its
pill shape). Caught in browser smoke after fa394c1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:40:39 +02:00
mwiegand
fa394c1f7a
style(overlays): redesign workshop items section
Drops the input-mode radio; the single textarea now accepts any mix of
items and collection URLs (backend autodetect landed in 5c56f18).
Refresh button moves below the items table into a .table-actions row
that also shows an item count + total size summary. Adds .workshop-input
mono font rule and a _humanize_bytes helper alongside the overlay_detail
view.

Plan deviation: PageAssertions has no not_to_contain_text method, so
the e2e test scopes those checks to a body locator instead. Caught in
review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:35:38 +02:00
mwiegand
34b65fcbbe
style(overlays): redesign create-overlay modal
Reorders fields to Name → Type → System-wide. Drops the legacy fieldset
border and the now-stale "path is generated automatically" hint. Type
radios use the new .radio-row vocabulary with always-visible
descriptions; the admin-only system-wide checkbox becomes a .switch-row
toggle. Form field names are unchanged, so the overlay-creation handler
is untouched.

Plan deviation: live_server e2e fixture now also sets LEFT4ME_ROOT.
This is required because the new test creates an overlay end-to-end via
the UI, and create_overlay_directory() writes under $LEFT4ME_ROOT,
which defaults to /var/lib/left4me (unwritable on dev machines).
The two existing live_server consumers (test_editor, test_smoke) only
visit /blueprints/<id> routes that don't touch the filesystem, so this
change is safe for them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:16 +02:00
mwiegand
6cce8b7be7
feat(css): add .field/.radio-row/.switch-row/.table-actions primitives
Reusable form-control primitives used by the upcoming overlay create-modal
and workshop-section redesigns. Custom-styled radios and a switch built
from native inputs (no JS), so accessibility and form behavior come for
free. Tokens unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:28:33 +02:00
mwiegand
5c56f18d0c
refactor(workshop): autodetect collections; drop input_mode form field
add_items now always calls expand_collections after parsing input, so a
single textarea accepts any mix of item IDs/URLs and collection IDs/URLs
without a mode toggle. The legacy "items vs collection" branching in the
handler is gone. Existing tests strip the now-ignored input_mode field;
two new tests cover the autodetect (collection-only) and mixed-paste
paths.

Plan deviation: rather than baking the expand_collections passthrough
into the _patch_steam helper (the plan's suggestion), uses a module-
autouse fixture _stub_expand_collections to stub it to identity by
default. The autouse approach handles the patch-shadowing problem the
helper version would have introduced for the new autodetect tests (which
need to assert specific args on a per-test expand_collections patch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:27:50 +02:00
mwiegand
6a04594c19
feat(workshop): batched expand_collections() helper
Adds expand_collections(ids) to steam_workshop: one GetCollectionDetails
POST covers a mixed batch of item and collection IDs, returning a flat
deduplicated list of item IDs in input order. Foundation for the upcoming
items-vs-collection autodetect in the workshop add_items handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:23:54 +02:00
mwiegand
f1b0cbb5f1
docs(overlays): create-modal + workshop-section implementation plan
Six-task TDD plan that turns the 2026-05-18 redesign spec into
concrete steps: expand_collections backend helper + tests, handler
unification (drop input_mode), CSS component primitives, create-modal
template rewrite + e2e test, workshop-section template rewrite +
fixture + e2e tests, final stale-content sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:20:07 +02:00
mwiegand
0ffc3fde3d
docs(overlays): create-modal + workshop-section redesign spec
Specifies the create-overlay modal redesign (field reorder, custom
radio-list, switch instead of checkbox, drop legacy path hint) and the
workshop-items section restructure (drop input-mode radio in favor of
autodetected items-vs-collections via batched GetCollectionDetails).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:53:51 +02:00
mwiegand
308fa4eb26
docs(stylesheet): redesign from first principles
Throw away the historical naming. New vocabulary chosen for clarity and
agentic-dev predictability: parts use hyphenated child classes
(.card-header), variant modifiers chain on the parent (.button.primary),
state stays on ARIA attributes. Variants compose via Tier-3
component-scoped tokens (--button-bg etc.) — .button.danger.outline is a
real outlined-danger button with no combination rule.

Adds toast, spinner, heading, app-header as first-class components.
Renames panel→card, modal→dialog, badge→tag; collapses state-* into tag
variants via ui.lifecycle_tag. Adds an explicit template-rewrite phase
in the migration plan, since every template's class attributes change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:33:47 +02:00
mwiegand
536c3384bf
docs: stylesheet redesign implementation plan
Ten tasks aligned with the spec's seven-commit migration:
foundation → elements/layout → core components → composites →
macros → widget relocation → utilities → styleguide → AGENTS.md
→ cleanup. Token migration table for old→new names. Pytest unit
tests for the field a11y macro and the /styleguide route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:07:24 +02:00
mwiegand
a0501a20fb
docs: stylesheet redesign design
Replaces the current ~1.4k LOC stylesheet with a tiered design system:
@layer-ordered cascade, two-tier tokens, budgeted component classes,
five high-leverage macros, in-app style guide, and the "system is closed"
workflow rule (every page element comes from the catalog).

Validated by a throwaway /spike comparing Pico v2, Simple.css, and the
pure-custom design; pure-custom won on the code-feel criterion the user
weighted highest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:55:20 +02:00
mwiegand
fa9acd3027
style(player-card): avatar spans full card height; name + meta stacked
Both current cards and recent chips now lay out as a 2-column grid:
larger avatar (36px current / 28px recent) on the left occupying both
rows, name on row 1 column 2, meta on row 2 column 2. Removes the
inline "· " prefix from recent-chip meta — it was a separator for the
old single-line layout and reads as visual noise when stacked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:01:33 +02:00
mwiegand
b5cde8ed85
style(server-detail): pin tab-pane height so all three tabs stay same size
Switches .tab-pane from max-height: 18rem to height: 18rem. The console
pane was already always 288px tall because its flex layout reserves
space for the pinned input; the log and files panes were only as tall
as their content, so the inspection strip shrank when log scrollback
was short. Now every tab reports the same 288px height — strip height
no longer jumps when you switch tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:54:53 +02:00
mwiegand
1105f578e4
style(server-detail): grow inspection-strip tab panes by 50%
Bumps .tab-pane max-height from 12rem (192px) to 18rem (288px) so the
log/console/files content area gets the same +50% vertical breathing
room the tab buttons themselves received in eabb976.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:53:47 +02:00
mwiegand
36e4b61581
fix(server-detail): scope console tab-pane flex to :not([hidden])
The .tab-pane[data-tab="console"] flex rule added in eabb976 had equal
specificity to .tab-pane[hidden] { display: none } and came later in
the cascade, so the hidden console pane rendered display:flex right
underneath the active log pane — the page showed both at once,
doubling the inspection strip's height. Adding :not([hidden]) keeps
the hidden rule winning when the tab is inactive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:50:01 +02:00
mwiegand
eabb9764b9
style(server-detail): grow inspection-strip tabs; pin console input above scrollable transcript
Tab padding: --space-xs → --space-s (vertical, 2×) and --space-m → calc(--space-m * 1.5) (horizontal, +50%), plus font-size: 1.05em. strip-expand bumped from --space-xs/--space-s to --space-s/--space-m proportionally. Console panes (inline tab-pane and modal body) are now flex columns so the transcript scrolls and the input form stays pinned at the bottom; max-height: none overrides the global 400px cap in both scopes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:43:54 +02:00
mwiegand
70b80d4ceb
fix(server-detail): tall modal heights, true recent count, re-fetch on reopen, drop dead macro + arg
- 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 <noreply@anthropic.com>
2026-05-17 21:40:20 +02:00
mwiegand
2d28d9f800
test(e2e): tab switching + expand-to-modal on server detail
Append two new e2e tests that cover the inspection strip:
- test_tabs_switch_between_log_console_files: verifies data-active-tab
  attribute mirrors tab clicks and the correct tabpanel is shown/hidden
- test_expand_opens_matching_modal: verifies the ⛶ button opens the
  <dialog> matching the active tab name

Also fix the pre-existing test_hover_download_initiates_file_download
for the new tab-based layout: click the Files tab first (rows were
inside a hidden tabpanel), and scope the row locator to the tab pane
to avoid a strict-mode violation now that the modal also contains a
duplicate file tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:32:19 +02:00
mwiegand
6de5f90626
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 <noreply@anthropic.com>
2026-05-17 21:25:36 +02:00
mwiegand
96bbd0c136
fix(server-detail): restore auto-escape via macro-call blocks + extract console_form macro
Replace four raw-string | safe config_field calls with {% call config_field_block %}
blocks so Jinja auto-escaping is preserved for server.hostname, server.name,
blueprint.name, server.rcon_password and g.user.username. Extract a console_form
macro to eliminate the duplicated inline/modal form and restore the missing
placeholder on the modal input. Add XSS regression test that confirms the fix
is load-bearing (test fails when templates are reverted to pre-fix state).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:23:40 +02:00
mwiegand
11142c1d08
feat(server-detail): state cluster + inspection strip + five modals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:18:38 +02:00
mwiegand
808a59b2db
feat(base): include tabs.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:15:34 +02:00
mwiegand
eb0c1a52db
feat(js): tabs.js — tab activation + expand-to-active-modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:14:34 +02:00
mwiegand
be3a00a8f5
feat(css): state-cluster, inspection-strip, compact player grids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:12:31 +02:00
mwiegand
e2b6f39828
feat(server-actions): remove inline job-log; link → job-log-modal trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:10:02 +02:00
mwiegand
6656588b8f
refactor(live-state): hoist display_name into {% set %} to DRY card loops
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:08:46 +02:00
mwiegand
20fb564246
feat(live-state): compact 4-col current + 5-col recent chips + N Recent trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:07:15 +02:00
mwiegand
9554661e5a
fix(live-state): cap recent_rows query at 50 to bound row count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:05:13 +02:00
mwiegand
309354942a
feat(live-state): expose sliced recents + total count to template
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>
2026-05-17 21:02:57 +02:00
mwiegand
7963b69cb3
feat(templates): add _macros.html with config_field macro 2026-05-17 21:00:01 +02:00
mwiegand
5ca3db4a6e
docs(spec): server detail page redesign
Groups server state into a single top cluster (lifecycle + live state
+ config), demotes log/console/files to a tabbed inspection strip with
expand-to-modal, and routes the job log behind a modal so the page no
longer reflows during HTMX polls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:47:11 +02:00
mwiegand
b45adcd819
feat(console): add color legend under console input
Three labelled swatches mirror the dropdown's name colors so users
can decode the cvar/command/sourcemod color scheme without guesswork.
Plain-text caveat next to the sourcemod swatch notes that those
commands are plugin-dependent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:56:54 +02:00
mwiegand
44e82e3c42
feat(console): color-code sm_* (SourceMod) suggestions distinctly
sm_* commands depend on the SourceMod plugin being loaded on the
target server, which is not always the case. Render their names in
the third syntax-palette color (purple via --cm-number) so the user
can tell at a glance that these may not exist on the server they
are targeting. Vanilla cvars and commands keep their existing
pink/green colors. Theme-aware via the existing token swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:44:27 +02:00
mwiegand
d21cd72f8d
test(files): cover server-detail hover-download
New test module (test_server_detail.py) — the server-detail page is
NOT a files-overlay (files_overlay=False in the template), it just
reuses _overlay_file_tree.html in read-only mode. Tests live
separately to make the semantic split visible.

The test navigates to /servers/<id>, hovers the server.cfg row to
defeat the CSS :hover gate on .files-row-actions
(opacity:0/pointer-events:none → 1/auto), clicks the ⬇ download
link, and asserts both the suggested filename and the byte content
of the downloaded file.

The :hover gate is load-bearing: without locator.hover() first,
pointer-events:none blocks the click. A regression that ships
actions always-visible would change the user-flow ergonomics and
needs to update this test deliberately.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:22:17 +02:00
mwiegand
b43bb9e0fa
test(files): add server-detail e2e fixture
Adds server_with_files fixture: seeds alice + a Blueprint + a Server
row pinned to port 27015, then pre-creates
LEFT4ME_ROOT/runtime/<server_id>/merged/ with a single seed file.
The /servers/<id> page lists files from that merged directory (the
kernel-overlayfs view of a running server), which never exists on
dev/test boxes — without the pre-mkdir + seed, the page renders
the empty-state branch instead of file rows.

Server.port is a global UNIQUE constraint on the model, but every
e2e test gets its own SQLite DB, so a fixed L4D2-default port is
fine.

Sets the stage for the Tier 3 hover-download test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:20:20 +02:00