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>
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
Address code-review feedback on the sync-before-lock change:
- pull_shared_log now catches (SyncError, GitHubSyncError, OSError) instead
of a blanket `except Exception`. crdt_sync wraps every requests transport
error in GitHubSyncError, so those three are the whole realistic failure
surface; the gate still fails closed on all of them, and a genuine bug now
surfaces instead of being hidden as "sync unavailable". Drops the
broad-exception pylint disable and the per-file BLE001 ignore in pyproject.
- Rename _cli_gate._is_due -> _should_lock: too easily confused with the pure
local predicate gate_is_due it wraps. The new name says what it decides.
- Rename the list `satisfied` -> `satisfied_slots` in _reconcile_after_fetch;
it's a list of slots, not a boolean.
No behaviour change; 100% coverage retained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
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
Replace the bespoke GitHub-Gist sync with the shared crdt-sync /
crdt_sync_dart libraries so diet-guard uses the same CRDT transport as
the other apps.
- Python: delete _sync_github.py (+ its test); rewrite _sync.py,
_cli_sync.py, _sync_merge.py against crdt-sync. Add the crdt-sync git
dependency (pyproject + pylint deps) and S107 to the test ignore list.
- Dart: rewrite app sync_service.dart / sync_merge.dart against
crdt_sync_dart (new git dep + crypto); update log_meal and settings
screens and their tests.
Verification: pytest 347 passed at 100% coverage; flutter analyze clean;
flutter test 306 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
gatelock and diet-guard-sync were folded into shared monorepos:
gatelock -> github.com/kuhyx/utils (subdirectory), diet-guard-sync ->
github.com/kuhyx/syncs (subdirectory). Update the gatelock pip URL,
SYNC_REPO_NAME, the devices-dir path prefix (PC and phone), and add a
persisted-value migration so an already-configured phone picks up
'syncs' on next load.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pulls every other device's pushed log from GitHub-backed dumb storage,
merges it with the local log, and pushes this device's own merged copy
back -- the PC half of the diet-guard-app sync plan.
- _sync_merge.py: pure union-by-id merge, tombstone always wins, legacy
(time, desc) dedup for pre-id entries. Commutative and idempotent.
- _sync_github.py: minimal GitHub Contents API client (list/get/put),
distinguishing a 404 on an unused path from the repo itself being
unreachable.
- _sync.py: orchestration -- pull, merge, re-sign every persisted entry
regardless of origin, write, rebuild the food bank, push. Re-signing
unconditionally is load-bearing: an unsigned phone-origin entry would
otherwise be silently dropped on the very next read once a machine
holds the shared HMAC key.
- _foodbank.rebuild_food_bank(): the "replay a full log into a fresh
bank" entrypoint the Python side was missing (the Dart port already
had its equivalent). Backs sync's bank-rebuild step.
- New diet-guard-sync.service/.timer (15-minute cadence, headless, a
separate unit from the gate so a held lock can't stall sync) and a
new install.sh step to install them.
- Created the private kuhyx/diet-guard-sync GitHub repo for storage.
Incidental to this feature: adding the `sync` subcommand pushed _cli.py
past the repo's 500-line cap, so `gate`'s CLI glue moved out alongside
sync's into _cli_gate.py/_cli_sync.py -- same split pattern already used
for the gate window logic itself, not a sync-specific design choice.
338 tests, 100% branch coverage. Verified importing and running cleanly
under /usr/bin/python (the production interpreter), not just the dev
venv -- the gap that caused the earlier 3-day outage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF