Commit Graph

12 Commits

Author SHA1 Message Date
e761926df4 Body tab: weight/body-fat logs, BMI, ideal weight, goal-based kcal target, workouts + Health Connect steps, week planner
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 09:03:03 +02:00
8836453f8c fix: initialise the catering queue at startup
`KuchniaQueueService` had no `init()` call in `main()`, so on a real device
the whole feature would have done nothing: every entry point checks
`isInitialized` first and returns quietly, which reads as "no delivery today"
rather than as an error. All 694 tests passed throughout, because widget tests
initialise the service directly.

Caught by grepping main.dart before the phone deploy, not by the suite. So it
is now a repo invariant: `repo_invariants_test.dart` asserts main() names both
catering `init()` calls, and the assertion was checked against a main.dart
with the line deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQpCiPaSbyRpwsLkH3dgsX
2026-08-23 09:08:54 +02:00
8905d072d0 feat: carry the catering credential to the phone
The Dart half of the credential sync: a `KuchniaCredentialService` that owns
the value locally, a `sync_merge_kuchnia.dart` adapter mirroring the Python
one, and a `_syncKuchniaCredential` tick alongside the banks and budget. The
phone can now receive the panel password, which is the prerequisite for it
fetching anything itself.

One source of truth, per the answered question: the value lives in the synced
document store and nowhere else. Deliberately *not* mirrored into
`flutter_secure_storage` -- two homes for one value is how a stale password
survives a change made on the other device, and the credential has to be
readable synchronously at merge time anyway.

Gated by a second shared fixture, `tests/fixtures/kuchnia_credential.json`,
asserted by both suites. Two cases carry the weight:

- an **unparsable** edit time must fall back to the same clock on both sides
  (Python catches `ValueError` from `fromisoformat`, Dart's `tryParse` returns
  null). Verified they agree on `wall_time_ms == 0`; mutating that expectation
  fails both suites.
- a **blank half** contributes nothing, so a half-filled settings form cannot
  push an empty-string password that wins LWW against a real one.

`KuchniaCredentialService.init()` is called in the WorkManager isolate as well
as `main()`. A fresh isolate has its own static state and `_syncKuchniaCredential`
no-ops when the singleton is missing, so without it a background tick would
neither publish this device's credential nor receive a peer's -- and on the
phone the background tick is most of the syncing there is. Same class of trap
as the `initSyncDeviceId()` one already documented there.

`.toDouble()` came off the bank record: the fields are already `double`, so it
was a no-op the analyzer flags. The field *types* are what keep the encoding
byte-identical to Python's, and `kuchnia_parity_test.dart` asserts that.

654 Dart tests, 1040 Python tests, 100% Python branch coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQpCiPaSbyRpwsLkH3dgsX
2026-08-23 08:25:27 +02:00
d2e62bf210 Read the meal schedule from a forward-only history (Dart)
The phone/desktop half of the previous commit, so both devices now derive
their slots the same way. Still no edit surface, so `MealScheduleService`
returns the default and the derived hours are unchanged at 08/12/16/20.

`slot.dart` takes the schedule as a required argument, mirroring `_slots.py`,
and `slot_test.dart` gains the same every-hour x several-schedules sweep the
Python suite runs. That sweep is the contract: the two must agree on which
checkpoint a meal satisfies for every cell, not merely on the endpoints.

Also fixes a real bug this feature would otherwise have shipped on top of.
`syncToSlots` used the slot hour as the notification id but only cancelled ids
in the *current* schedule, so changing 08/12/16/20 -> 08/11/14/17/20 would
leave ids 12 and 16 posted with nothing that could ever cancel them: permanent
reminders for checkpoints that no longer exist. It now sweeps the whole 0..23
id space. The regression test was confirmed to fail against the old loop
(4 cancels vs 24) rather than passing vacuously.

MealScheduleService.current degrades to the default when the singleton is
absent, so `due_slot_check` deliberately does not init() it -- that would
reach path_provider from widget tests and from the WorkManager isolate before
main() runs. main.dart owns the real init, and background_sync_service.dart
does its own, since a fresh isolate has its own static state and would
otherwise nag against the default four meals.

609 Dart tests, 761 Python, both suites green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
2026-08-16 14:41:37 +02:00
e23f7ac3b4 Push under a persisted per-install uuid, not a fixed role id
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
release-apk / release (APK) (push) Waiting to run
diet_guard pushed as "pc" (Python), "phone" (Android) and "desktop" (web).
A role constant collides the moment a second machine takes the same role,
and after a reinstall the new install inherits the previous one's CRDT
identity -- the same reasoning home_inventory already records for choosing a
uuid. Both halves now mint a uuid once and persist it: the PC in
~/.local/share/diet_guard/.device_id, the app under SharedPreferences
'crdt.nodeId' (the key todo and home_inventory already use).

The old constant survives as the legacy id and is passed to sync_log /
syncLog, so devices/<legacy>/ is skipped as this device's own. Without that
every tick would re-download and re-merge this device's own pre-migration
log as though a peer had written it -- idempotent under LWW, but hundreds of
KB, 96 times a day. It drops to None once that path is reclaimed; a test
covers both states, including a negative control proving the skip is the
legacy id's doing.

All four documents move together (food_log, food_bank, food_bank_manual,
budget), and the hist:<date> fields riding on the budget record are
untouched -- merge_record unions field names, so they relay through
unchanged.

The resolver lives in its own module (_device.py / sync_device_id.dart)
because _sync_merge needs it for HLC stamping too and importing _sync there
would be a cycle. Dart resolves it asynchronously at startup, before
LogStorageService.init(); currentSyncDeviceId falls back to the compile-time
role constant until then, so a too-early Hlc stamp gets a valid id rather
than crashing.

conftest redirects the new id file in the same change that introduces it --
without that a test mints a uuid into the real state dir and this machine's
live sync identity is decided by whichever test ran first. Verified by
checking ~/.local/share/diet_guard/ before and after: untouched.

Verified live, not just against mocks: the sync timer ran mid-change and
migrated this machine to f8f46da6. All 358 food_log records and all 100
food_bank records are present under the new path (0 missing vs devices/pc/),
and the new log's HLCs carry only its own id -- confirming it did not
re-merge the old path as a peer.

Python: 687 tests, 100% branch coverage. App: 518 tests, analyzer clean.
Pins bumped to crdt-sync-v0.6.0 / crdt_sync_dart-v0.7.0 (pyproject,
requirements.txt, .pre-commit-config.yaml, pubspec), and the new library is
installed into /usr/bin/python's user site-packages so the production timer
does not ImportError on its next tick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZH89WEqAjuJT8WZdqc7AP
2026-08-09 17:18:42 +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
0e9acb9e18 Wire theme.dart into the app; replace remaining raw Colors.* literals
main.dart now builds MaterialApp's theme from buildAppTheme() instead of
ColorScheme.fromSeed(Colors.teal) -- theme.dart existed but was never
actually used until this commit. photo_viewer_screen.dart,
settings_screen.dart, and day_status_calendar.dart's raw Colors.*
literals (Colors.black, Colors.red, Colors.white38, Colors.grey.shade800,
...) now read from Theme.of(context) instead.

Also rewrites CLAUDE.md's Visual Design & Theming section, which
previously contradicted the unified-design-system skill (noise texture,
multi-layered shadows, ColorScheme.fromSeed -- all things that skill
explicitly forbids).

This was the other prerequisite the on-fill/warm-palette fixes actually
needed: theme.dart existing but unwired meant the app was still running
Flutter's default seeded theme.
2026-07-21 21:50:16 +02:00
c12faa3920 app: package the desktop app as the web build, and delete the Linux embedder
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
Completes the move: `linux/` is gone, replaced by the web build served by the
wrapper in a Chrome `--app` window. The icons and the .desktop installer move
out of `linux/` first -- they are not Flutter scaffolding and are still
needed; the entry now points at the packaged launcher and matches the
`--class=diet_guard_app` the wrapper passes, so the taskbar shows the app's
icon rather than a browser's.

install_arch.sh carries over the three traps each of which produced an
install that looked fine until launched: `dart build cli` rather than
`dart compile exe` (which refuses any package whose dependency graph has
native build hooks), `options=('!strip')` (makepkg would otherwise discard
the AOT snapshot, leaving a binary that runs as the bare Dart VM printing a
usage message), and no browser in `depends` (this machine's policy uninstalls
`chromium` seconds later, failing dependency resolution).

Also adds the frame-cadence instrument the performance claim is measured
with, armed by `--dart-define=DIET_GUARD_FRAME_STATS=1` and read with
`String.fromEnvironment` -- `bool.fromEnvironment` only accepts the literal
`true`, so `=1` would evaluate to false and a whole run would silently
produce no data. It reports frame-to-frame cadence rather than build/raster
durations, since work that fits the budget says nothing about the frame rate
actually achieved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:32:24 +02:00
802ee0fe6d app: run the companion app's desktop target as a web build
Flutter's Linux embedder manages ~20fps at 3840x2160 -- measured on a stock
`flutter create` counter app, so it is the toolkit rather than this code --
where the same Dart rendered by Chrome sustains ~144fps (evidence in
~/todo/docs/desktop-performance-findings.md). The desktop target therefore
becomes a Flutter *web* build; Android is untouched.

`dart:io` does not fail a web *compile* -- it resolves to a stub that throws
at runtime -- so every platform edge moves behind a conditional export rather
than being discovered later as a blank white window:

- storage: `document_store*.dart`, files on Android and IndexedDB in the
  browser (mirrored to the desktop wrapper, which is what makes a wiped
  Chrome profile recoverable). Document names stay the exact pre-web
  filenames so an installed phone keeps reading its existing log.
- photos: `blob_store*.dart` plus `attached_image*.dart`, since `Image.file`
  cannot resolve an IndexedDB key. The picker itself has web support; what
  changed is the storage and display model. Camera *capture* is genuinely
  lost on desktop -- and photos never crossed devices anyway, `imagePath`
  being stripped before push.
- scheduling: `background_tasks*.dart`. A browser has no WorkManager
  equivalent -- a closed tab runs nothing -- so the desktop runs an in-page
  5-minute check while its window is open. That loses nothing on the PC,
  where `diet-guard-gate.timer` already locks the screen on an unlogged
  slot, which is strictly stronger than a notification.
- notifications: `notification_backend*.dart`, the browser Notifications API
  in place of flutter_local_notifications.
- the GitHub token: `token_vault*.dart`. On web it is *not* held by the
  browser at all -- flutter_secure_storage's web backend is WebCrypto-wrapped
  localStorage with the wrap key beside the ciphertext, which is obfuscation,
  not encryption at rest -- so the desktop wrapper owns it and proxies the
  API (`github_client_factory*.dart`).

`Platform.isAndroid` itself throws on web, so every platform branch now tests
`kIsWeb` first; the battery-optimization row (permission_handler, no web
implementation) is hidden off Android.

The desktop pushes under device id `desktop`, not `phone`: sharing an id
would have made the two devices overwrite each other's pushed log on every
tick. The Python side enumerates device directories generically, so it needs
no change.

Tests move onto the same seams -- the file-backed stores keep the atomicity
and unreadable-file guarantees where they belong.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:31:32 +02:00
4c6083b768 Add food bank, edit-entry, and app settings screens; history redesign
Work-in-progress feature set accumulated ahead of the log-meal compact
layout change: a food bank browser, a shared entry-edit screen, an
app-wide settings service, and a substantially reworked history screen
with filtering/sorting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UorgLvWJ4huH55tmXoUAZ
2026-07-04 05:18:32 +02:00
adbfb20e9a Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4)
M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect
GitHub" button that runs the device-code flow; tapping with no client id
now opens a setup dialog (instructions + inline paste field) rather than
a buried inline hint. Bakes in the app's own OAuth App client id so fresh
installs work with zero manual config. Auto-syncs immediately after
connect. Verified end-to-end on the real phone: OAuth flow → token saved
→ PC's 48-entry log merged in (confirmed via food-bank vs manual source
labels in History).

M4 – Background meal-slot notifications: WorkManager periodic task (15 min
floor) checks for overdue slots and posts/cancels notifications via
flutter_local_notifications. New permissions: POST_NOTIFICATIONS,
WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
INTERNET (was missing — latent sync bug). "Disable battery optimization"
button in Settings. Verified on real phone: WorkManager registered, forced
run posted a real notification ("Meal not logged / You haven't logged your
16:00 meal yet."), isolated to background path (only caller is the
WorkManager dispatcher, not any foreground lifecycle hook).

AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources
to true for release even with no proguard config; R8 stripped
WorkDatabase_Impl's reflection-only constructor, crashing every launch
with NoSuchMethodException. Explicitly disabled both flags in
build.gradle.kts. Verified via dexdump (constructor present) and on-device
launch (no crash). Proper R8 keep rules are the long-term fix; tracked.

177 tests, flutter analyze clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
ee5a7660cb Add Flutter companion app skeleton with local meal logging
Milestone 1 of the diet-app-as-wise-balloon plan: a phone-native way to
log meals away from the PC, sharing the exact on-disk JSON shape
diet_guard already uses (same field names, no translation layer).

- lib/models/: 1:1 Dart mirrors of the Python dataclasses (Nutrition,
  FoodEntry, MealItem, FoodBankRecord, Slot), including the per-100g/
  amount-eaten portion scaling that matches _resolve.resolve_nutrition's
  semantics exactly.
- lib/services/log_storage_service.dart: plain-JSON persistence to
  food_log.json's exact shape (no sqflite -- the canonical format
  already is this JSON).
- lib/services/foodbank_service.dart: ports _foodbank.py's upsert/fuzzy
  search logic for autocomplete.
- lib/screens/: log_meal_screen.dart (single-item logging) and
  meal_builder_screen.dart (composite multi-item meals, logging full
  per-component macros via the new components field).

Verified end-to-end on a physical device (BL9000): built, installed,
logged a real meal through the UI. 77 Flutter tests passing, `flutter
analyze` clean against very_good_analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:22:42 +02:00