Commit graph

326 commits

Author SHA1 Message Date
mwiegand
2f6a9cfba0
feat(left4me-overlay): idmap bind mounts for l4d2-sandbox-owned lowerdirs
Insert an idmapped bind mount in front of each lowerdir whose top-level
uid matches l4d2-sandbox at overlay-mount time, so that overlayfs copy-up
produces left4me-owned upperdir entries instead of EACCES.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 23:48:07 +02:00
mwiegand
3a2c379b71
plan(left4me-overlay): idmap lowerdir bind mounts for cross-uid copy-up
Persist the implementation plan for adding idmapped bind mounts to
left4me-overlay so that overlay copy-up from l4d2-sandbox-owned lower
layers (script-built overlays) produces left4me-owned upperdir entries
the gameserver can write. Mechanism verified end-to-end on ovh.left4me
in a temp dir on 2026-05-14.
2026-05-14 23:42:36 +02:00
mwiegand
bbb2b983bc
harden(l4d2web): per-username login rate limit alongside per-IP
A 20-attempts-per-60s budget keyed by IP doesn't slow a distributed brute force that rotates source IPs. Add a parallel per-username bucket with the same threshold so a single account can't burn through more than 20 failed logins/min regardless of where they come from. Empty usernames aren't bucketed (would DoS the anonymous 401 path). Successful login clears both buckets.
2026-05-14 22:26:20 +02:00
mwiegand
0e2a78e065
secure(l4d2web): block non-admin writes on system overlays; last-admin guard on deactivate
_load_files_overlay docs already promised "owner or admin" for mutations, but the check only filtered by overlay.type — system overlays (user_id IS NULL) were writable by any logged-in user. Add the explicit 403 for non-admins; read-only routes remain open across all overlay types.

Mirror the delete-route last-admin guard on /admin/users/<id>/deactivate so a future auth-model change (service accounts bypassing require_admin, etc.) can't accidentally lock out the system.
2026-05-14 22:24:19 +02:00
mwiegand
74b7f61437
harden(l4d2web): default security response headers and generic error handlers
- after_request hook sets X-Content-Type-Options=nosniff, X-Frame-Options=DENY, Referrer-Policy=strict-origin-when-cross-origin, and a strict CSP (default-src 'self', script-src self+nonce, frame-ancestors 'none', form-action 'self'); HSTS added on secure non-test responses
- per-request CSP nonce minted in g.csp_nonce; servers.html's inline showModal script picks it up
- 404 and 500 handlers return short plain-text responses so a misbehaving deployment can't leak tracebacks via Werkzeug's debug page
2026-05-14 22:21:36 +02:00
mwiegand
2902c9cc82
harden(l4d2web): auth/session — clear on login+logout, constant-time CSRF, role-change invalidation
- login_user clears any pre-login session state before stamping user_id/pw_changed_at/admin so a fixated cookie value cannot smuggle data past the login boundary
- logout_user now session.clear()s instead of only popping user_id, removing leftover pw_changed_at/admin markers
- CSRF token comparison uses hmac.compare_digest
- load_current_user rejects sessions where the stamped admin flag no longer matches the user row, preventing a demoted admin from retaining elevated access until next password change (backward-compatible: sessions issued pre-upgrade lack the marker and pass through until next login)
2026-05-14 22:18:46 +02:00
mwiegand
66d14feca5
refactor(l4d2-web): harden console-history.js against HTMX version drift and races
- pendingCommand captured in htmx:beforeRequest (not requestConfig).
- ensureLoaded shares a single inflight Promise across concurrent calls.
- Document why synthetic null-id entries are safe in the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:42:05 +02:00
mwiegand
6f49efd44a
feat(l4d2-web): console panel UI on server detail page
- _console_line.html: command + reply, error variant, "(no reply)" placeholder.
- server_detail.html: console section between Live State and Files, replays
  last 50 history rows server-side; HTMX form appends new lines via hx-swap.
- console-history.js: ArrowUp/Down recall against /console/history JSON;
  scroll-to-bottom on load and after each new line.
- CSS: fixed-height scrolling transcript, terminal-ish styling, spinner via
  HTMX in-flight class.
- test_console_routes.py: update 4 assertions from legacy [ERROR] literal
  to console-error CSS class (matches new semantic markup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:39:21 +02:00
mwiegand
ecc4aa28c6
refactor(l4d2-web): tighten console route limit test and dedupe is_error
- ?limit clamp test now actually verifies the clamp instead of just
  passing through 5 rows.
- Single is_error assignment per branch, single db.add path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:35:22 +02:00
mwiegand
553b280e40
feat(l4d2-web): backend for RCON console with persisted transcript
- POST /servers/<id>/console runs a command via rcon.execute_command and
  persists every outcome (success / empty / error) to command_history.
- GET /servers/<id>/console/history returns paginated newest-first JSON
  for client-side up-arrow recall.
- server_detail() now passes the last 50 history rows as console_history
  for server-side replay on page load.
- 404 on ownership mismatch — no admin override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:32:13 +02:00
mwiegand
c4dffd471b
feat(l4d2-web): add command_history table for RCON console transcript
A row per RCON command execution: (user, server, command, reply, is_error,
created_at). Composite index on (user_id, server_id, id) supports the only
query shape — "latest N for this user+server", id DESC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:26:56 +02:00
mwiegand
9ef9ffdbde
chore(l4d2-web): clarify rcon req_id constants and helper docstring
Add comment noting _EXEC_REQ_ID/_MARKER_REQ_ID are arbitrary client-chosen
values unrelated to SERVERDATA_* packet-type constants. Update _connect_and_auth
docstring to accurately reflect that OSError/socket.timeout propagate raw from
post-connect send/recv, while only connect failure is wrapped in RconError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 21:24:41 +02:00
mwiegand
085fd714a5
feat(l4d2-web): add execute_command to rcon service with full test coverage
Extracts _connect_and_auth helper from query_status, adds execute_command
using the trailing-marker pattern for multi-packet reassembly, and covers
all paths (happy path, multi-packet, empty reply, auth failure, timeout,
input validation, marker drain) with 10 new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:21:41 +02:00
mwiegand
1d3eb51871
docs(plan): RCON console on server detail page
Plan for adding a per-server RCON console: HTMX append-swap input form,
fixed-height scrolling transcript replayed from CommandHistory on load,
multi-packet response handling, owner-only access, 30s timeout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:14:06 +02:00
mwiegand
6cc1736f17
feat(l4d2-web): add hostname edit form to server detail page 2026-05-13 15:42:46 +02:00
mwiegand
963851c0e1
feat(l4d2-web): emit hostname in spec config with ephemeral fallback 2026-05-13 15:31:12 +02:00
mwiegand
d42383dc37
chore: add dev.db and opencode.json to gitignore 2026-05-13 14:29:57 +02:00
mwiegand
69d93dda4f
feat(l4d2-web): accept hostname on server update, default empty on create 2026-05-13 14:29:53 +02:00
mwiegand
0a7f48f174
feat(l4d2-web): add hostname column to Server model 2026-05-13 14:26:14 +02:00
mwiegand
f3f0a8927a
docs: add server hostname implementation plan 2026-05-13 14:21:30 +02:00
mwiegand
fcf3143b39
docs: add server hostname cvar design spec 2026-05-13 14:19:57 +02:00
mwiegand
fe43f67b51
feat: include password-reveal.js in base template 2026-05-13 11:37:47 +02:00
mwiegand
ab83f5fd2b
feat: add RCON password row to server detail page 2026-05-13 11:37:28 +02:00
mwiegand
d9aa6bd395
feat: add password reveal toggle JS 2026-05-13 11:36:40 +02:00
mwiegand
e75feb0649
docs: add rcon password display implementation plan 2026-05-13 11:36:08 +02:00
mwiegand
358a835d65
docs: add rcon password display design spec 2026-05-13 11:35:46 +02:00
mwiegand
d113b7821c
fix(live-state): remove loading=lazy from avatars to fix Firefox/Safari flash
Firefox and Safari defer lazy images by one paint cycle even when cached,
causing a blank frame on each innerHTML swap. These avatars are always
in-viewport and cached after the first poll, so lazy loading has no benefit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:34:53 +02:00
mwiegand
175e4e653c
fix(live-state): eliminate flash on poll by switching to innerHTML swap
outerHTML removes and re-inserts the section on each tick, causing a
blank frame. Keeping the <section> as a stable DOM container and
swapping only innerHTML means avatars and text update in-place without
any teardown/reconstruct cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:26:54 +02:00
mwiegand
096d18ac64
feat(live-state): use Steam avatarfull (184x184), downscale in CSS
64x64 avatarmedium looked soft on high-DPI screens. Switching the
GetPlayerSummaries field to avatarfull (184x184) and constraining
display size to 64px via .live-state .avatar gives sharp rendering on
retina/4k panels at the cost of a slightly larger CDN fetch (still
hot-linked, so no proxying cost).

Also adds the previously-missing CSS for the live-state player grid:
avatar+name+meta arranged in a tight 2-column grid per card, link
spans the avatar+name so the meta stays non-interactive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:17:51 +02:00
mwiegand
6cbe7dc9f2
feat(live-state): link player cards to their Steam profile
Wraps avatar + persona name in an a[href=steamcommunity.com/profiles/<id>]
in both the Current and Recent blocks. Steam auto-redirects to the user's
vanity URL on follow, so we don't need to store profileurl separately.

target=_blank + rel=noopener noreferrer to keep the dashboard page in
place when a link is followed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:51:50 +02:00
mwiegand
674c4df360
deploy: add STEAM_WEB_API_KEY to web.env template
For the live-state panel's Steam profile enrichment (persona names +
avatars). Optional: empty value disables enrichment and the panel falls
back to in-game names + placeholder avatars.

The actual web.env is materialized by the ckn-bw bundle's Mako; the
template here documents the operator-facing shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:25:03 +02:00
mwiegand
37a9ad68a2
fix(live-state): cast poll_seconds to int for HTMX hx-trigger
HTMX's hx-trigger="every Ns" syntax does not accept fractional seconds —
a config override like 7.5 would render every 7.5s and silently break
auto-refresh. Floor to int with a 1s minimum.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:23:15 +02:00
mwiegand
9aaa26d9a9
feat(servers): add live-state panel with current and recent players
HTMX-refreshed /servers/<id>/live-state fragment renders snapshot
summary, current players with avatars/ping, and recent-player history;
server_detail.html bootstraps it via hx-trigger="load".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:20:01 +02:00
mwiegand
b00a3cceea
test(live-state): assert stale server's map is not rendered in the badge
Closes the negative-assertion gap from the Task 10 review: without this
check, a regression that drops the freshness guard would still pass the
positive 2/4 + c1m2_streets assertions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:17:02 +02:00
mwiegand
072d9f78e7
feat(servers): show live counts + map badge in server list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:14:57 +02:00
mwiegand
0dc61d5de4
feat(live-state): start daemon poller, prune history, close stuck sessions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:10:55 +02:00
mwiegand
be476112ee
feat(live-state): enrich roster with cached Steam profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:02:58 +02:00
mwiegand
33899f8c17
feat(live-state): reconcile player sessions on each poll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:58:30 +02:00
mwiegand
c9cd2557fd
style(live-state): drop unused imports staged for later tasks
threading, time, Callable were imported in anticipation of Task 9's
daemon-thread startup. Task 9 will re-add them when actually needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:55:36 +02:00
mwiegand
f48d624dcc
feat(live-state): poller writes RLE snapshots to server_live_state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:53:58 +02:00
mwiegand
f88d07a473
feat(steam): add GetPlayerSummaries client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:48:02 +02:00
mwiegand
465a103c3a
feat(servers): generate rcon_password on server create
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:43:56 +02:00
mwiegand
2a440dae45
feat(facade): append rcon_password as final server.cfg line
Source cvar semantics are last-wins; appending the rcon_password after
all overlay exec lines and blueprint config ensures no overlay or user
config line can silently override it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:40:56 +02:00
mwiegand
83d2a9932c
refactor(rcon): harden _parse_duration; surface fixture handler errors
- _parse_duration wraps int() in try/except so malformed connected
  durations raise RconError (not ValueError leaking past the poller's
  except RconError).
- fake_rcon_server captures handler exceptions and re-raises at context
  exit, so a buggy test handler surfaces as a real failure instead of
  silently degrading into a client-side timeout.
- Two new parser tests: HH:MM:SS duration parsing and malformed input
  coverage.
- Fix Steam ID formula typo in the spec doc (Z*2 + Y, not Y*2 + Z; Y is
  the low bit). Code was already correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:39:32 +02:00
mwiegand
b95a82b8a4
feat(rcon): add Source RCON client + status parser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:31:32 +02:00
mwiegand
e25e7098f6
refactor(live-state): drop redundant ix_sps_server_recent index
The two indexes ix_sps_server_open and ix_sps_server_recent were
byte-identical because SQLAlchemy's Index(name, *cols) form drops the
DESC ordering the spec intended. Rather than reach for text("left_at
DESC"), drop the second index entirely — SQLite scans the ASC index
backwards at no measurable cost. Spec and plan updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:27:01 +02:00
mwiegand
0f825686c6
feat(live-state): add schema for snapshots, sessions, steam profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:18:24 +02:00
mwiegand
a5f7b736a2
docs/plan: server live-state display implementation plan
Thirteen TDD-structured tasks covering schema migration, RCON client,
spec injection, password generation, Steam Web API client, live-state
poller (RLE snapshots + session reconciliation + profile enrichment +
retention + thread startup), server list badge, server detail
fragment, deploy env, and end-to-end smoke.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:10:33 +02:00
mwiegand
202026e11a
docs/spec: add server live-state display design
RCON-based polling with run-length-encoded snapshots, session intervals
with min/max ping, Steam profile cache, and a server-detail roster of
current + recent players hot-linked from Steam CDN avatars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:03:26 +02:00
mwiegand
e52219b1e9
deploy: weaken refresh-timer dep on web.service from Requires to Wants 2026-05-11 23:22:42 +02:00