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>
Spec for adding srccfg-vocab autocomplete to the runtime console input
on server-detail. Reuses the editor's ranking algorithm (extracted to a
shared module) but ships a small vanilla dropdown so the console stays
independent of CodeMirror. Tab/Esc drive the dropdown; ArrowUp/Down keep
recalling history; Enter always submits the typed text, never the
highlighted suggestion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for the swift3-style ?modal=<path> pattern: same route renders full page
or layoutless fragment based on an HX-Modal header, ~50-line JS module owns
URL+history, HTMX owns fetch+swap, native <dialog> owns show/hide. Pilot
migrates the file editor's open/render flow only — save flow stays AJAX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codemirror/lang-bash is not an official package. cm6's official path
to bash highlighting is @codemirror/legacy-modes/mode/shell wrapped in
StreamLanguage.define(), matching the same mechanism we use for the
custom srccfg mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Briefs the next brainstorming session with: what we built, the four
contenteditable failure modes that made it unshippable, what's still
in the repo (Playwright harness, dev-server, original spec/plan as
historical reference), the decision pending (CodeMirror 6 vs
textarea-overlay), inputs to load, and an explicit "don't restart
this cycle" caveat against trying a third contenteditable variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CSS lives in a dedicated stylesheet loaded only by the editor-assets partial,
not folded into components.css — keeps the editor's footprint isolated from
the global widget styles. Drop the two-stage vocab sourcing in favor of a
single cvarlist/cmdlist dump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrade blueprint config, overlay script, and files-editor textareas with a
reusable vanilla-JS editor. Textarea stays as value carrier so form POST and
files-overlay.js fetch paths are untouched. Seed cvar vocabulary from the
existing l4d2-server-cvar-reference.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version implied l4d2host has tz patterns to defer. An
inventory grep showed it has no datetime usage at all (no `from datetime`
import anywhere in the tree). Replace the bullet with the verified
finding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validated design for the migration framed by the 2026-05-16 handoff doc.
Two findings shaped this design: (1) DateTime(timezone=True) is a no-op
on SQLite per the round-trip spike, so the fix must live in app code;
(2) every byte on disk is provably UTC (no datetime.now() / utcnow() /
CURRENT_TIMESTAMP / func.now() anywhere), so a result-side tzinfo stamp
is correct, not optimistic.
The chosen approach: a UtcDateTime TypeDecorator that raises on naive
bind and stamps tzinfo=UTC on read. Single PR, two commits (test-first
for clean bisect). No DDL change, no Alembic migration, no on-disk
data transform.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sets up the next session to migrate models.py DateTime columns to
timezone=True and remove the defensive .replace(tzinfo=None) shell.
Surfaces evidence and open questions (SQLAlchemy/SQLite round-trip
behaviour, existing data migration, pw_changed_at marker semantics)
rather than pre-baking an implementation plan that could bury false
premises.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unify three coexisting time-display styles (raw datetime repr, bespoke
inline math, route-side humanize_delta) behind a single timeago Jinja
filter returning a <time> element with relative label and UTC tooltip.
Symmetric past/future ladder with second precision and day-month-year
fallback >7d. Naive-datetime DB-column cleanup tracked as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrate from pip-install-e + setuptools to a uv workspace with a
committed uv.lock for deterministic deps. Switch both members to
hatchling, and move package sources into nested standard layout
(l4d2host/l4d2host/, l4d2web/l4d2web/) so builds work from a
read-only source tree — setuptools wrote egg-info to source under
the old layout, which broke uv sync on the root-owned /opt/left4me/src.
Local dev install: `pip install -e ./l4d2host -e ./l4d2web` -> `uv sync`.
.envrc switches from `layout python python3.13` to `use uv`. Python
pinned to 3.13 via .python-version.
l4d2web now declares its cross-dep on l4d2host explicitly via
[tool.uv.sources] (workspace = true). l4d2web/alembic.ini and
l4d2web/alembic/ stay at the project root (standard alembic layout).
Test fixes:
- tests/__init__.py added to both test dirs so pytest doesn't shadow
l4d2host as a namespace package via outer-dir walk.
- 3 CWD-relative paths in tests (l4d2web/static/css/{tokens,layout}.css
and js/sse.js) anchored to Path(__file__) so they survive layout
changes.
- Two test_install.py tests now monkeypatch HOME to tmp_path so they
stop silently mutating ~/.steam/sdk32 on every run.
628 tests pass under sandboxed `uv run pytest`.
Per docs/superpowers/plans/2026-05-15-uv-workspace-execution.md;
prereq for the ckn-bw bundle's uv-sync action (queued).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Queued for a future agent: collapse the 5-action venv chain in ckn-bw
(create_venv + pip_upgrade + pip_install [the tempdir-copy dance] +
alembic_upgrade + seed_overlays) into 3 actions backed by a uv
workspace at the left4me repo root and a single `uv sync --frozen`
driven by a committed uv.lock.
Handoff is self-contained: spike test for the source-cleanliness
assumption, fallback to Medium scope if that fails, concrete file
edits in both repos, migration order, verification matrix, and risks.
Independent of the just-shipped deployment-responsibility reshape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
deploy/README.md: rewrite intro to reflect that deploy/files/ and
deploy/scripts/ are the canonical sources of truth (not examples), with
hardening drop-ins explicitly listed; reference fixtures in
files/usr/local/lib/systemd/system/ noted as such.
spec: add ## Status block marking the deployment-responsibility migration
shipped 2026-05-15.
Cleanup: remove the old scripts/{libexec,sbin,tests}/ paths that were
still tracked after the 2834ad4 move to deploy/scripts/. The content
is already present at deploy/scripts/; these entries were a tracking
artifact from an incomplete git mv.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layout consistency: everything ckn-bw deploys to the host now lives
under deploy/. ckn-bw's install_left4me_scripts copy-action goes away
in lockstep with this commit and is replaced by target-side symlinks.
Also updates all path references in docs, tests (conftest.py parents[]
depth, test_overlay_helper.py HELPER_SOURCE), and deploy/README.md.
Part of 2026-05-15-deployment-responsibility-design.md migration step 4.
Brainstorm happened; design at 2026-05-15-deployment-responsibility-design.md.
Handoff doc stays as the historical framing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conservative reshape coming out of the brainstorm: application-shape
static artifacts move to left4me/deploy/ and are delivered to the
target via bw symlink items pointing into /opt/left4me/src/deploy/...
(safe because the runtime-state relocation made the checkout
root-owned). Per-host shape — base unit bodies, slice CPU pinning,
env templates, nginx/timers/nftables metadata — stays bw-managed in
ckn-bw.
Moves: hardening drop-ins (new), sudoers (dedup mirror), sysctl
drop-in (dedup mirror + absorb ptrace_scope metadata entry),
privileged scripts (relocate scripts/ to deploy/scripts/, replace
install-action with symlinks).
Five-step migration with sysctl consolidation as the canary, then
hardening drop-ins, sudoers, scripts, cleanup. Lands before the
build-overlay-unit refactor so that work can ship its hardening
drop-in inline using this pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync deployment references for the runtime state relocation
shipped via ckn-bw (commit 6fae2fd). /opt/left4me/ is now a
root-owned deploy-artifact root (just src/); .venv and steamcmd
live at /var/lib/left4me/{.venv,steam}.
Touches:
- deploy/files/.../left4me-web.service: PATH + ExecStart
- deploy/files/.../left4me-workshop-refresh.service: WorkingDirectory
(was /opt/left4me, now /opt/left4me/src to match the web unit),
PATH, ExecStart
- scripts/sbin/left4me wrapper: flask path
- deploy/tests/test_example_units.py: PATH + ExecStart assertions
for the web unit; also fix a pre-existing broken assertion that
read "Environment=PATH=..." (the unit has Environment=HOME=...
PATH=... on one line, so "Environment=PATH=" was never present)
- now reads just "PATH=..."
- deploy/README.md: paths
- l4d2host/tests/test_cli.py: LEFT4ME_STEAMCMD fixture path
Design + as-shipped record:
docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md.
The original (narrower) prereq spec at
docs/superpowers/specs/2026-05-15-handoff-noneditable-install.md
is marked superseded with a pointer to what shipped + why the
scope grew (setuptools writes egg-info to source during PEP 517
build prep).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained spec for the next agent to land the editable→
non-editable install switch and the root-ownership flip on
/opt/left4me/src. Prereq for the deployment-responsibility brainstorm:
target-side symlinks from /etc/... into the checkout's deploy/files/
only become safe once the checkout is unwritable by the left4me user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hardening refactor + uid-collapse make the "what does left4me own
vs. ckn-bw own" question more pointed. The 2026-05-06 deployment
design already framed this: deploy/files/ in left4me mirrors target
paths, configmgmt integrates. Some artifacts have drifted into the
ckn-bw reactor since (systemd unit emissions, sysctl defaults); the
brainstorming session reconciles.
Sequenced after uid-collapse. Self-contained for a fresh Claude
session to read cold via superpowers:brainstorming.
Session-handoff updated to point at this as the next-next queued work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hardening refactor that just landed closes the same-uid attack
surface (FS view, ptrace, /proc visibility, signals) for the web +
gameserver units via systemd directives plus system-wide
kernel.yama.ptrace_scope=2. Keeping the script-sandbox on a separate
uid was the inconsistent half-step — defense-in-depth only, with
build-time-idmap complexity attached. One principle wins: harden
once, share the uid.
scripts/libexec/left4me-script-sandbox: drop the idmap block (uid
lookups, STAGING setup, cleanup_staging trap, mount --bind
--map-users), switch User=/Group= to left4me, point BindPaths at
\$OVERLAY_DIR directly. Header comment updated to reflect
hardening-not-uid as the same-uid defense. nsenter self-wrap kept —
it's about mount-namespace escape, not uid.
Tests + comments + companion docs updated. Build-time-idmap and
overlay-idmap plans marked SUPERSEDED; user-uid-split spec revised
to "1 user is correct"; one-line update notes on the hardening
specs and the build-overlay-unit-design.
Companion ckn-bw commit removes the l4d2-sandbox user + group and
tightens /var/lib/left4me from 0711 → 0755 (the traverse-only mode
was specifically for the sandbox uid).
Approved-but-not-executed plan to collapse the two-user model
(left4me + l4d2-sandbox) into one. The build-time-idmap that
translates sandbox writes back to left4me uid becomes a no-op when
source uid == target uid, so it's removed along with ~30 lines of
helper plumbing. Hardening already covers the same-uid attack
surface the sandbox uid was defending against, so collapsing makes
the architecture consistent with the web/server hardening-only
decision.
Plan: docs/superpowers/plans/2026-05-15-uid-collapse.md
Handoff: docs/superpowers/specs/2026-05-15-session-handoff.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 1/2/3-user question is answered: stay at 2 (left4me + l4d2-sandbox).
The defenses that motivated a 3-user split (cross-uid ptrace,
cross-server contamination, web-side reach into gameserver state,
DB/env exposure to srcds) are closed by the systemd hardening
composition: PrivateUsers + PrivatePIDs + TemporaryFileSystem +
SystemCallFilter=~@debug + empty CapabilityBoundingSet.
The residual filesystem-ACL surface (mode 0640 root:left4me on DB and
web.env) is noted as a separate concern — covered for the current
deployment shape, revisit if shape changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four corrections noted by the test plan's executor in commit 461b8d0:
- PID-lookup race: pgrep+head can pick the wrong instance. Replace
with systemctl show -p MainPID --value left4me-server@N.service.
- gdb-from-host ptrace check: nsenter into only the mount namespace
with root caps bypasses the SECCOMP filter, so the test is a false
positive. Replace with systemd-run-with-same-directives probe, or
syscall-filter inspection.
- D5 pgrep pattern: 'srcds_linux.*\@2' doesn't match because @N is
in the unit name, not argv. Use systemctl show -p MainPID.
- scmp_sys_resolver is in the seccomp package on Debian 13, not
libseccomp-dev.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified during plan execution that the ckn-bw systemd-bundle emitter
handles tuples and empty values as expected. SocketBindAllow port
range hard-coded since systemd directive variable substitution is not
universal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Going back to the inline-in-reactor shape: hardening directives land in
ckn-bw's systemd_units reactor as shared Python dicts (HARDENING_COMMON
+ HARDENING_SERVER + HARDENING_WEB), spread into each unit's Service
block. Educational reference units in deploy/files/.../*.service stay
and get per-directive comments. Operator discipline hand-syncs the
reference to the reactor; no CI drift test.
The broader responsibility reshape — hardening drop-ins living in
left4me with ckn-bw as a thin file-shipper — is worth pursuing as a
separate dedicated session, not bundled into this refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardening composition is application knowledge (which paths to bind, that
srcds is i386, what breaks sudo). It belongs in the left4me repo as
drop-in .conf files under deploy/files/etc/systemd/system/<unit>.d/.
ckn-bw shrinks: keeps the base units in its reactor, removes the
hardening keys, ships the drop-ins to /etc/systemd/system/. Existing
educational reference units in deploy/files/.../*.service are deleted in
favor of the drop-ins, which carry per-directive comments. Broader
configmgmt-responsibility reshape (base units leaving the reactor)
deliberately deferred to a future session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior handoff pointed this session at running the test plan; that's
done (commit 461b8d0). Update the handoff to point the next session at
writing docs/superpowers/plans/2026-MM-DD-hardening-refactor.md against
the proven composition, including the two amendments (x86 arch,
PrivatePIDs) and the MDW permanent exclusion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ran the 11-test plan against left4me-server@1 (canary) and left4me-web
on left4.me / Debian 13 / systemd 257. Cleaned up all unit drop-ins;
kept the Test 9 sysctl (kernel.yama.ptrace_scope=2) per spec.
Outcomes:
- server@1 systemd-analyze: 7.5 EXPOSED → 1.3 OK
- left4me-web systemd-analyze: 8.7 EXPOSED → 4.1 OK
- All 8 attack vectors in Test 8 (D1.a-c, D2.a-c, D3, D5) blocked
- Test 6 (MemoryDenyWriteExecute) fails as predicted — Source engine
i386 .so files have text relocations; exclude from final composition.
- Test 11 (24-48h soak) skipped per operator decision.
Two amendments to the spec's proposed composition required for the
refactor:
- SystemCallArchitectures=native x86 (not bare 'native') — srcds_linux
is i386, the kernel kills every native-only call.
- PrivatePIDs=true added — ProtectProc=invisible alone cannot hide
gunicorn from srcds because both run as uid 980; PrivatePIDs gives
each instance its own PID namespace and closes D2.b.
Spec bugs surfaced and documented in the "Output" section: PID lookup
via pgrep (race vs. instance), Test 4/10 gdb-from-host doesn't
actually exercise the unit's SECCOMP filter, Test 8 D5 pgrep pattern
won't match. Tooling note corrected: scmp_sys_resolver is in
'seccomp' package, not 'libseccomp-dev'.
Next session: write docs/superpowers/plans/2026-MM-DD-hardening-refactor.md
against the proven composition; supersede the uid-split spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframe the queued uid-split decision into a broader hardening analysis.
Audit found the same-uid attack surface (DB readable from srcds, ptrace
allowed, RCON stored plaintext) is closable by either uid split or
systemd directive composition; the three specs ground that choice in a
threat model, survey the defenses, and lay out a self-contained test
plan to run on left4.me next. uid-split spec deferred pending results.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Short companion to the existing topic-specific handoff docs. Captures
the situationally-fresh state at the end of the 2026-05-15
deploy-dir-rethink + janitorial sweep so a fresh session can pick
up cold: what just landed, what's next (uid-split), what's NOT next
(build-overlay-unit, until uid-split decides), and the
decision-relevant signals that emerged during this session — mostly
that the 2-uid model was freshly load-bearing in the build-time-idmap
work and that srcds hardening already covers most of what a
gameserver-uid split would add.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both items were operational verifications (not code changes) against
the deployed test host ovh.left4me (141.95.32.8).
Item 8: orphan idmap binds in PID 1's mount namespace.
`sudo findmnt --task 1 -o TARGET | grep /var/lib/left4me/runtime/.*/idmap/`
returned zero matches with left4me-server@{1,2}.service both active.
Either swept earlier or never appeared on this host; nothing to umount.
Item 9: Optimized Settings (overlay 8) files-overlay sanity.
Dir is left4me:left4me end-to-end; `sudo find /var/lib/left4me/overlays/8
-type f -uid 981` returned empty. The invariant "files-overlays are
populated by the web app as left4me, never through the sandbox helper"
holds.
Remaining live janitorial items: 7 (conditional on the build-overlay-unit
refactor) and 10 (SourceMod 1.13 calendar reminder, ~late 2026).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Janitorial item 6 in 2026-05-15-janitorial-cleanup.md. The v1 sandbox
design (2026-05-08-l4d2-script-overlays-design.md) was approved
2026-05-08 and superseded the same day by the v2 systemd-only design
(2026-05-08-l4d2-script-sandbox-v2-systemd.md). The current
left4me-script-sandbox helper uses systemd-run in service-unit mode;
no bwrap binary is invoked. The v1 spec still described bubblewrap as
the engine.
- v1 spec gets a top-of-file banner pointing at v2 as the supersede.
Body preserved; the rest of the v1 design (overlay-type unification,
resource caps, helper auth) is still valid — only the sandbox engine
changed.
- l4d2web/services/overlay_builders.py: ScriptBuilder docstring
"bubblewrap + systemd-run" → "hardened systemd-run transient
service" (the as-built reality).
- scripts/tests/test_script_sandbox.py: stray "/bwrap" in a comment
cleaned up. Negative regression assertions (`assert "bwrap" not in
text`) intentionally retained as the guard against accidental
re-introduction.
- Plan docs left untouched (historical action snapshots).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the implementation plan that landed in the preceding commit
(2026-05-15-deploy-dir-rethink.md) under docs/superpowers/plans/, and
marks the two related specs:
- 2026-05-15-deploy-dir-rethink-design.md (the source handoff) gets a
"Resolved by …" banner at the top with a one-paragraph summary of
the decisions taken. Body preserved for archaeology.
- 2026-05-15-janitorial-cleanup.md gets a status banner noting that
items 1, 3, 4, 5 are fully resolved by the deploy-dir-rethink plan
and item 2 is partially resolved with a third option the original
enumeration didn't list: only the truly-dead two static units
(cake.service, nft-mark.service) deleted, the reactor-emitted set
(server@, web, workshop-refresh.{service,timer}, slices) retained
as curated examples. Resolved items left in place but flagged.
Remaining live janitorial items: 6 (bubblewrap doc drift), 7
(conditional on build-overlay-unit refactor), 8 (operational idmap
bind cleanup), 9 (Optimized Settings overlay verification), 10 (SM
1.13 calendar reminder).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups bundled into a single commit:
- docs/superpowers/specs/2026-05-15-janitorial-cleanup.md collects
the "do later" small TODOs that surfaced across the recent idmap
+ consolidation work: dead cake-related artifacts, obsolete
static systemd units in deploy/files/, the bubblewrap→systemd-run
doc drift, stale gameserver-side idmap binds on un-checked
instances, calendar reminder for SM 1.13 stable. Each item is
small and self-contained.
- docs/l4d2-server-cvar-reference.md captures the research from
the early-session L4D2 cvar deep-dive: tickrate sweet spots,
nb_update_frequency cheat-protection + sm_cvar workaround,
cvars that don't exist in L4D2 (net_maxcleartime,
z_resolve_zombie_collision_multiplier per RCON probe), recommended
plugins, MetaMod/SourceMod branch tracking, and the empirically-
verified idmap-propagation-through-rebind kernel-6.12 quirk.
Reference material, not a spec — lives at docs/ root.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Explicit clarification so the next agent doesn't go looking for
user-unit friction. left4me-server@.service and left4me-web.service
are system units that drop to User=left4me; the 3-user split is a
literal one-line edit per unit. No lingering, no pam_systemd, no
per-user systemd instance bootstrap. The privileged
ExecStartPre/ExecStopPost steps stay root via the + prefix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 2-user split (left4me + l4d2-sandbox) has been inherited as a
constraint across multiple recent plans (idmap-on-mount, build-time-
idmap, helper consolidation) without ever being designed
end-to-end. Three plausible configurations: collapse to 1 user
(rejected for security), keep at 2 users (status quo), or split web
from game into 3 users for blast-radius limiting on either side.
Doc captures the threat-model heuristics, cross-uid file-access
plumbing options (shared group vs. world-read), idmap implications,
a step-by-step migration sketch for the 3-user variant, and explicit
out-of-scope items (per-instance gameserver uids, etc.). Detailed
enough that a future session can pick a configuration and execute
without re-deriving the design space.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The script content lives in the overlays.script DB column and the
unit's %i is the row id, so the worker-writes-script-to-fs step in
the original sketch is duplication. Document three options (worker
writes / unit fetches via helper / pipe to stdin) and recommend the
unit-fetches variant with RuntimeDirectory= auto-cleanup. Promote
this to the top of the open-decisions list since it shapes the
worker, the unit, and whether a fetch-script helper is added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The build-time idmap landing today required a nsenter self-wrap in
left4me-script-sandbox to escape the web app's PrivateTmp namespace
before pre-creating the idmapped staging bind. Working but band-aid:
the helper is reinventing what a systemd template unit would do
declaratively. Mirror the left4me-server@.service pattern with a
build-overlay@.service template — ExecStartPre does the idmap bind in
PID 1's namespace by default, the hardening flags live in the unit
file, ExecStopPost tears down. Worker switches to sudo systemctl start.
Doc captures full proposed unit, worker rewrite sketch, sudoers
update, migration order, verification steps, and the ~5h estimate
so a future session can pick this up cold and execute.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 2026-05-15 script-consolidation pass landed a working but
half-finished mental model: deploy/files/ was retroactively promoted
from "historical reference" to "canonical source," but only for the
script files. Several adjacent things (sudoers/sysctl duplication
across both repos, the systemd unit files that ckn-bw's reactor
ignores, deploy-test-server.sh's role, dead-code apply-cake) didn't
get resolved. Capture the open questions and pointers so a future
session can pick this up and commit to a coherent shape.
Co-Authored-By: Claude Opus 4.7 <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>
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.
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>