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>
Adds left4me-workshop-refresh.service (oneshot, triggers flask
workshop-refresh) and left4me-workshop-refresh.timer (04:00 daily,
Persistent=true, 15 min jitter). Wires both into the deploy script's
cp and enable blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catch only requests.RequestException in refresh_overlay so that
server-side data errors (e.g., ValueError) bubble up as 500 rather
than being disguised as a 502 "steam api error". Update the 502 test to
use a real requests exception, add a sibling test that verifies
non-requests exceptions propagate, and explicitly assert that refresh
enqueues a build_overlay job even when Steam returns no entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /overlays/{id}/refresh lets the overlay owner (or any admin)
re-fetch fresh Steam metadata for all items and enqueue a rebuild.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Commit 16adc5c silently dropped two defensive guards from the
symlink-creation loop in WorkshopBuilder.build. Restore them:
- refuse to overwrite a non-symlink file that collides with a workshop
name (logs a message, skips creation)
- refuse to overwrite a foreign symlink (target outside the cache root)
Also: change `skipped` from list to set (O(1) membership test, no
duplicates possible), and add a brief comment above WorkshopMetadata
construction explaining which fields download_to_cache actually uses.
Two regression tests added to pin the guard behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the old skip-uncached-with-warning logic in WorkshopBuilder.build
with an inline download phase that calls _download_with_retry for each item
whose cache file is absent or stale (mtime/size mismatch). Stamps
last_downloaded_at / last_error after each download, and skips items with
no file_url. Update test fixture to utime cache files so mtime matches
time_updated, delete the now-superseded skip-warning test, and add six
new builder-level behavior tests covering the new download path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the gap where added workshop items never reach disk until an
admin presses the global refresh button. Downloads piggyback on the
per-overlay build_overlay job; daily updates come from a systemd
timer + CLI subcommand that enqueues the existing refresh job.
Today's profile feature shipped without the design spec landing in
docs/superpowers/specs/ — the design lived only in the plan-mode
scratch file at ~/.claude/plans/. Tighten the wording so the next
agent treats spec-commit and plan-commit as non-skippable steps and
verifies both files are tracked before claiming the work is shipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The matching design doc for the implementation plan committed in
6eb9bd0. Captures the session-invalidation reasoning (Django-style
"keep current session, kill others") and the open questions resolved
during brainstorming.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same validate_new_password used by the web change-password flow,
so the policy is enforced uniformly across CLI and HTTP entry
points. Existing CLI tests bumped to passwords that satisfy the
new floor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verifies that on a successful change the digest rotates, the
password_changed_at advances, this session keeps working with the
re-stamped marker, and a parallel session forged from the
pre-change marker is rejected by load_current_user.
profile_password_change now writes a naive password_changed_at so
the in-memory marker matches what SQLite returns on next read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the change-password endpoint:
- Per-IP rate limit reusing services/rate_limit
- Required fields, mismatched-confirm, policy, wrong-current
branches each redirect with a specific ?error= key
- Rotates digest + password_changed_at, then re-stamps the
current session marker so this browser stays logged in
while other sessions get rejected by load_current_user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the page reachable from the username link in the header.
Renders the form skeleton; the POST handler lands in the next
commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>