Commit graph

127 commits

Author SHA1 Message Date
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
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
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
2875993339
docs(console): add implementation plan for console autocomplete
Bite-sized, TDD-style plan for the spec at
docs/superpowers/specs/2026-05-17-console-command-autocomplete-design.md.
Seven tasks (rankVocab extraction → second esbuild target → vanilla
dropdown module → stylesheet → wire-up → smoke test). Will be executed
task-by-task via subagent-driven-development.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:33:56 +02:00
mwiegand
02d96b593e
docs(console): add design for console command autocomplete
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>
2026-05-17 17:20:47 +02:00
mwiegand
86ac10a1d9
docs(files): handoff plan for files-overlay Playwright e2e tests
Companion to the just-shipped files-overlay rewrite. The rewrite
verified each step's behavior live in Chromium but left no automated
browser regression net. This handoff plan specifies what to add:

  * Fixture extension (conftest.py): a files_overlay_server fixture
    that seeds a files-type Overlay with one text file, one binary
    file, and a nested folder under tmp_path-rooted LEFT4ME_ROOT.
  * 11 test cases in three tiers — Tier 1 covers the critical paths
    (text edit save, create-new, 409→askConflict, binary replace,
    new-folder + delete, rename-on-save), Tier 2 rounds out drag /
    upload / deep-link, Tier 3 hits the server-detail download
    button.
  * Patterns to follow + pitfalls (the SESSION_COOKIE_SECURE=0 gotcha,
    the data-rel-path location split between text and binary modes,
    the htmx.ajax async wait, why os-drag-with-folders can't be
    synthesized).

Pinned references at the bottom point at the existing test_editor.py
pattern model and the relevant module-header comments. Estimated
half-day for the critical 7 cases, full day for the full 11.

Lives under docs/superpowers/plans/ per the project's planning-
artifact convention. Move to specs/ if it's executed and turned into
shipped tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:20:37 +02:00
mwiegand
b6db596d0f
docs(files): drop the completed files-overlay rewrite plan
The plan at docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md
described work that's now shipped across commits 4fa3964..5f82950
(12 implementation steps + the Step 0 prefactor). The design intent
lives in the resulting code (4 modules under static/js/files-overlay/,
the URL-addressable editor template, the shared route helpers) and
the commit messages capture each step's reasoning.

Git history preserves the plan content if anyone needs the original
roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:02:50 +02:00
mwiegand
d76ee05956
docs(files): errata — script tag lives in overlay_detail.html, not base.html
files-overlay.js is loaded from overlay_detail.html:285 (with defer),
not base.html — the JS activates only when .files-manager exists,
which is only on overlay detail for files-type overlays. Loading from
base.html would pull it onto every page. The plan's first draft had
this wrong in four places (step 1, step 4, step 10, critical files
table). Following the plan verbatim would have moved the script tag
to the wrong template — exactly the failure mode that
feedback_validate_before_implementing memory warns about.

Added an Errata section at the bottom of the plan documenting this.
Also clarified that all new module script tags should use defer to
match the existing pattern (the modules query the DOM at load and
need the body parsed first).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:02:21 +02:00
mwiegand
4337002bd0
docs(files): rewrite plan for files-overlay.js (3 phases, 12 commits)
Three-phase plan to consolidate files-overlay.js's mixed event-binding
patterns and complete the URL-addressable modals migration:

  Phase A (4 commits): split the 35 KB IIFE into 4 focused modules
  under static/js/files-overlay/ — core, editor, dialogs, uploads —
  with consistent document-level delegation. Behavior unchanged.

  Phase B (6 commits): migrate the two remaining inline-dialog flows
  (create-new-file, binary-replace) to URL-addressable modals via a
  new /files/new route and a binary-mode branch in the edit route +
  template. Delete the legacy <dialog id="files-editor-modal"> from
  overlay_detail.html. editor.js becomes single-purpose (~200 lines).

  Phase C (2 commits): extract shared path/editability helper for
  routes/files_routes.py; delete the now-unused /files/content JSON
  endpoint; consolidate save/replace rename duplication.

Each commit is independently verifiable + revertable. Natural pause
points at the end of each phase. Plan is the handoff artifact for
cross-session execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:49:02 +02:00
mwiegand
712ccc9861
docs(modals): plan errata — 3 verbatim-code defects + 3 inserted tasks
The URL-addressable modals plan shipped in 14 commits. Three places
where the plan's verbatim code didn't survive contact with the codebase
(has_request_context guard, LEFT4ME_ROOT-aware fixture, save-handler
direct-bind) are now documented at the top of the plan, with commit
references for the fixes. Also notes the inserted tasks 8.5/8.5b/9b
and the Task 6 design refinement (close-event single state sink) so a
future re-executor sees the actual shipped pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:03:10 +02:00
mwiegand
d05d00449f
docs(modals): implementation plan for URL-addressable modals pilot
10-task TDD plan: context processor + partial → editor template →
GET /files/edit route → modal slot + script stub → modal-router.js
(click+fetch+show → close+popstate+dismiss → bootstrap) → CM6 re-init
→ files-overlay.js wiring → remove inline dialog + Chromium matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:09:59 +02:00
mwiegand
fcab4b0b72
docs(modals): URL-addressable modals design (pilot: file editor)
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>
2026-05-17 11:03:46 +02:00
mwiegand
ebf6d2ebc6
plan(textarea-editor-v2): bite-sized TDD implementation plan
15 tasks covering: editor-src scaffold, vocab generator, srccfg
StreamLanguage mode, light/dark themes, autocomplete source, editor-entry
façade, esbuild build script + first bundle, tokens.css + editor.css,
editor.js glue (mount + submit-capture + __filesEditor alias),
_editor_assets.html partial, form-contract pytest pre-wiring gate,
template wiring with GET-asserts-markup TDD, files-overlay.js bridge
swap, Playwright e2e (autocomplete-accept + copy regression), docs +
final smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:41:26 +02:00
mwiegand
43d4104cef
fix(spec): use legacy-modes/shell for bash language
@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>
2026-05-17 01:36:50 +02:00
mwiegand
778f98dedf
spec(textarea-editor-v2): commit CodeMirror 6 design
Approved 2026-05-17. Supersedes 2026-05-16-textarea-code-editor-design.md.

Architecture: CodeMirror 6 (bundled via esbuild, committed to
static/vendor/). Form-bridge: submit-time copy (cm6 owns the doc;
capture-phase submit handler writes textarea.value once; JSON-save
path calls controller.getValue()). srccfg grammar via StreamLanguage;
bash via @codemirror/lang-bash; autocomplete via @codemirror/autocomplete.
Vocab generated from the existing repo-root ./cvar_list (2196 entries).
Theme follows the site's prefers-color-scheme model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:35:27 +02:00
mwiegand
db5b2810a9
spec(textarea-editor): handoff after contenteditable rollback
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>
2026-05-17 00:57:33 +02:00
mwiegand
10cf0da3d2
fix(editor): capture-phase keydown + popup leak + cache warmup
Addresses Critical #1 + Important #2/#3/#4 from the Task 9 code review.

CRITICAL — Tab/Enter were stolen by CodeJar before the popup handler
saw them. CodeJar registers its keydown listener during construction
(line ~159), so it ran first in bubble order: Tab handler
preventDefaulted and inserted 2 spaces, Enter handler preventDefaulted
+ stopPropagation'd (with leading indent), so the popup-accept either
ran on corrupted state or never fired at all. Fix: register the popup
listener with {capture: true} and call stopPropagation on the keys we
own — that way capture phase fires before CodeJar's bubble listener
and the key is fully consumed by the popup while it's visible. Normal
typing (popup hidden) early-returns without stopPropagation, so
CodeJar's tab-indent + enter-preserve-indent still work when there's
no autocomplete to accept.

IMPORTANT — destroy() leaked the popup <ul> into document.body. Each
mount/destroy cycle (e.g. modal close/reopen) left an orphan popup.
Fix: pop.remove() in destroy().

IMPORTANT — async refreshPopup could race in stale renders if the
first keystroke fired the vocab fetch and the second keystroke
captured a different ctx before the fetch resolved. Fix: warm the
cache with a fire-and-forget loadVocab(language) at mount, so the
first user keystroke hits cache. Eliminates the only realistic window
for the race.

IMPORTANT — acceptCompletion's Range.setStart could throw
IndexSizeError on pathological state (caret inside a tokenized span
where the fragment isn't fully upstream). Fix: try/catch the entire
DOM mutation block, log + dismiss on failure. Plus an inline comment
documenting the single-text-node invariant the current grammars hold.

Plan source updated for the capture-phase fix (most important for
future regeneration); the other fixes are smaller and only mirrored
into the actual code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:49:23 +02:00
mwiegand
4bace3ab5a
plan(textarea-editor): fix stale jar reference in autocomplete
The Task 9 plan template used the captured \`jar\` closure variable in
acceptCompletion, which becomes stale after setLanguage's
tear-down-and-remount. Same class of bug Task 4's review caught and
fixed. Update the plan to match the correct implementation.
2026-05-16 20:42:16 +02:00
mwiegand
c6f10e632d
test(blueprint): also assert prism.css is referenced in editor assets
The plan template (and verbatim implementation) listed five of the six
editor asset URLs in the structural test — vendor/prism.css was
omitted. If a future change drops the Prism stylesheet from the
partial, syntax tokens lose their color rules silently and the test
still passes. Add the missing assertion and update the plan to match.

Addresses Minor #1 from the Task 6 code review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:35:44 +02:00
mwiegand
04f9a4d6a2
fix(editor): narrow findFilenameInput scope + dispatch input from setValue
Addresses two Minor follow-ups from the Task 4 code review:

- findFilenameInput previously included `body` in its closest() selector,
  meaning any "auto" textarea outside a modal would walk all the way up
  and pick up the files-editor modal's filename input from elsewhere in
  the document. Drop `body` so out-of-modal "auto" usage degrades
  cleanly to "plain".
- setValue now dispatches an `input` event on the textarea after
  writing, matching the onUpdate mirror. Task 10 wires the files-editor
  modal to call setValue when loading file content — without this fix,
  textarea-listening code (e.g. unsaved-changes indicators) wouldn't
  see programmatic loads. Now setValue and user typing produce the
  same observable side effects.

Plan source block updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:35:47 +02:00
mwiegand
e058b45ff2
plan(textarea-editor): consolidate Task 4 editor.js into one block
The plan had Step 1 (initial widget) + Step 2 (setLanguage patch); the
implementation merges them into one final file. Update the plan to
show the final file verbatim so a future regeneration produces the
same output. Step 2 in the plan is renumbered to 'Manual verification
note' (just the deferred-to-Task-6 sentence) for completeness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:30:22 +02:00
mwiegand
cdcb7e4853
style(editor): visible light-mode popup active state + plan sync
Addresses Important #1 + Minor #2/#3 from the Task 3 re-review:

- --color-bg-popover-active light value: #f3f4f6 → #e5e7eb. The prior
  value was within ~1.05:1 luminance of the white surface — keyboard
  navigation through the autocomplete list had no visible focus
  indicator in light mode. e5e7eb (Tailwind gray-200) clears that.
- Drop dead fallback hexes on the four guaranteed tokens
  (--color-string/-keyword/-number/-bg-popover-active). They never
  fired post-fix and only produced a dark-mode-only palette if
  tokens.css somehow failed to load — i.e. they were misleading.
- Plan source block (Task 3 Step 2) replaced with the post-fix CSS
  verbatim + a new Step 2b that documents the tokens.css additions
  alongside the editor.css template, so a fresh regeneration
  produces the same file.

Deferred: cross-cutting --font-mono token (Minor #4 — would touch 7+
sites outside Task 3's scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:27:07 +02:00
mwiegand
f9c8518212
style(editor): theme-aware syntax tokens + match textarea metrics
Addresses Important #1 + #2 from the Task 3 code review:

- Adds --color-string, --color-keyword, --color-number,
  --color-bg-popover-active to tokens.css in both the :root and dark
  blocks. GitHub-style palette tuned for legibility on each theme's
  surface.
- Updates .editor-code to use the same padding tokens, font-family
  stack, font-size, and line-height as the existing textarea rule so
  the contenteditable doesn't visibly jump when the widget mounts.
- Drops the caret-color override (browser default adapts to system
  theme — no token needed).

Plan source block updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:22:27 +02:00
mwiegand
db1a255223
fix(editor): drop dead -? from srccfg number regex
The `\b` word boundary anchor prevents the optional minus from ever
matching from positions where signed numbers naturally appear (` -1`,
`(-1`, `=-1` all word-boundary-from-non-word and the `-?` fires zero
chars). Negative numbers are tokenised via the operator class instead,
which is the consistent behaviour the grammar already exhibits.
Plan source block updated to match so a fresh regeneration produces the
same file.

Addresses Minor #1 from the Task 2 code review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:15:34 +02:00
mwiegand
4a1b6d5fac
plan(textarea-editor): make Prism bash-grammar grep minifier-safe
Spec reviewer caught that the literal 'Prism.languages.bash' string
doesn't appear in the minified prism.js (minifier renames Prism→e).
Switch the verification grep to match either the bash shebang token
or any languages.bash assignment; both survive minification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:10:56 +02:00
mwiegand
f5ac61d99b
plan(textarea-editor): fix CodeJar download URL (v4.x at dist/codejar.js)
Task 1 implementation discovered that codejar v4.0.0 ships its
browser bundle at /dist/codejar.js, not the package root. The vendor
README already records the correct URL; this patch keeps the plan
itself accurate for future regeneration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:05:23 +02:00
mwiegand
1ec5e80a73
plan(textarea-editor): use curl-based vendoring (subagent-executable)
The original Task 1 instructed a human to click through prismjs.com's
configurator UI — a step a subagent can't perform. Replace with direct
curl from jsdelivr for both Prism components (core + clike + bash
concatenated) and CodeJar, plus a sed-based ESM-export strip and a
window-global shim. Updates the vendor README template accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:00:48 +02:00
mwiegand
9618109f0f
plan(textarea-editor): 12-task TDD implementation plan
Vendors Prism + CodeJar, builds the editor widget incrementally
(mount/sync → highlighting → autocomplete → files-editor integration),
scaffolds Playwright + writes the e2e editor test. Form-contract Python
tests guard each call-site wiring step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:04:56 +02:00
mwiegand
b19b00e706
spec(textarea-editor): adopt dedicated editor.css, simplify vocab sourcing
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>
2026-05-16 17:52:50 +02:00
mwiegand
bef6f0cdd9
spec(textarea-editor): syntax highlighting + autocomplete via CodeJar + Prism
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>
2026-05-16 17:33:10 +02:00
mwiegand
0c552082dc
spec(tz-aware-datetime): correct speculative l4d2host carve-out
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>
2026-05-16 13:01:25 +02:00
mwiegand
99b528e563
spec(tz-aware-datetime): design for UtcDateTime migration
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>
2026-05-16 11:53:22 +02:00
mwiegand
b04bcbce7c
spec(tz-aware-datetime): handoff for the naive-datetime cleanup
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>
2026-05-16 11:21:24 +02:00
mwiegand
fdcefcfec6
plan(timeago-shared-display): nine-task TDD migration to a Jinja filter
Lays out the file-by-file migration from the current three time-display
styles to the unified timeago filter from the design spec. TDD ordering
with tests-first, per-task commits, line-numbered locators, and an
explicit verification pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:05:42 +02:00
mwiegand
f3cd981957
spec(timeago-shared-display): one Jinja filter for all user-facing datetimes
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>
2026-05-16 10:59:15 +02:00
mwiegand
2adf42655e
plan(server-log-current-invocation): scope server log to last unit start
Today the Server Log panel shows the last 200 lines of the unit's entire
journal — mixing the current run with leftovers from prior starts. Filter
on systemd's per-(re)start InvocationID so the panel begins at the most
recent start, idles with keepalives when the unit has never run, and
force-disconnects on restart so the SSE client reconnects to the new run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:31:53 +02:00
mwiegand
49992b3a26
refactor(repo): uv workspace + hatchling + layout restructure
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>
2026-05-15 22:04:29 +02:00
mwiegand
b13d164931
spec(uv-workspace): handoff for the venv-chain → uv workspace migration
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>
2026-05-15 20:16:38 +02:00
mwiegand
450f9f1591
deploy/docs+cleanup: describe symlink model; drop stale scripts/ tracked paths
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>
2026-05-15 19:48:59 +02:00
mwiegand
2834ad4911
deploy: move scripts/{libexec,sbin}/ into deploy/scripts/
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.
2026-05-15 19:38:42 +02:00
mwiegand
55d5ab4017
plan(deployment-responsibility): mark Task 3 done 2026-05-15 19:30:35 +02:00
mwiegand
672fd9660b
plan(deployment-responsibility): five-task migration with sysctl canary
Implementation plan for 2026-05-15-deployment-responsibility-design.md.
Bite-sized steps per task; each task ends with both repos committed
and ovh.left4me idempotent. Tasks: (1) sysctl consolidation canary,
(2) hardening drop-ins, (3) sudoers symlink, (4) scripts relocation
+ symlinks, (5) cleanup + docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:57:45 +02:00
mwiegand
ddf97b3a05
spec(deployment-responsibility): mark handoff resolved by the design doc
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>
2026-05-15 18:51:12 +02:00
mwiegand
c446f6c8eb
spec(deployment-responsibility): design — symlink hardening drop-ins, sudoers, sysctl, helpers
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>
2026-05-15 18:48:13 +02:00
mwiegand
434ee20339
refactor(deploy): venv + steam now under /var/lib/left4me
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>
2026-05-15 17:56:32 +02:00
mwiegand
ff2b5c4c5a
spec(noneditable-install): handoff for the install refactor prereq
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>
2026-05-15 16:53:19 +02:00
mwiegand
15c620f95c
spec(deployment-responsibility): handoff for brainstorming the deploy split
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>
2026-05-15 15:56:38 +02:00
mwiegand
8971b23617
refactor(sandbox): collapse l4d2-sandbox user into left4me
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).
2026-05-15 15:50:57 +02:00
mwiegand
146cb01450
plan(uid-collapse): drop l4d2-sandbox user; handoff to next session
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>
2026-05-15 15:39:51 +02:00