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
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