Issue #1: idmap target now uses parent+name (overlays_workshop instead of
workshop) to prevent basename collisions across allowlist roots; explicit
die() on collision detected in the loop.
Issue #2: env-var uid stubs (renamed to LEFT4ME_TEST_SANDBOX_UID etc.) are
only honoured when LEFT4ME_OVERLAY_PRINT_ONLY=1, so a misconfigured systemd
unit override cannot influence real uid mapping.
Issue #3: os.stat(lowerdir) is wrapped in try/except OSError with a die()
that shell-quotes the path and includes the exception, matching the helper's
existing error style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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.
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.
_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.
- 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)
- 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>
- _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>
- ?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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- _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>
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>
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>