Also bumps mcp 2.2.0, ruff 0.16.7, types-requests 2.33.0.20260906 and, in
web/, react 19.3.0, vitest 5.0.0 (+ coverage-v8), eslint 10.10.0,
typescript-eslint 8.70.0, vite 8.3.0 and the small ones, so the freshness
gate lets this through. Web lint/build/tests/coverage (100 %) unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
Keeps the dependency-freshness gate green; unrelated to the engagement
gating removal in the previous commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A qualifying game process running is now sufficient on its own to bill
the budget. Previously a tick only counted if the screen wasn't locked,
the game's window was X11-focused, and input hadn't gone idle for 5
minutes — with an idle-grace refund on top. That leniency is gone
entirely: no focus check, no idle check, no screen-lock check, no
refund. Deletes the engagement-probing modules (_playtime_engagement,
_engagement_probes, _engagement_types, _x_focus, _x_probe, _screen_hold,
_controller_input) and their config fields, simplifies the audit log
and budget-view/web session schema accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The serve rework was sitting untracked: a /proc-based port guard, the
start-or-defer decision, and a stale-bundle rebuild. It had no tests, and it
changed cmd_serve(_config, state) to cmd_serve(argv) without updating the one
test that called it, so the suite was red and coverage was 251 lines short.
Tests, all against fakes:
- _fake_proc builds a synthetic /proc. _NET_TCP is derived from _PROC at
import time, so patch_proc redirects both; patching _PROC alone would have
had these tests parse the host's real process table.
- _serve_guard is split across two test files to stay under the 250-line cap.
os is replaced wholesale in every termination test, so no signal can reach a
real process even if the guard's self-protection were removed.
- _web_build mocks subprocess entirely and redirects every path constant into
tmp_path. _no_subprocess now blocks _web_build.subprocess.run too: an
unguarded test would fire a real `npm run build`, rewriting web/dist and
perturbing frontend_is_stale() for every later test in the run.
Source changes, both required rather than incidental:
- _serve_guard's wildcard match was a `# noqa: S104` on a "0.0.0.0" literal,
which the no-noqa hook rejects outright. It is now the all-zero hex column
/proc actually prints, so the dotted-quad literal never appears.
- main/__init__.py's serve dispatch arm was itself uncovered; it now has a
test asserting the raw argv passes through unparsed.
Also drops the document.hidden guard from useBudgetPoll: it skipped the very
first load, so the budget panel sat on "Reading budget state…" forever in a
background tab, which is every tab under browser automation.
Verified: 1712 python tests, 100% branch coverage; 128 vitest, 100%. The
guard's /proc parser was checked read-only against a real listener on :8000
and resolved the same pid as `ss`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKFzwzwRU1JD7SFLHtQBsz
The budget tab answered "how much did I play" but not "what did I play":
playtime_state.json and playtime_history.json each stored one aggregate
seconds figure per gaming day, so the 14-day chart was a row of featureless
bars. Separately, qualifying_pids had exactly two sources — the SteamAppId
env var and a hardcoded 16-launcher frozenset — so osu!lazer, which sets no
SteamAppId and matches no launcher name, was never billed at all.
qualifying_pids now returns pid -> attribution key instead of a bare pid set.
The app id was already in hand and thrown away on its first line; it now
survives into PlaytimeState.per_game, alongside a user-editable
counted_processes list in config.json seeded with osu!lazer.
Two things the parts/whole relationship forces:
- per_game only ever sums to AT MOST seconds, never equals it. backdate
refunds the idle grace on every engaged-to-paused edge, and
_state_or_recover synthesises a full budget with no breakdown. The
refund is debited from last_credited_key, and the remainder is rendered
as "Unattributed" rather than stored — one path then covers legacy days,
focus-probe failures and drift alike.
- counted_processes is kept apart from LAUNCHER_PROCESS_NAMES. That
frozenset is also the total-block kill list, and it routes through
kill_and_uninstall_launchers, which pacman -R's the owning package.
Counted entries are killed on total block, never uninstalled.
Both storage formats stay at schema 1: load_state already filters to
dataclass fields, so a record without per_game migrates for free, and the
history reader has to accept both a bare float and an object regardless, so
a version bump would distinguish nothing.
Chart segments use the shared design system's categorical ramp (--cat-1..6),
capped at six games plus "Other" because a seventh distinguishable hue does
not exist under simulated CVD. The red over-budget fill is gone: colour now
encodes which game, and the dashed budget line alone says over or under.
Snapshot persistence moves to _snapshot.py to keep config.py under the
250-line cap. Three modules bound config paths at import time, which
bypasses the conftest redirect — they now reach them through the module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKFzwzwRU1JD7SFLHtQBsz
The cutoff badge asserted "launchers are masked" from `blocked` alone, but
the two can disagree: the total-block branch of `playtime_tick` calls
`reconcile(should_block=False)` and leaves `blocked_at` set, so the mounts
are gone while the day still reads blocked. The panel already fetches
`rules.masked_launchers` for its "Masked now" row, so the badge now reads
the same live count instead of inferring one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fast-vs-slow rush-ratio class, the STYLE_LABELS raw-key fallback, and the
non-positive estimated-total preset row were all unexercised, failing the
100% branch-coverage pre-push gate independently of the palette change in
the prior commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes every finding in the repo's own audit: spacing/radius scale, letter-
spacing compensation on <=13px text, --on-fill for filled segmented-control/
badge/button surfaces, 12-col .cards grid, mis-nested corner radius,
box-shadow -> outline on the dark-UI selection ring, and completes the
phantom --accent-bg token.
Verified: lint + 84/84 tests clean, build succeeds, re-verified identical
in an isolated worktree at HEAD, and pixel-checked against the real `serve`
command's output via headless-Chrome screenshot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User can now pick any owned game by Steam app_id via `pick-manual <id>`.
The script resolves the game name, asks for YES confirmation, then locks
all other commands for 14 days or until the game is 100% complete.
Post-assignment steps (uninstall others, install, hide library) mirror
the automatic pick flow. Lock is checked before every command including
add-exception. Also fixes pre-existing test failures in hltb, stats,
and web_dataset modules and adds 100% coverage for all changed code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>