Commit Graph

6 Commits

Author SHA1 Message Date
c77e7d9acf Split the last four Dart lib files; file-length gate now reports zero
Final batch of the 250-line sweep. `check_file_length.sh --all` exits 0: every
file in the repo is at or under 250 lines.

  github_mirror_screen.dart  391 -> 205  (+ actions, client_id_dialog parts)
  foodbank_service.dart      314 -> 209  (+ foodbank_service_manual part)
  settings_screen.dart       286 -> 237  (+ settings_kcal_goal part)
  firebase_backend.dart      282 -> 100  (+ firebase_client.dart)

Three of the four are `part` files rather than separate libraries, and that is
forced rather than stylistic: the moved code drives private state
(`_busy`, `_status`, the text controllers) and calls private helpers
(`_readBank`, `_normalize`), none of which are visible across a library
boundary in Dart. Widening them to public just to split a file would leak
internals the rest of the app has no business calling.

Within those parts, mixins rather than extensions, for a concrete reason: an
extension is not a subclass of `State`, so calling `setState` from one is an
`invalid_use_of_protected_member`. Fields the State must own (the controllers
it creates and disposes) stay declared there and are reached through abstract
getters.

`firebase_client.dart` is a real library split -- opening an authenticated
client has no private coupling to the keystore-backed account record, only to
its public API. The one exception, dropping the account marker without setting
the opt-out flag, became `forgetAccountMarker()` rather than exposing the
secure store.

Verified: flutter test 555 (unchanged), flutter analyze clean, 707 Python
tests / 100% branch coverage, check_patch_targets 0, pre-commit clean,
check_file_length.sh --all exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 21:07:24 +02:00
546041584f Add Google sign-in support for the Firebase sync account
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
release-apk / release (APK) (push) Has been cancelled
One-tap sign-in for a fresh install, matching the pattern already shipped
in todo and crdt_sync_dart: openFirebaseWithGoogle() (google_platform.dart
abstracts the plugin's platform-channel dependence), isFirebaseConfigured()
that reads the stored session rather than trusting the account marker
alone (a marker with no live session is what made a synced phone read as
"not connected" for a full session), and storedAccount() for read-back
without the desktop-wrapper fallback that throws on Android.

Also generalizes crdt_sync_dart's ref pin to v0.10.0 and vendors the
override-guard script for the pre-commit hook already wired to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdSp6Nbrs9xvsWkCMDN9a1
2026-08-12 23:36:10 +02:00
458f10515b Make the phone publish on every tick, and fail closed on Firebase errors
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
release-apk / release (APK) (push) Has been cancelled
Meals logged on the phone stopped reaching the PC, so the gate kept locking
for slots that were already logged and the only way through was to spam
"nothing". The PC's fetch was never broken: it was faithfully pulling a
Firebase that did not contain the phone's meals.

The phone had no working publish backstop on either transport:

- `checkAndNotify` gated its sync on `due.isNotEmpty`. Logging every meal
  promptly means nothing is ever due, so the ~15-min periodic task never
  published. It was a notification job that happened to sync, not a sync
  timer. It now syncs on every tick it is asked to, then computes due slots
  from the merged result, so a meal pulled from another device suppresses a
  nag instead of triggering one.
- `backgroundSyncPush` never called `initSyncDeviceId()`. A fresh isolate has
  its own static state, so `currentSyncDeviceId` fell back to the compile-time
  'phone' constant and published to `devices/phone/` rather than the persisted
  uuid -- which then makes the next foreground push see `unchanged` and skip
  writing the uuid directory entirely.

Separately, on the "Fetch from sync" path itself: `FirebaseSyncError` and
`FirebaseAuthError` are siblings of `GitHubSyncError` under `RemoteSyncError`,
not subclasses, so every per-request failure of the *primary* backend escaped
`pull_shared_log`'s catch tuple and raised a traceback out of a helper
documented to fail closed with a reason string. `ConfigError` needed the same
treatment by a different route: it subclasses `Exception` directly, so it is
outside `RemoteSyncError` too, and is now translated at `_client_for_run`.

The PAT is no longer mandatory either. Firebase is primary, but `_read_token`
ran before the client was ever built, so a Firebase-only machine could not
sync at all -- the commits that fixed this touched only the Dart side.

A stalled sync was invisible: the config gates returned silently and
`openFirebase` swallowed every error. `dart:developer`'s `log()` does not
reach logcat on a release build (verified on device), so logging alone would
not have surfaced it. `SyncHealth` records each tick's outcome and the log
screen shows a banner when publishing has actually stopped.

Also make the real-Tk probes bypass the window manager. A tiling WM retiles a
managed toplevel instead of honouring the requested geometry, so `winfo_height`
reported 0/1px and the layout and focus-ring assertions failed on noise rather
than on any regression -- 8 failures on the dev machine that only passed under
Xvfb. `overrideredirect` makes the measurement deterministic with or without a
WM; the probes still detect a genuine overflow at 1366x300.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RT8X32S9yDLW1Xf1PDq8z7
2026-08-10 14:35:37 +02:00
688bd7b1de Finish the three sync bugs: gate, SIGKILL flag, and silent catches
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
Auditing the whole fleet for the bugs found on the desktop turned up three
that were still live here.

1. `--virtual-time-budget=45000` was still in the provisioning args. The
   previous commit added the comment explaining why it must not be there but
   left the flag itself, so headless Chrome kept SIGKILLing itself (exit -9)
   and could still truncate a sync. Actually removed now; a provisioning run
   exits 0.

2. `backgroundSyncPush` gated on `settings.isConfigured` -- "has a GitHub
   token" -- so the WorkManager background push was skipped entirely on a
   device connected only to Firebase. Same defect as _autoSync's, one layer
   down. Either backend now counts.

3. Three swallowed exceptions, all in the sync path: the in-isolate config
   read, the background push itself, and `loadAccount()`. The last one is the
   worst -- it hid *why* provisioning failed, which is indistinguishable from
   "no account set" until it says so, and cost real debugging time this
   session. All log at warning/error with the stack trace and stay non-fatal,
   per the repo's no-silent-failures rule.

Verified after the change, not just compiled: provisioning exits 0 and the
desktop (c891b813) still holds 359 food_log records, identical to the PC
daemon's, 0 missing.

522 tests, analyzer and pre-commit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
2026-08-09 22:28:18 +02:00
55fade1e15 Let the desktop provision its sync account from the wrapper
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
Same change todo just took, for the same reason: app/'s desktop target is a
Flutter web build in a Chrome --app window, so FlutterSecureStorage resolves
to the web backend (AES-encrypted localStorage, keyed inside the browser
profile). Nothing outside the page can write that store — `secret-tool`
targets libsecret, which only a Linux embedder build would read, and a Dart
CLI cannot even import flutter_secure_storage, since it is a Flutter plugin
that fails to resolve on the VM. Both verified rather than assumed.

The wrapper is the one process that can read the filesystem and serve the
page, which is exactly why it already holds the GitHub token instead of the
browser (see github_proxy.dart). So the sync account now arrives the same
way: loadAccount() falls back to GET /sync-account when the store is empty
and persists the result, making the route a one-shot provisioning step
rather than a standing dependency.

It reads ~/.config/crdt-sync/{firebase.json,password} — the same pair
`diet_guard sync` already uses — so the machine keeps one source of truth.

Off unless CRDT_SYNC_SERVE_ACCOUNT is set, and 404 (not 403) when disabled so
a probe cannot tell the route exists. On Android there is no wrapper: the
request simply fails and loadAccount() returns null exactly as before.

clearAccount() writes an opt-out marker the fallback checks, so Disconnect
cannot be silently undone by the next launch re-adopting the account.

518 tests, analyzer clean, pre-commit clean. Pinned to crdt_sync_dart-v0.8.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
2026-08-09 21:41:08 +02:00
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