mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-12 07:50:50 +01:00
97959e892d
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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 |
|||
| 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>
|
|||
| f7f06aee53 |
Remove unused features; fix reminder false alarms; share all state
Four asks, plus the fixes two review passes turned up. Removals (all unused): - "Repeat last meal" button, and the reward/temptation-bundling prompt it was the sole trigger for. Replaced with a post-log progress card showing kcal against budget, macros, and the adherence streak. - The multi-item meal builder, on both the gate and the phone. The stored `components` field stays: nothing writes it now, but historical composite entries and the food bank still read it and it is part of the wire format. - Meal photos entirely: 13 files, `imagePath`, the photo filter, the wrapper's /blobs/ route, image_picker, and the CAMERA permission. Reminder false alarms had three real causes, all fixed: - Logging never reconciled notifications, so a reminder sat on screen up to 15 minutes after the meal was logged -- on the phone itself. - The background check never pulled, so a PC-logged meal nagged forever until the app was opened. Now mirrors _cli_gate._should_lock: cheap local check, network only when a slot looks due, then re-check. - Meals before 08:00 / after 22:00 were attributed to no slot at all. New slot_for_log/slotForLog clamps them to the first/last slot, byte-identical across Python and Dart. Deliberately separate from elapsed_slots, which would otherwise make every slot fall due at 23:00. Budget history, so lowering the budget stops rewriting the past: - Forward-only (effective_from, kcal) entries; each day is judged against the budget in force then. Verified live: setting 2000 left a 36-day adherence streak and the whole calendar pixel-identical. - Rides as hist:<date> fields on the existing budget record. crdt_sync merges per-field over the union of field names and both devices push the merged log, so a device that predates the feature relays them untouched -- no coordinated rollout, no second document. - The seed guard tests emptiness, not file presence: _sync can write an empty history after merging with a pre-feature peer, and a presence check would then never seed again, silently reclassifying every past day. One shared source of truth for every piece of data: - Body weight now travels as its own `weight` CRDT field. Inside `value` it was collateral damage of whole-map LWW -- any phone budget edit deleted it, and the protein target with it. - Both halves of the food bank sync. The curated half (foods added without eating them) was the last device-local state. The derived half merges with count as the clock, so LWW means max-count-wins; the log stays authoritative for membership, because a CRDT union never shrinks and a peer's stale copy would otherwise resurrect a deleted food permanently. - Bank adapters honour tombstones, and Dart parses bank records defensively: a cast failure there is an Error, not an Exception, so one malformed remote record would have broken phone sync permanently. _test_guard.py blocks any pathlib write to real user state while pytest is in the process, whatever conftest is loaded. A throwaway test run outside the package conftest destroyed a real GitHub token during this work; this makes that class of accident impossible rather than merely discouraged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 84bd550331 |
Migrate diet_guard to gatelock v0.2.0 per-output locking
The gate is laid out once per live output instead of once on the primary, and claims the screen through an Arbiter at RANK_DIET_GUARD so the wake alarm and the workout locker can both outrank it. Three kinds of state needed three different answers. The display fields already fanned out for free -- GateVars holds root-mastered StringVars bound to labels. The typed-in fields did not: amount, per, meal name and the four macro cells were bare tk.Entry widgets, so N copies would have been N independent forms. They are variable-backed now, which is what makes logging work from whichever screen the user is actually looking at. The description box and the suggestion list cannot be shared at all -- tk.Text and tk.Listbox have no variable -- so those genuinely diverge and reads answer from the copy the user touched. The History tab is per-surface too: its grid, budget field and edit button are driven across every monitor, with the budget likewise variable-backed so the value cannot differ between screens mid-edit. Laying the gate out on the primary monitor only was considered and rejected: not having to hunt for the screen the form landed on is the entire point of per-output locking. Tests: the suite hung rather than failed on first run -- diet_guard patches its own `tk`, but LockWindow builds Toplevels through gatelock's, and real tkinter over a MagicMock root recurses without bound. `_hermetic_gatelock` 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_gatelock_fanout runs on two; each assertion was checked by cropping the fan-out loops to the first surface and confirming it goes red. _gatelock_ui and _gatelock_calendar were each split in two to stay under the 500-line limit, and GateVars' typed-in fields moved into a nested GateEntryVars rather than raising pylint's attribute cap. 470 tests, 100% branch coverage, pre-commit clean, no lint suppressions. |
|||
| 957769ff0e |
Add budget-adherence calendar, streaks, and freely-editable synced budget
Both platforms gain a calendar tab/screen (green/yellow/red/not-logged day coloring, logging/adherence streaks, year-to-date tally), and tapping a day drills into that day's logged entries. This requires the daily budget to become a normal, freely-editable value synced between devices like the food log — replaces the previous chattr +i seal (deliberately, per user decision) with plain last-edit-wins sync via a new budget.json alongside food_log.json. The budget field defaults to 2200 kcal, starts read-only, and unlocks via an Edit/Save toggle on both the PC gate's History tab and the phone app's Calendar screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dHac1iYQKTMZEtL7xDJvA |
|||
| be94b4e147 |
Sync before locking the gate + manual "Fetch from sync" button
The PC gate forced a re-log when a meal was already logged on the phone but the 15-min sync had not yet propagated it. Two ways to close that gap: - The gate now pulls the shared log right before it would lock -- and only when a lock is otherwise due, so the network cost is paid on the rare tick that matters. A meal already pushed by the phone then unlocks without re-entry. Fails closed: any sync error (no token, network, disk write-back) falls through to the local lock decision, never skips it. - A "Fetch from sync" button on the lock screen runs the same pull on manual request: it auto-unlocks when the pull satisfies every pending slot, advances to the next meal on a partial pull, or reports no change. Disabled in --demo so the sandbox never triggers a real sync/push. - Both paths share one fail-closed wrapper, _sync.pull_shared_log(), which also keeps the single broad-except in one place. - _write_log is now atomic (temp file + Path.replace): the gate and the 15-min sync timer can now both write the log, so a torn read must be impossible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd |
|||
| 84898c0272 |
feat: split oversized modules for 500-line limit, fix kasa coverage gap
Split diet_guard/_gatelock.py, wake_alarm/_alarm.py, and the usage_report.py/_usage_report_parsing.py pair into focused sub-modules so every Python file is <= 500 lines, satisfying test_file_length.py. Install python-kasa into .venv (declared in requirements but missing after the 3.13->3.14 venv upgrade), fixing 8 failing smart_plug tests and restoring 100% coverage. Also includes prior in-progress work from the working tree: the wake_alarm Progress/View/Hardware field-grouping refactor, brother_printer query module + tests, diet_guard foodbank/state/cli updates, new shared coerce/logging_setup helpers, morning_routine orchestrator tweaks, dwm window-manager config, gaming scripts, and misc maintenance/digital-wellbeing script updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |