Commit Graph

4 Commits

Author SHA1 Message Date
e8f0ecaae1 Split the six Python gate files; guard the fake-tk patch set
Sixth batch of the 250-line sweep. All six gate sources named for this batch
are now under cap, plus _gate_layout_probe.py, which this batch's import
rewiring pulled into the changeset (over-cap count 38 -> 31).

  _gatelock_ui.py       477 -> 174  (palette/leaf-helper hub)
  _gate_layout_probe.py 346 -> 196
  _gatelock_groups.py   308 -> 137
  _gatelock_calendar_ui 295 ->  77
  _gatelock_calendar.py 286 -> 210
  _gatelock.py          263 -> 238
  _gatelock_mealflow.py 259 -> 207

New siblings: _gatelock_fields (input widgets), _gatelock_layout (assembly),
_gatelock_widgetgroups (per-widget mirroring), _gatelock_calendar_types
(bundles/palette), _gatelock_calendar_widgets (History builders),
_gatelock_dashboard and _gatelock_budgetedit (mixins), _gatelock_lockfile
(the stdlib-only flock), _gate_focus_probe and _gate_measurement.

The load-bearing part is the test seam, not the line counts.
`_gate_fixtures._GATE_TK_MODULES` patches `tk` on a hand-listed tuple, and
that list fails OPEN: a widget builder split into a new sibling that nobody
adds to it builds *real* Tk widgets against fake parents. Neither existing
detector sees that -- check_patch_targets.py only validates paths that still
resolve, and the ~8s runtime only catches network calls. It bit twice during
this split (real StringVars from make_calendar_vars returned MagicMocks
instead of the asserted text), so test_gate_tk_modules.py now asserts the
invariant directly: every module binding `tk` is in the patch set or named in
_NO_WIDGETS with a reason. It caught a pre-existing gap too --
_gatelock_support was never in the set.

Same fail-open shape drove two other choices: `escape_tab_trap` is a call-time
indirection so the keyboard test's patch of `_escape_text_tab_trap` still
bites, and the flock pair moved with its conftest patch target updated in the
same edit (that one fails closed). build_layout is imported from
_gatelock_layout directly rather than re-exported lazily, which avoids both a
cycle and an import-outside-toplevel suppression.

Verified: 703 Python tests / 100% branch coverage, the 5 layout-fits tests
under xvfb, 555 Flutter tests (unchanged), flutter analyze clean,
check_patch_targets 0, `sync` 0 (403 entries), pre-commit clean, and
`gate --demo` renders both tabs with live dashboard data.
~/.local/share/diet_guard byte-identical across the batch (gate timer paused,
restored after).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:08:03 +02:00
458f10515b Make the phone publish on every tick, and fail closed on Firebase errors
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
release-apk / release (APK) (push) Has been cancelled
Meals logged on the phone stopped reaching the PC, so the gate kept locking
for slots that were already logged and the only way through was to spam
"nothing". The PC's fetch was never broken: it was faithfully pulling a
Firebase that did not contain the phone's meals.

The phone had no working publish backstop on either transport:

- `checkAndNotify` gated its sync on `due.isNotEmpty`. Logging every meal
  promptly means nothing is ever due, so the ~15-min periodic task never
  published. It was a notification job that happened to sync, not a sync
  timer. It now syncs on every tick it is asked to, then computes due slots
  from the merged result, so a meal pulled from another device suppresses a
  nag instead of triggering one.
- `backgroundSyncPush` never called `initSyncDeviceId()`. A fresh isolate has
  its own static state, so `currentSyncDeviceId` fell back to the compile-time
  'phone' constant and published to `devices/phone/` rather than the persisted
  uuid -- which then makes the next foreground push see `unchanged` and skip
  writing the uuid directory entirely.

Separately, on the "Fetch from sync" path itself: `FirebaseSyncError` and
`FirebaseAuthError` are siblings of `GitHubSyncError` under `RemoteSyncError`,
not subclasses, so every per-request failure of the *primary* backend escaped
`pull_shared_log`'s catch tuple and raised a traceback out of a helper
documented to fail closed with a reason string. `ConfigError` needed the same
treatment by a different route: it subclasses `Exception` directly, so it is
outside `RemoteSyncError` too, and is now translated at `_client_for_run`.

The PAT is no longer mandatory either. Firebase is primary, but `_read_token`
ran before the client was ever built, so a Firebase-only machine could not
sync at all -- the commits that fixed this touched only the Dart side.

A stalled sync was invisible: the config gates returned silently and
`openFirebase` swallowed every error. `dart:developer`'s `log()` does not
reach logcat on a release build (verified on device), so logging alone would
not have surfaced it. `SyncHealth` records each tick's outcome and the log
screen shows a banner when publishing has actually stopped.

Also make the real-Tk probes bypass the window manager. A tiling WM retiles a
managed toplevel instead of honouring the requested geometry, so `winfo_height`
reported 0/1px and the layout and focus-ring assertions failed on noise rather
than on any regression -- 8 failures on the dev machine that only passed under
Xvfb. `overrideredirect` makes the measurement deterministic with or without a
WM; the probes still detect a genuine overflow at 1366x300.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RT8X32S9yDLW1Xf1PDq8z7
2026-08-10 14:35:37 +02:00
4a5e350baf Fit the meal gate on 1366x768 with a full day logged, and stop the auto-scroll
gatelock 0.4.0 (pinned here) stops the viewport following the app's own
focus_set(): on 2026-08-03 that moved the screen locker's lock screen to
mid-content and back with nobody touching the keyboard, and this gate uses
the same viewport. Scrolling now happens only on the user's own keypress,
wheel or click.

That makes fitting the screen the gate's own responsibility rather than
something a scrollbar covers for, and with a day of meals logged this one did
not: 770px of content in a 739px pane. Trimmed the vertical rhythm (the
title is one step down the scale, and the gaps around the description,
suggestions, macros and footer are one step tighter) for 718px, with room for
the dashboard to keep growing.

Its layout test now requires the gate to *fit* rather than merely be
reachable by scrolling, empty and populated. Dropped the 1024x600 tier: no
machine runs it, this gate cannot reach it without dropping content, and a
target nobody uses is a constraint that only ever costs.

The probe presses Tab before moving focus, because a bare focus_set() no
longer scrolls -- it would have been testing a path no user can take. The
Tk fakes learned pack_forget(), which the viewport calls to take the
scrollbar down whenever the content fits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 16:00:13 +02:00
1702685afd Make the meal gate usable without a pointer on a 1366x768 screen
The Log Meal tab did not fit this laptop, and had not for some time. Measured
under a real 1366x768 X server: it required 866px inside a 739px content pane
with every field EMPTY, and 938px with a day of meals logged.

Because the content frame was place()-centred it was sheared at BOTH edges at
once -- the "Diet Gate" title and the slot header (which meal you are being
asked to log) off the top, the calorie headline and the whole dashboard off the
bottom -- with no scrollbar anywhere in the package and nothing to indicate
content was missing. This is a hard lock: overrideredirect, a global grab, VT
switching disabled. Unreachable content is unrecoverable.

Two independent causes, both needed fixing:

- Every font size was handed to Tk as *points*. Tk encodes the unit in the
  SIGN of the size, and _gatelock_typography carried a comment asserting the
  opposite ("Tk has no separate px/pt distinction"). At ~100 DPI that made
  every string ~30% oversized. Negative sizes now; the docstring records why.
- There was no scroll container. build_layout now uses gatelock's
  ScrollableSurface, which top-anchors, scrolls by keyboard, and scrolls to
  follow focus.

Font sizes alone were not enough: they take the layout to 702px, still over the
571px pane at 1024x600. Both fixes are load-bearing.

The grams/items selector was mouse-only. tk.OptionMenu's Menubutton defaults to
takefocus=0, so ::tk::FocusOK rejects it and it never entered the tab ring --
verified by walking the real ring, which ran Entry -> Text -> Listbox -> Button
-> TNotebook and skipped it entirely. Switching units rescales the whole entry,
so a pointerless user could not correct "250 grams" to "3 items" inside a lock
they cannot leave without logging a meal. It is now two Radiobuttons, which are
in the ring by default and take both Space and Return -- and which post nothing,
unlike a menu, whose grab-stealing is why screen-locker bans OptionMenu on lock
surfaces outright.

Also: the focus ring was invisible (Tk's default is 1px BLACK on #211D1B) and
_build_desc actively inverted it by setting highlightbackground, the UNfocused
ring, so the box outlined while idle and went black the moment it took focus.
Enter now activates buttons (X11 Tk binds only <space>). Text widgets no longer
trap Tab. notebook.enable_traversal() is called, so Ctrl+Tab and Ctrl+PageDown
work. wraplength drops 900 -> 640, the line-length cap, which also stops a
label forcing a 900px-wide layout onto a 1024px screen.

The Flutter app had two controls with no keyboard route at all. Its desktop
target is the web build in a Chrome --app window, so a touch gesture is not a
fallback there, it is the only interaction:

- Deleting a history entry was bound solely to onLongPress. Long-press has no
  keyboard equivalent whatsoever. It now has a focusable IconButton; the
  long-press stays as a redundant pointer shortcut.
- Calendar day cells were a bare GestureDetector, so navigating to
  date-filtered history was pointer-only and invisible to a screen reader.
  Now Semantics(button: true) over an InkWell.

Plus: the filter sheet built a TextEditingController inside build() with the
caret forced to end-of-text, so every keystroke discarded it and silently undid
mid-string Home/End/arrow editing. And nothing capped width, so on the 1366px
desktop window prose ran ~180 characters per line (twice the readable limit)
and a four-digit kcal field spanned ~1334px. AppWidth adds those caps.

Tests: the render check is a failing test, not a script -- ruff runs with
--unsafe-fixes, whose T201 fix DELETES print statements, which would silently
turn a diagnostic into a no-op. test_gate_layout_fits.py was verified red on
the pre-fix code ("content 866 in viewport 768 -> NO SCROLL VIEWPORT ->
UNREACHABLE CONTENT") and green after. The Flutter suite gains its first
keyboard-reachability assertions; it previously had ~109 tester.tap calls and
zero checks that anything was focusable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:21:27 +02:00