The pinned howlongtobeatpy (1.0.23) takes a user agent in
send_website_request_getcode and dropped parse_all_scripts; the call
still matched 1.0.20, which only worked because the deployed env was
behind its own pin. Now on the pinned API, and the env upgraded.
Three modules carried the same executable/path constant block since the
250-line split; it is _store_tools. Four carried the same "restore the
hours a refetch lost" loop; it is restore_prior_hours. Both detached
launches go through one helper that returns the process. Thirteen
try/except/else blocks are flat, dataclass fields come from
dataclasses.fields, and the rest are one-line fixes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
A server started as `python -c "from ..._web_server import serve; serve(
port=8000)"` ran for five hours after the code beneath it changed, reporting
an 8h gaming budget while the daemon enforced 6h and had already cut gaming
off. Two separate holes let that happen.
The staleness check only ran from cmd_serve, so a server started any other
way never met it. And is_our_server matched argv containing
"steam_backlog_enforcer.main", which a `python -c` process does not have --
so that server could not even be identified as ours: `run.sh serve` would
have refused the port and named it as a stranger rather than replacing it.
_serve_stale puts the check inside the server, where no launch path can skip
it. Every route, static included, returns 503 once any package .py is newer
than this process, because a fresh page fetching stale numbers is the same
lie with extra steps. The answer is memoised for five seconds so it does not
stat the package per request; that bounds the staleness window instead of
leaving it open forever.
Refusing is only half a fix -- a process that 503s forever is honest and
useless -- so a refused request also stands the server down. The new
steam-backlog-enforcer-web.service supplies the other half with
Restart=always: it dies on stale code and returns on current code with
nobody having to remember anything. A user unit, not a system one; it serves
read-only views and needs no privilege. install.sh installs and enables it
rather than printing instructions.
is_our_server now substring-matches the joined argv, so run.sh serve,
python -m ...main serve and a bare python -c are all recognised.
newest_py_since moves into _serve_stale and _serve_guard delegates to it.
Both needed the same scan, and two copies would have been free to disagree
about whether a server is current -- which is the entire failure being fixed.
Verified end to end: healthy 200, touch _budget_view.py, next request 503,
systemd replaces the process, back to 200 reporting 6.0h and blocked=True.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.
What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), `socket.timeout` where `TimeoutError` is (UP041),
deprecated `typing` imports (UP035), PEP 695 `type` aliases (UP040), and ruff's
stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`.
That last one makes the source 3.14-only at the syntax level, which is what
`requires-python = ">=3.14"` already promised; kuhy chose it deliberately over
holding ruff a release back (2026-08-29).
ruff, ruff-format, mypy --strict and pylint all clean at the new target. The
two modified web/ TypeScript files are another session's in-flight work and
are deliberately left unstaged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
Adds a React/TypeScript frontend (web/) with a Python stdlib HTTP server
backend. The UI mirrors the CLI `stats` command in the browser, with
real-time sliders for ProtonDB rating, HLTB confidence thresholds, daily
play time, per-game time cap, playtime mode, no-HLTB-data fallback, and a
target-date planner. A parity badge confirms the client-side totals
reproduce the CLI defaults exactly (786 / 67031.1h / 143017.2h / 238447.9h).
Python side:
- `_web_dataset.py`: offline projection of HLTB/ProtonDB/snapshot caches
into a compact, secret-free JSON payload; 100% branch coverage
- `_web_server.py`: zero-dependency stdlib HTTP server serving the built
Vite bundle and the /api/dataset endpoint; 100% branch coverage
- `main.py`: new `serve` command wiring
Frontend (Vitest + RTL, 100% branch coverage enforced):
- TypeScript port of ProtonDB compound rating rule with full parity
- Pure client-side filtering via estimate.ts (no server round-trips)
- SVG completion timeline chart, sortable/searchable game table
- Steam dark theme; responsive layout
Pre-commit: adds `vitest-coverage` hook at pre-push stage requiring 100%
branch coverage on the React codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>