Commit Graph

12 Commits

Author SHA1 Message Date
26f1770090 feat: import Kuchnia Wikinga catering deliveries into the food bank
Pulls the day's delivered dishes and their real per-portion macros out of
panel.kuchniavikinga.pl, so a catering meal is one keypress instead of five
retyped numbers.

A delivered meal is not an eaten meal, so nothing is ever logged unattended:
dishes land in the *curated* bank, and entries are written only after an
explicit confirmation. Auto-logging would let the gate satisfy its own
checkpoint from a delivery note, and would record what the courier dropped off
rather than what was eaten.

Three surfaces:
  - `diet-guard kuchnia [--log] [--yes]`
  - the lock screen's "Today's delivery" button, which prefills the meal form
    dish by dish (the user still clicks Log & Continue)
  - a warm-up after each logged meal, on the existing background thread

All event-driven; no new systemd timer. The two automatic paths go through
`refresh_delivery_once`, guarded to one fetch per day.

The panel is an undocumented white-labelled Dietly API; the endpoint map was
recovered from its JS bundles and confirmed against live responses. Notes,
including the traps, are in docs/kuchnia-wikinga.md.

Load-bearing details:

* Macros are per portion and `weight` is that portion in grams -- proven, not
  assumed: 4P+4C+9F reproduces the stated calories within ~1% on every meal of
  the captured day. `_energy_is_consistent` re-runs that check per dish and
  drops any that fails; a per-100g mix-up is invisible in a payload and would
  silently skew every logged meal by a factor of several.
* `bank_dishes` compares nutritional fields before calling `add_manual_entry`,
  which restamps `t` unconditionally. Without the compare, every refresh
  republishes the whole curated bank to every peer. Tests assert on call count.
* Duplicate log suppression reads today's synced log, not a local marker:
  `log_meal` mints a fresh uuid, so a re-import after marker loss would merge
  as duplicates on every peer.
* The gate hook is in `on_focus_ready`, not `_cli_gate._should_lock`:
  `gate_is_due()` never reads the food bank, so a refresh there could not
  change the lock decision while adding a third-party round trip to the ~105ms
  `gate --check` fast path.
* `requests` is lazy-imported so the gate's not-due tick does not pay for it.
* Credentials and the cached session cookie live in ~/.config/diet_guard at
  mode 600, outside the synced tree. The credentials file is written by hand,
  never by the package -- the same contract as sync_token.

`_GateDashboard` is merged into `_GateMealFlow`, and both "pull it in from
elsewhere" flows share one `_PullFlows` mixin: pylint's max-parents counts
every MRO entry, so the gate's controller chain had no room for a new class.

Also adds a side-by-side row for the two pull buttons -- stacking them cost
42px and pushed the tab past the viewport at 1366x768.

989 tests, 100% branch coverage, pre-commit clean.
2026-08-22 23:04:22 +02:00
0173f4c56c Queue behind stronger lockers; bump gatelock to v0.7.1
Diet_guard was one of the two lockers missing gatelock's
wait_for_turn fix -- confirmed live on 2026-08-21, it held the global
grab for ~3 minutes against a newly-arriving, higher-ranked
screen_locker, which just retried and logged instead of making
progress. Now queues behind any stronger claim before arming.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:06:19 +02:00
2bdb7925a8 Read the meal schedule from a forward-only history (Python)
Wires the Python side to the configurable schedule. No edit surface yet, so
nothing writes a schedule and current_schedule() returns the default -- the
derived hours are still 08/12/16/20 and the cutoff is still 22:00. Verified by
running it: before any edit the slots are unchanged, after recording 08-20 x5
they become 08:00, 11:00, 14:00, 17:00, 20:00, and a 2020 day still resolves
to the four-meal schedule.

_slots.py now takes the schedule as a *required* argument rather than reading
module constants. Required, not defaulted, on purpose: a default would let a
call site missed in this refactor keep deriving the old fixed hours on one
device, and a slot one device offers while the other does not is a checkpoint
that can never be satisfied. Making it required let mypy enumerate the nine
call sites instead of trusting a grep.

The history is forward-only and seeds the default at the epoch *before*
recording today's value, so switching to five meals leaves past days judged
against the four-meal schedule they actually ran under. Same shape and same
reasoning as _budget_history.py; it will ride the same CRDT record.

Two details worth naming:

- MEAL_SCHEDULE_FILE lands in DATA_DIR, so conftest's _isolate_state redirect
  is patched in this same commit rather than a later one. Confirmed with a
  canary that the redirect is live and that ~/.local/share/diet_guard/ has no
  .meal_schedule after a full run.
- _mcp_read imports _budget.current_schedule already, so the meal-schedule
  lookup is aliased rather than shadowing it.

_gatelock._pending_slots is spelled `is None` rather than `or` now. The two are
in fact equivalent -- if 0 is a slot it is necessarily the first slot, checked
exhaustively -- but the intent should not rest on that coincidence.

761 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
2026-08-16 14:28:22 +02:00
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
f7f06aee53 Remove unused features; fix reminder false alarms; share all state
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
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>
2026-07-26 16:39:52 +02:00
84bd550331 Migrate diet_guard to gatelock v0.2.0 per-output locking
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
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.
2026-07-25 16:56:13 +02:00
957769ff0e Add budget-adherence calendar, streaks, and freely-editable synced budget
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
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
2026-07-13 21:31:45 +02:00
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
2026-07-11 17:54:38 +02:00
843f5e0221 Extract diet_guard from testsAndMisc as a standalone repo
Rewrites python_pkg.diet_guard imports to diet_guard, vendors the
shared as_float coercion helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
scaffolds standalone lint/test config matching testsAndMisc's real
enforced bar (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage).
2026-06-22 12:18:39 +02:00
205b214d78 Migrate diet_guard to the shared gatelock backend
MealGate now composes gatelock.GateRoot + gatelock.LockWindow instead of
inheriting the deleted _GateWindow/_GateRoot, and its HMAC signing goes
through gatelock.log_integrity. This is the first of three migrations
(diet_guard -> screen-locker -> wake_alarm) extracting the lock-window
mechanics that diet_guard's own _GateWindow proved out into a shared,
reusable package. Window-mechanics tests moved with the code; diet_guard's
suite now only tests its own wiring (LockConfig choice, hook delegation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCdT46zV8hESDvbgYMGDLt
2026-06-21 18:16:45 +02:00
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>
2026-06-14 07:19:37 +02:00
400f89b469 feat(diet_guard): add meal-logging screen-lock gate with trigger fix
Add the diet_guard package: a screen-locking meal-logging gate that fires
on 4-hour slots (08/12/16/20) and records calories/macros, persisting an
autocompleting food bank.

- Trigger fix: the systemd timer fires at session start (Persistent=true)
  before lightdm has written ~/.Xauthority, so the gate crashed with a
  TclError instead of locking the screen. Add wait_for_display() /
  _display_is_ready() in _gatelock.py and wire it into _cli._cmd_gate so the
  gate retries on the next tick instead of crashing; add
  Environment=XAUTHORITY=%h/.Xauthority to the service as belt-and-suspenders.
- Food-bank hardening: a transiently corrupt food_bank.json was warned about
  on every keystroke and then silently overwritten (data loss). _read_bank
  now quarantines it via _quarantine_corrupt_bank() (warn-once + timestamped
  backup) before starting fresh.
- Multi-item meals: new _meal.py (MealItem, meal_total, MEAL_SOURCE),
  remember_meal() + _upsert() in _foodbank.py, and a "+ Add item" control in
  the gate that logs both the individual items and the composite meal.
- Bundle resolve_nutrition's manual macros into a ManualMacros dataclass to
  stay within the argument-count limit.

diet_guard at 100% branch coverage; full pre-commit suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:32:39 +02:00