serve runs unconfigured (fresh installs could not reach web Setup); web control UI finished
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-10 19:22:17 +02:00
parent f75dcd09e2
commit a9ca4a4ee7
4 changed files with 21 additions and 133 deletions

View File

@ -205,3 +205,20 @@ hard-codes it.
`gaming_unblock{phrase}`, `gaming_reset_arm{phrase}`, `gaming_unblock{phrase}`, `gaming_reset_arm{phrase}`,
`gaming_reset_heartbeat{pending_id}`, `gaming_reset_commit{pending_id, phrase}`, `gaming_reset_heartbeat{pending_id}`, `gaming_reset_commit{pending_id, phrase}`,
`gaming_reset_cancel{pending_id}`, `restart{}`. There is no `stop`. `gaming_reset_cancel{pending_id}`, `restart{}`. There is no `stop`.
## Verification record (2026-10-10)
- All 24 commands driven through the UI in headless chromium (vmbox guest
`sbe-g`, scratch state). `done` was verified in the guest only, never
live (user rule). `setup` success path: scratch HOME on the host, real
key → "Steam accepted the key" → dashboard. `serve` is allowed while
unconfigured; that is how a fresh install reaches the Setup page.
- Live after deploy: status UI == CLI; System page demo dialog = user job,
cancellable; restart dialog = root, not cancellable.
- Long ops (scratch state): check ~4x faster (tampering re-fetch 20 in
flight + early stop), scan −23 % once the no-achievements skip cache is
warm, scan cancel 71 s → 1 s, stats −1.7 s (HLTB search-URL memo).
- Test safety: `tests/_no_real_effects.py` blocks real `os.kill`/`killpg`
to anything pytest did not spawn and Steam/kill argv in real `Popen`,
and fails any test that hits it (a stale demo test once killed the
live game).

View File

@ -1,130 +0,0 @@
# Move every run.sh command into the React web UI
REMOVE ME AFTER FINISH
Spec agreed with the user on 2026-10-10 (grilling rounds in session
`session_01M3QPamsDnLQyQ6g4rresQf`). Contract: `DOCS-web-control-api.md` +
`web/src/api/contract.ts`.
## what
A production-quality React/TS UI that covers ALL 24 CLI commands (views,
jobs with live progress, setup wizard, daemon + server health), so nobody
has to type `./run.sh <cmd>`. The CLI stays as backend/recovery path; bare
`./run.sh` opens the UI.
## must
- Privileged ops go through the root daemon's `ctl.sock` (SO_PEERCRED);
the web server never gets root.
- Friction enforced server/daemon-side (phrases, gaming-reset 300 s
heartbeat countdown, unblock cap; exceptions are immediate since de76c65,
user decision 2026-10-10). More friction, not
less.
- must not: any way to stop/disable the enforcer (UI, CLI or socket).
Restart only, rate-limited, after a state flush.
- Long ops: profile on scratch state, fix top hotspot each, determinate
progress (step N/M, item, ETA) for anything >2 s.
- Stack: React 19 + React Compiler + TanStack Router/Query, newest stable,
exact pins; tokens from `~/src/utils/unified-design-system`; keyboard
operability per its `DOCS-operability.md`.
- `.desktop` entry opening a Chrome `--app` window (never port 9222).
## done
- Every command has a working UI control, driven in headless chromium
against a scratch state dir, with a screenshot each; one live pass of
status + done.
- Before/after timing table for long ops, none slower.
- vitest 100 % on `web/src`, pytest 100 % on new Python modules,
pre-commit + dependency-freshness green.
- Daemon socket: passed in vmbox, then deployed (`sudo ./install.sh`,
daemon restarted) and verified live.
## status
- [x] contract frozen
- [x] A: Python job runner + Progress/Prompter refactor of input() sites
(applied to main tree, uncommitted; CLI "Type YES" → friction phrases)
- [x] B: root daemon ctl.sock + two-phase privileged ops — passed in vmbox,
NOT deployed to host yet (needs `sudo ./install.sh` + daemon restart)
- [x] C: frontend shell + all screens against mocked backend (landed;
contract split into contract.ts + jobContract.ts for the 250 cap;
`npm run dev:mock` runs it against web/mock)
- [x] D: server write endpoints, auth, SSE, views, catalog; wire A+B
- [x] D2: integration fixes (arm phrase, not_found/server_stale, dev proxy,
token reload, per-port token, job ordering/locked-vs-busy/spawn fail,
save_credentials keeps settings)
- [x] user decided (Q16): `reset` never unblocks the store, CLI or web —
`release_store` flag and web `_reset` wrapper removed; verified on
scratch HOME (tests in test_main_misc* will need updating)
- [x] E: profile + optimise long ops — check ~4x (tampering re-fetch 20 in
flight + early stop), scan −23% after the no-achievements skip cache
warms (`_achievement_skip.py`), scan cancel 71 s → 1 s, stats −1.7 s
(HLTB search-URL memo). Coordinator fix: skip cache only records
Steam's explicit 400 "no stats", never timeouts/5xx (verified).
- [x] user request: "Pick my own game…" in list/done prompts (`_own_pick.py`),
cancel any job waiting on a prompt (cancel.request marker) — verified
- [x] G: VERIFIED 2026-10-10 — scratch HOME (grid 1369, "wicher" →
Witcher 1/2/3, scroll 0 dropped frames p95 16.8 ms, filters, pick-mode
re-ask/back/cancel, mock mode) + vmbox guest `sbe-g` (own-pick →
Witcher 2 assigned in a real pick job, API + browser). Screenshots
/tmp/sbe-g/. Pick submits only via the "Pick …" button (no
double-click assign). Owned-cache `has_stats` + records survive only
once the daemon runs G code (H deploy). Awaiting user confirmation.
- [x] G (spec): Steam-like library browser (user spec Q21–Q26, 2026-10-10, all as
recommended): new Library page grid of ALL owned games with cover art,
same component replaces the own-pick text prompt (new prompt kind
`game`; CLI keeps text search); ineligible games greyed with reason;
art = Steam librarycache (`~/.local/share/Steam/appcache/librarycache/
<appid>/<hash>/library_capsule.jpg`, 558 games) → server-side CDN fetch
+ disk cache (`shared.cloudflare.steamstatic.com/store_item_assets/
steam/apps/<id>/library_600x900.jpg`, not store-blocked) → title tile;
lazy images; fuzzy typo-tolerant search + top-8 suggestion dropdown,
Enter/arrows, roman↔arabic numerals; card: cover, title, ach %, HLTB h,
playtime, installed/assigned badges; sort name/recent/completion/HLTB;
filters hide-completed/installed/pickable. BUILT 2026-10-10 (uncommitted):
`_web_library.py`, `_web_art.py`, owned-cache `games` records,
`ineligible_reason` + `GamePicker`/`JobPrompter.game`, web/src/library/*,
jobs/GamePrompt.tsx, Library.tsx toggle, styles/library.css. Scratch
verification + mock + DOCS update delegated; own-pick end-to-end runs
in a vmbox copy of the enforcer (user, 2026-10-10: never live). DONE: grid lists all ~1369
owned games with smooth scroll, "wicher" → Witcher 1/2/3 in suggestions,
own-pick from the browser in a live Check-now job assigns the game.
- [ ] H: deploy DONE 2026-10-10 15:23 (`sudo install.sh` + daemon restart,
ctl.sock live; gaming-reset arm → heartbeat → cancel verified live,
204/410). STILL TODO: every-screen screenshot pass (real server,
scratch HOME) + one live status + done pass.
- [x] tests: pytest 2707 pass, 100 % line+branch; vitest 523 pass, 100 %
(React Compiler off under vitest only: its memo-cache branches are
unreachable for coverage)
- [x] F: run.sh bare → UI (`scripts/open_ui.sh`), .desktop entry (install.sh
`install_desktop_entry`) — opened live, WM class verified
- [x] H screenshot pass DONE 2026-10-10 (vmbox guest `sbe-g`, shots
/tmp/sbe-h/shots/): all 24 commands driven through the UI. Fixed on the
way (uncommitted): unhide skips like hide without Steam; enforce demo
button was a self-link (enforce is a "screen") → runs `{demo: 1}`,
restart sends `{demo: 0}`, mock mirrors the real catalog; enforce spec
now `cancellable` (demo loops forever, had no Cancel); demo run crashed
consuming the root-only restart-gap marker → demo never touches it;
"unsupported" hint named the daemon for a server error. serve restart
verified under systemd-run (PID + page reload); setup verified with a
bad key only (real key not copied off the guest). Live: status UI ==
CLI; done stays guest-only (deviation from `done`, by user rule).
Second round: enforce dialog per preset (`enforcePreset.ts`: demo =
user job, cancellable, no root chip; restart = root, not cancellable;
mode field hidden); demo budget log → CONFIG_DIR (was root-only
/var/log, EACCES); `serve` exempt from the total block (refused →
web unit crash-looped for the whole block); gaming-reset first attempt
= daemon refused commit 34 ms before ready_at (host/guest clock skew +
instant retype) → commit unlocks 1 s after ready_at.
- [x] user decision 2026-10-10: keep exceptions immediate; help text, DOCS
and mock catalog fixed.
- [x] user confirmation → tests, coverage, pre-commit, commit, deploy
(`sudo ./install.sh` + daemon restart for the _ctl_gap fix)
- [x] 2026-10-10 test leak closed: a stale `enforce --demo` test ran the real
demo loop on the host, read the live /proc, billed and SIGTERMed the
game being played and shut Steam down. New autouse guard
`tests/_no_real_effects.py`: os.kill/killpg only reach pytest and its
own spawns, real Popen refuses Steam/kill argv, an unpatched demo tick
fails at once, and any blocked call fails the test at teardown. Proven
in vmbox: the OLD test now fails in 1 s with the fake game alive; with
the tripwire removed the os.kill layer alone blocks the SIGTERM.
- [ ] F2: live verification of the finished UI
- [x] user confirmation → tests, coverage, pre-commit, commit (2026-10-10)

View File

@ -26,8 +26,9 @@ if TYPE_CHECKING:
from steam_backlog_enforcer.config import Config, State from steam_backlog_enforcer.config import Config, State
# Usable before an API key exists: setup creates one, add-exception never # Usable before an API key exists: setup creates one, add-exception never
# talks to Steam. # talks to Steam, and serve hosts the web Setup page that asks for the key
UNCONFIGURED_OK: Final = frozenset({"setup", "add-exception"}) # (refusing it left a fresh install with no way into the UI's setup).
UNCONFIGURED_OK: Final = frozenset({"setup", "add-exception", "serve"})
NOT_CONFIGURED_MSG: Final = "Not configured. Run 'setup' first." NOT_CONFIGURED_MSG: Final = "Not configured. Run 'setup' first."

View File

@ -36,7 +36,7 @@ class TestLockReason:
_gate.NOT_CONFIGURED_MSG _gate.NOT_CONFIGURED_MSG
) )
@pytest.mark.parametrize("command", ["setup", "add-exception"]) @pytest.mark.parametrize("command", ["setup", "add-exception", "serve"])
def test_setup_commands_need_no_key(self, command: str) -> None: def test_setup_commands_need_no_key(self, command: str) -> None:
with _block(active=False): with _block(active=False):
assert _gate.lock_reason(command, Config(), State()) is None assert _gate.lock_reason(command, Config(), State()) is None