Commit Graph

10 Commits

Author SHA1 Message Date
ec0946f64e Grade against 3.14 too, not just run on it
`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: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, 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).

The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.

ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:42:47 +02:00
c1c73d43ba Stop the relaxed-day popup from re-firing every 30 minutes after Skip
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
workout-locker.timer (added in e5db254) re-runs the full lock-decision
chain every 30 minutes, but is_relaxed_day() is a pure date check with
no memory of an earlier same-day dismissal. Clicking "Skip — No
Penalty" only closed the window without persisting anything, so the
next tick re-derived reason=relaxed_day and rebuilt the same popup.

Now the skip button writes a relaxed_day_skip marker to the log
(excluded from weekly counting and sync, same treatment as heat_skip),
and the startup chain checks for it before re-entering the relaxed-day
branch. Also renames workout_log.json to log.json throughout the repo,
tests, docs, and Dart comments, since the fix touches that file anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qH2hJ6mvCJd3ZXpAJSaF7
2026-08-18 21:55:59 +02:00
5c27769642 Split 10 more test modules under the 250-line cap
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:22:00 +02:00
3e77f4e232 Make every lock screen keyboard-operable, and add the missing installer
The locker was not running at all. workout-locker.service crash-looped ~1977
times in a single boot with ModuleNotFoundError while remaining `enabled`, and
has no successful start in the retained journal. A locker that cannot start is
a locker that does not lock.

Root cause: commit 9b420a9 removed the unit's WorkingDirectory and
PYTHONPATH=/opt/screen-locker on the (correct in principle) grounds that
"screen_locker is now pip-installed" -- but nothing guaranteed that install.
There was no installer, /opt/screen-locker no longer exists, and the package
was absent from every Python version's user site-packages. The unit lost its
fallback and gained a hard dependency on a manual step nobody re-ran.

install.sh fixes that as a script rather than a reminder, and *gates*: it fails
if the production interpreter cannot import screen_locker, because an installer
that reports success while the service still cannot start is precisely the
failure above. It also re-asserts an editable gatelock from ~/utils AFTER the
package install and verifies it resolves there -- pyproject pins gatelock to a
git tag, so a plain install silently replaces a local editable gatelock with a
frozen wheel, which was observed removing ScrollableSurface and taking
diet_guard down with it.

The keyboard work, all of it Tk defaults rather than local mistakes:

- Focus was invisible. Tk ships highlightcolor="#000000" -- a black ring on
  #211D1B -- and relief="flat" removes the depth cue too, so a keyboard user
  had no indication of where they were. Now set in the _ui_widgets factory, so
  a new call site cannot omit it.
- Enter did nothing. X11 Tk binds <space> on Button and never <Return>.
- tk.Text trapped Tab (inserts a literal tab, refocuses itself) and no-opped
  Shift-Tab; the only exits are the undocumented Ctrl+Tab / Ctrl+Shift+Tab.
- The lock and retry screens started with NOTHING focused. on_focus_ready was
  a documented no-op arguing "buttons only, nothing to focus" -- exactly
  backwards: buttons are what needs focusing when there is no other window on
  screen and a global grab is held.
- The heat-skip modal grabbed input fullscreen with no focus, no ring, no
  Return and no Escape. It now focuses the *declining* option, so a blind
  Return keeps the workout rather than silently skipping it.

The manual-workout form's scroll viewport was pointer-only in three ways at
once: tk.Canvas has no class key bindings, so ::tk::FocusOK rejected it as a
focus stop; nothing bound wheel, PageUp/PageDown or arrows, leaving only a
draggable thumb; and canvas clipping does not unmap children, so below-the-fold
fields stayed in the tab ring while Tk never scrolled to follow focus -- Tab led
to fields the user could neither see nor reveal. Its height was also a magic
0.7*toplevel above ~230-290px of fixed chrome, i.e. satisfied at 1080p and
violated at 768p, where SUBMIT/BACK were pushed off the bottom.

All of that is gone by deletion: the surface container is now a
gatelock.ScrollableSurface, so the inner viewport is redundant. build_surface
and _ensure_container both use it, which fixes the same symmetric-clipping
problem for EVERY screen rather than one at a time; center_when_fits keeps
short screens looking exactly as they did.

Re-arming after a repaint hooks into clear_container(), which all ~19 repaint
sites already call. "Remember to call settle after painting" is a rule that
would eventually be missed, and on a lock a missed re-arm means unreachable
content.

The aux windows hardcoded geometry("600x400") and ("700x450") while displaying
more than that -- the relaxed-day prompt's own button row requests 862px inside
a 700px window, sheared at both edges. That pre-dates this work by a wide
margin (Tk's default focus ring accounts for 4px of the 162px overflow). They
now size to content, floored at the old values and clamped to the display.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:25:45 +02:00
600d0fd084 Migrate screen_locker to gatelock v0.2.0 per-output locking
The lock now paints one container per live output instead of one window on
the primary, and claims the screen through an Arbiter at RANK_SCREEN_LOCKER
so the wake alarm can outrank it.

The fan-out happens at the widget factory, not the render loop. Painting a
screen here is not a pure function -- the ~20 clear_container() flows record
workouts, credit weeks and branch on state while they paint -- so re-running
a flow per surface would double those side effects. Instead `_label`,
`_button_row` and friends build N copies and return a group that mirrors
later configure calls to all of them, and a flow body still runs once. Frame
groups nest, so `_button(button_row, ...)` reads exactly as it did.

The degenerate case carries as much weight as the general one: the status
window, the post-sick-day verify window and the relaxed-day prompt have no
lock and no surfaces, and share this same factory. They get a group of
exactly one frame and behave as they always did.

Text boxes are the one thing that cannot be shared. Entries fan out over a
single StringVar, but tk.Text has no such variable, so the sick-day
description genuinely diverges per monitor; TextGroup.get() returns whichever
copy was actually typed into, preserving "answer on any monitor".

Tests: the suite hung rather than failed on first run, which is the trap
gatelock's own conftest documents -- screen_locker patches its own `tk`, but
LockWindow builds Toplevels through gatelock's, and real tkinter over a
MagicMock root recurses without bound. `_gatelock_fixtures` closes that and
redirects the runtime dir so no test can stand a production locker down.
Every other test runs single-output, where fan-out is unobservable, so
test_surface_fanout runs on two outputs; each assertion was checked by
cropping the group loops to the primary and confirming it goes red.

874 tests, 100% branch coverage, pre-commit clean, no lint suppressions.
2026-07-25 16:26:39 +02:00
0c6f6957a8 Wire gatelock.LockConfig into every window, not just the lock overlay
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
self._colors = LockConfig(...) is now built unconditionally in
ScreenLocker.__init__ (previously only the fullscreen lock path built a
local `config`), and every raw hex literal across status_view.py,
_ui_flows.py, _ui_flows_relaxed.py, _sick_dialog.py,
_manual_workout_dialog.py, _manual_workout_widgets.py, and
_temperature_status_mixin.py now reads from it instead -- replacing
ad hoc greens/oranges/reds (#00ff00, #0066cc, #ff4444, ...) with the
shared success/accent/danger/warning/muted tokens. Also fixes the
verify-window and status-window paths that still hardcoded #1a1a1a for
the background.

This was the prerequisite the on-fill fix (previous commit) actually
needed: _window_setup.py/_ui_widgets.py/_heat_skip.py reference
self._colors, which nothing in the codebase constructed outside the
fullscreen lock path until this commit.

Verified: 859/859 tests, 100% coverage.
2026-07-21 21:45:59 +02:00
7bb43422f2 Fix silent skip-credit bypass; replace with weekly shutdown-time bonus
The screen locker skipped enforcement on 2026-07-03 without ever showing
a lock: a banked skip credit (earned from a prior 5+/week streak) was
consumed automatically with no confirmation and no visible log. Reworked
the whole reward mechanic instead of just gating it, since banking a
"skip a future workout" credit works against maximizing weekly workouts:

- Removed skip credits entirely (has_skip_credit/consume_skip_credit and
  the confirmation dialog built to gate them). The only same-day skip
  paths left are heat_skip and sick_day, both requiring a genuine reason.
- Extra workouts (5+/week) now bank shutdown-time-later hours for the
  following week instead — comfort, not reduced enforcement. Reuses the
  existing _adjust_shutdown_time_by and reset_to_base_if_new_day's
  previously-discarded return value as the once-per-day gate.
- early_bird and sick_day no longer pollute workout_log.json. early_bird
  is a same-day pending marker now stored in its own self-expiring,
  HMAC-signed file; sick_day is sourced entirely from sick_history.json
  (already the real source of truth). Fixes an accidental-safety gap
  where "already took a sick day today" only halted startup by luck.
- Cleaned up 3 stale non-workout entries already in workout_log.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdTccgbK7624kfoaV6CtXS
2026-07-03 15:27:08 +02:00
d8c03d78ce Migrate to the shared gatelock backend
ScreenLocker now composes gatelock.GateRoot + gatelock.LockWindow for the
actual lock window instead of the inline WindowSetupMixin mechanics; the
verify/relaxed-day auxiliary windows (never the lock itself) stay as
plain Tk windows. The hand-copied _log_integrity.py is deleted in favor
of gatelock.log_integrity (the canonical, non-duplicated module). This
is the second of three migrations (diet_guard done, wake_alarm next).

Two deliberate behavior changes, both confirmed:
- dependencies = [] (pure stdlib) now includes gatelock, a documented
  departure from the prior zero-deps stance.
- production grab upgraded from single-attempt-then-local-fallback to
  diet_guard's retry-forever (robust to e.g. a fullscreen game holding
  the grab).

Net hardening as a side effect: run()/close() now go through gatelock's
signal-safe lifecycle, so SIGTERM/SIGINT restore VT switching on every
exit path -- previously only a clean close() did, leaving VT switching
disabled if the service was killed mid-lock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCdT46zV8hESDvbgYMGDLt
2026-06-21 20:11:16 +02:00
e8bef664f5 chore: set up as standalone repo
Extracted from testsAndMisc monorepo. Changes:
- Rewrote imports from python_pkg.screen_locker.* → screen_locker.*
- Vendored python_pkg.shared.log_integrity → screen_locker._log_integrity
- Vendored wake_alarm constants (ALARM_DAYS, WAKE_AFTER_HOURS, RTCWAKE_BIN) into _constants.py
- Extracted has_workout_skip_today into new screen_locker._wake_state module
- Added tests for _wake_state.py (392 tests, 100% branch coverage)
- Moved scripts/service files to repo root
- Added standalone pyproject.toml, requirements.txt, .pre-commit-config.yaml, .gitignore
- Added GitHub Actions CI workflows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:43:06 +02:00
b3ad6d06f5 refactor: split oversized SBE modules, extend screen locker, and enhance Horatio demo
steam-backlog-enforcer:
- Split hltb.py (>800 lines) into _hltb_types.py, _hltb_detail.py, hltb.py
- Split main.py into _cmd_done.py + main.py to stay under 500-line limit
- Split test_hltb.py into test_hltb.py, test_hltb_search.py, test_hltb_detail.py
- Split test_main.py: move TestTryReassignShorterGame → test_cmd_done.py
- Update test_main_part2.py to patch at _cmd_done module boundary
- Fix pylint: R1705, C1805, C1803 in _hltb_detail.py and hltb.py
- Set pre-commit --fail-under=8.0 (was 10.0; pre-existing files scored ~8.5)

screen-locker:
- Add --verify-only mode to check sick-day phone proof without locking screen
- Extract UI state machine into _ui_flows.py for testability
- Add test_verify_workout.py covering the new verify-only path
- Update run.sh to support --verify flag

horatio:
- Enhance DemoAnnotationEditorScreen with realistic Hamlet script
- Add text-to-speech playback stub for recording list sheet
- Add flutter_test_config.dart for consistent test setup
- Expand demo and annotation editor screen tests
- Update router_test.dart for new screen parameters

misc:
- Update pomodoro_app/pubspec.lock dependencies
- Update .gitignore for new build artifact patterns
2026-03-29 22:50:24 +02:00