Commit Graph

5 Commits

Author SHA1 Message Date
da0b54d886 Sync meals and the food bank through Firebase, mirroring to GitHub
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
The largest consumer moves last on purpose: diet_guard was 71% of all sync
traffic, and both its surfaces (the PC timer and the phone app) share one
prefix, so they move together.

Firebase becomes the primary backend when it is set up, with GitHub kept as a
mirror so a device that has not moved yet still converges. An unconfigured
device keeps syncing exactly as before, which is also the rollback.

Revision tracking is new here. The PC hand-rolled its own pull-merge-push
instead of calling sync_log, so it had none: every tick re-downloaded every
peer's whole food log -- hundreds of KB, 96 times a day -- and re-pushed its
own regardless of change. It now reads one small revision map first, skips any
peer whose hash is unchanged, and suppresses a no-op push. Every sub-sync
(log, budget, food bank, manual bank) takes the client as a parameter, so one
swap covers all four.

The app could not have reached Firebase at all: nothing called saveAccount(),
so loadAccount() always returned null and openFirebase() always returned null.
Settings now has a Firebase section that stores the account in the OS keystore
and signs in immediately, so a typo surfaces on the screen where it can be
fixed rather than as a silently failed background sync hours later. The email
and password never touch the repo -- it is public.

Platform reaches now degrade instead of failing: an unavailable keystore or
application-support directory costs one tick of extra traffic, where before it
would have taken sync down entirely. Caught by `on Object` rather than
`on Exception`, because an uninitialised binding raises an Error.

Verified end to end against the real database, not just in tests: a live PC
sync merged 57 days / 339 entries and put 221 KB of food log plus 97 food-bank
records into Firebase; a second unchanged tick issued zero pushes; and the
Dart client signed in and read back all 339 meals and 97 foods, then completed
a push / publish-revision / suppress-no-op round trip under a throwaway prefix.

diet_guard: 618 tests at 100% branch coverage.
diet_guard_app: 477 tests. Two assertions moved from 4 pushes to 5 -- the
extra one is this device's revision, published after the log so a peer can
never cache "seen rev X" against a log it never received.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
2026-08-05 08:35:21 +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
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
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
67bc98c7e0 app: offline backstop — connectivity-gated push on reconnect
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
Immediate push on log only helps when the device is online. If a meal is
logged while offline, the in-process auto-sync fails silently and the meal
waited for the next app lifecycle event. Now each log also enqueues a one-off
WorkManager task constrained to NetworkType.connected, so the log uploads as
soon as connectivity returns, without the app being reopened.

- background_sync_service.dart: backgroundSyncPush() loads SyncSettings and
  runs runSync, returning WorkManager's success/retry flag (true when nothing
  to push or the push succeeded; false to retry after a transient failure).
  Unit-testable half; the dispatcher branch and the one-off registration are
  integration-only (real background isolate) and coverage-ignored, matching
  the existing periodic check.
- log_meal_screen.dart: _enqueueSyncBackstop() registers the task (replace
  policy coalesces a burst of logs; linear backoff retries) from both log
  paths. No-op off mobile.
- background_check_service.dart: dispatcher routes the new task name.
- log_storage_service.dart: writeLog is now atomic (temp file + rename) — the
  background push's runSync can write the log in a separate isolate while the
  foreground app also writes, so a torn read must be impossible.

Verified on device (BL9000EEA0000102): airplane mode on -> log -> background;
airplane mode off -> the phone's remote food_log.json commit advanced within
~15s, app never reopened (also confirms the token loads in the background
isolate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 19:11:54 +02:00