From f7f06aee5396bf9d5e4e2be4c7927427c1d65f9a Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 26 Jul 2026 16:34:15 +0200 Subject: [PATCH] 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: 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) --- CLAUDE.md | 68 ++- app/android/app/src/main/AndroidManifest.xml | 4 - app/lib/desktop/wrapper_server.dart | 14 +- app/lib/main.dart | 2 + app/lib/models/food_bank_record.dart | 46 +- app/lib/models/food_entry.dart | 41 +- app/lib/models/meal_item.dart | 61 --- app/lib/models/slot.dart | 17 + app/lib/screens/calendar_screen.dart | 16 +- app/lib/screens/edit_entry_screen.dart | 3 +- app/lib/screens/history_screen.dart | 94 +--- app/lib/screens/log_meal_screen.dart | 175 +++---- app/lib/screens/meal_builder_screen.dart | 152 ------ app/lib/screens/photo_viewer_screen.dart | 37 -- app/lib/screens/settings_screen.dart | 79 ++-- app/lib/services/app_settings_service.dart | 76 +-- app/lib/services/background_sync_service.dart | 3 + app/lib/services/blob_store.dart | 22 - app/lib/services/blob_store_factory.dart | 4 - app/lib/services/blob_store_indexeddb.dart | 84 ---- app/lib/services/blob_store_io.dart | 49 -- app/lib/services/blob_store_web.dart | 24 - app/lib/services/budget_history_service.dart | 133 ++++++ app/lib/services/budget_schedule.dart | 167 +++++++ app/lib/services/day_status_service.dart | 31 +- app/lib/services/desktop_wrapper.dart | 5 +- app/lib/services/due_slot_check.dart | 57 ++- app/lib/services/foodbank_service.dart | 21 +- app/lib/services/log_storage_service.dart | 13 - app/lib/services/photo_attach_service.dart | 56 --- app/lib/services/sync_merge.dart | 226 ++++++++- app/lib/services/sync_service.dart | 102 ++-- app/lib/widgets/attached_image.dart | 10 - app/lib/widgets/attached_image_io.dart | 58 --- app/lib/widgets/attached_image_web.dart | 92 ---- app/lib/widgets/photo_attach_field.dart | 138 ------ app/lib/widgets/today_progress_card.dart | 142 ++++++ app/pubspec.lock | 120 ----- app/pubspec.yaml | 2 - app/test/desktop/wrapper_server_test.dart | 44 +- app/test/models/food_entry_test.dart | 15 +- app/test/models/meal_item_test.dart | 60 --- app/test/models/slot_test.dart | 42 ++ app/test/screens/calendar_screen_test.dart | 5 + app/test/screens/history_screen_test.dart | 167 +++---- .../screens/log_meal_screen_sync_test.dart | 46 -- app/test/screens/log_meal_screen_test.dart | 435 +++--------------- .../screens/meal_builder_screen_test.dart | 265 ----------- app/test/screens/settings_screen_test.dart | 45 +- .../services/app_settings_service_test.dart | 211 +++++---- .../background_sync_service_test.dart | 11 +- app/test/services/blob_store_io_test.dart | 65 --- .../services/budget_history_service_test.dart | 133 ++++++ app/test/services/budget_schedule_test.dart | 168 +++++++ .../services/day_status_service_test.dart | 53 ++- app/test/services/due_slot_check_test.dart | 124 ++++- app/test/services/indexeddb_stores_test.dart | 62 +-- .../services/log_storage_service_test.dart | 69 --- .../services/photo_attach_service_test.dart | 68 --- app/test/services/sync_merge_test.dart | 314 ++++++++++++- app/test/services/sync_service_test.dart | 63 +-- .../widgets/today_progress_card_test.dart | 93 ++++ diet_guard/__init__.py | 8 + diet_guard/_budget.py | 55 +++ diet_guard/_budget_history.py | 233 ++++++++++ diet_guard/_cli.py | 4 +- diet_guard/_constants.py | 10 + diet_guard/_daystatus.py | 38 +- diet_guard/_foodbank.py | 66 +-- diet_guard/_foodbank_manual.py | 87 ++++ diet_guard/_gatelock.py | 1 - diet_guard/_gatelock_calendar.py | 15 +- diet_guard/_gatelock_core.py | 5 +- diet_guard/_gatelock_groups.py | 5 - diet_guard/_gatelock_mealflow.py | 112 +---- diet_guard/_gatelock_ui.py | 54 --- diet_guard/_gatelock_ui_types.py | 4 - diet_guard/_mcp.py | 4 +- diet_guard/_meal.py | 93 ---- diet_guard/_slots.py | 31 +- diet_guard/_sync.py | 130 +++++- diet_guard/_sync_merge.py | 213 ++++++++- diet_guard/_test_guard.py | 110 +++++ diet_guard/tests/conftest.py | 8 + diet_guard/tests/test_budget_history.py | 288 ++++++++++++ diet_guard/tests/test_daystatus.py | 46 +- diet_guard/tests/test_foodbank.py | 41 +- diet_guard/tests/test_foodbank_manual.py | 124 +++++ diet_guard/tests/test_gatelock_mealflow.py | 134 +----- diet_guard/tests/test_mcp.py | 2 +- diet_guard/tests/test_meal.py | 60 --- diet_guard/tests/test_slots.py | 42 ++ diet_guard/tests/test_sync.py | 48 +- diet_guard/tests/test_sync_banks.py | 246 ++++++++++ diet_guard/tests/test_sync_merge.py | 124 ++++- diet_guard/tests/test_sync_merge_banks.py | 188 ++++++++ diet_guard/tests/test_test_guard.py | 104 +++++ 97 files changed, 4462 insertions(+), 3273 deletions(-) delete mode 100644 app/lib/models/meal_item.dart delete mode 100644 app/lib/screens/meal_builder_screen.dart delete mode 100644 app/lib/screens/photo_viewer_screen.dart delete mode 100644 app/lib/services/blob_store.dart delete mode 100644 app/lib/services/blob_store_factory.dart delete mode 100644 app/lib/services/blob_store_indexeddb.dart delete mode 100644 app/lib/services/blob_store_io.dart delete mode 100644 app/lib/services/blob_store_web.dart create mode 100644 app/lib/services/budget_history_service.dart create mode 100644 app/lib/services/budget_schedule.dart delete mode 100644 app/lib/services/photo_attach_service.dart delete mode 100644 app/lib/widgets/attached_image.dart delete mode 100644 app/lib/widgets/attached_image_io.dart delete mode 100644 app/lib/widgets/attached_image_web.dart delete mode 100644 app/lib/widgets/photo_attach_field.dart create mode 100644 app/lib/widgets/today_progress_card.dart delete mode 100644 app/test/models/meal_item_test.dart delete mode 100644 app/test/screens/meal_builder_screen_test.dart delete mode 100644 app/test/services/blob_store_io_test.dart create mode 100644 app/test/services/budget_history_service_test.dart create mode 100644 app/test/services/budget_schedule_test.dart delete mode 100644 app/test/services/photo_attach_service_test.dart create mode 100644 app/test/widgets/today_progress_card_test.dart create mode 100644 diet_guard/_budget_history.py create mode 100644 diet_guard/_foodbank_manual.py delete mode 100644 diet_guard/_meal.py create mode 100644 diet_guard/_test_guard.py create mode 100644 diet_guard/tests/test_budget_history.py create mode 100644 diet_guard/tests/test_foodbank_manual.py delete mode 100644 diet_guard/tests/test_meal.py create mode 100644 diet_guard/tests/test_sync_banks.py create mode 100644 diet_guard/tests/test_sync_merge_banks.py create mode 100644 diet_guard/tests/test_test_guard.py diff --git a/CLAUDE.md b/CLAUDE.md index 6fc81e1..96d4079 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,13 @@ tally, both derived from the same day-status classification (`_daystatus.py` / `day_status_service.dart`). See `docs/design.md` for the original feature spec (meal-slot timing logic, -the Tue/Wed/Thu "filled most of the day" catch-up rule, multi-item meals). +the Tue/Wed/Thu "filled most of the day" catch-up rule). Note that the +multi-item **meal builder** described there was removed on 2026-07-26 as +unused, on both the gate and the phone; only its `components` *storage* +remains, so already-logged composite meals keep their per-item macros. Nothing +creates a new one. The phone's "repeat last meal" button and the +temptation-bundling reward prompt were removed in the same pass (the reward +was replaced by a post-log progress card). ## Scheduling @@ -58,10 +64,24 @@ fine-grained GitHub PAT scoped to `syncs`'s contents (read/write), then save it to `~/.config/diet_guard/sync_token`, mode 600. Until that file exists, every sync tick is a harmless no-op that logs `sync not configured`. -The food bank stays *derived*, never synced: only `food_log.json` round-trips -through GitHub, and each device rebuilds its own `food_bank.json` locally by -replaying the merged log (`_foodbank.rebuild_food_bank`) — this is what avoids -needing CRDT counter-merge logic for a food's `count`. +**Both halves of the food bank sync.** The derived bank +(`food_bank.json`) is still rebuilt locally from the merged log +(`_foodbank.rebuild_food_bank`), but it is then merged and pushed too, so the +two devices agree *immediately* instead of only after each has replayed the +log. Its merge clock is the record's own **`count`**, not a wall time: LWW +therefore means *max-count-wins*, which is the correct merge for a derived +counter (the device that has seen more of the log has the higher count) and +is idempotent, because the count only moves when the log does. That is what +sidesteps needing a real counter CRDT. + +The **hand-curated** bank also syncs +(`food_bank_manual.json`, `_foodbank_manual.py` / the manual half of +`foodbank_service.dart`). Those are foods the user added without ever eating +them, so they are not derivable from any log; before they synced they were +the last piece of device-local state in the app. One CRDT record per +normalized name, last-writer-wins by an `editedAt` stamp, union across +devices. `lookup_food`/`search_foods` merge them under the log-derived +records, so a food curated on the phone autocompletes in the PC gate. ## Production dependency installation — read this before adding any dependency @@ -103,9 +123,29 @@ silently does **not** reach the running service. timestamp so `diet-guard-sync.timer`'s `budget.json` sync (mirroring the food-log sync) resolves concurrent edits last-writer-wins. Do not reintroduce the seal as a "fix" — the removal is the feature. +- **Past days are judged against the budget that applied then.** + `~/.local/share/diet_guard/.budget_history` (`diet_guard/_budget_history.py`, + mirrored by `app/lib/services/budget_schedule.dart`) is a forward-only list + of `(effective_from, kcal)` entries; `status_map`/`statusMap` resolve each + day through it, so lowering the budget never retroactively breaks the + adherence streak or the YTD tally. It syncs as extra `hist:` + *fields* on the existing `budget` CRDT record rather than as a second + document: `merge_record` is per-field LWW over the **union** of field names + and both devices push the *merged* log, so a device that predates the + feature relays those fields untouched. `write_budget` / + `saveDailyKcalGoal` must seed the pre-write value to `1970-01-01` **before** + recording the new one for today — reversing that makes every past day adopt + the new budget, which is the exact bug this exists to prevent. - **Biometrics are used once and discarded.** `init` asks for biometrics to compute the daily budget, then the only persisted output is the computed budget number — never the biometrics themselves. +- **PC and phone share one source of truth for every piece of data.** The + food log, the budget, the budget history, body weight (`w`, its own + `weight` CRDT field), and the curated food bank all round-trip through + `kuhyx/syncs`. The derived food bank is a local cache of the synced log, + not separate state. There is deliberately nothing left that only one + device knows — if you add a new stored field, sync it or explain in a + comment why it physically cannot be shared. - **State lives entirely under `~/.local/share/diet_guard/`** — no cross-repo file coupling (unlike wake_alarm, which reads `~/screen-locker/screen_locker/workout_log.json`). Safe to reason about in @@ -162,9 +202,21 @@ Consequences worth knowing before touching `app/`: ## Do NOT -- Don't relax the meal-slot/macro logic without re-reading `docs/design.md` — - the Tue/Wed/Thu catch-up rule and multi-item meal summing are deliberate, - not accidental complexity. +- Don't relax the meal-slot logic without re-reading `docs/design.md` — the + Tue/Wed/Thu catch-up rule is deliberate, not accidental complexity. The + off-hours **clamp** in `slot_for_log`/`slotForLog` is likewise deliberate + (a meal before 08:00 counts toward 08:00, after 22:00 toward 20:00) and + must stay byte-identical across Python and Dart; do NOT "fix" it by + widening `elapsed_slots`, which would make every slot fall due at 23:00. +- Don't re-add the meal builder, the "repeat last meal" button, the reward + prompt, or **meal photos** — all four were removed deliberately as unused. + The photo removal took `image_picker`, the blob stores, the wrapper's + `/blobs/` route and the `CAMERA` permission with it; re-adding photos means + re-adding all of that *plus* syncing image blobs, since a device-local + attachment would violate the shared-source-of-truth rule above. Do not strip the + `components` field either: nothing writes it any more, but historical + composite entries and the food bank still read it, and it is part of the + sync wire format. - Don't add a dependency without doing the production install-path check above. - Don't reintroduce a seal/lock/`chattr +i` on the budget file as a "fix" — diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 497993a..071eaf1 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,4 @@ - -