Two Dart files whose contents were already cleanly separable.
wrapper_server.dart (254 -> 228): contentTypeFor is a pure function of the
path with no server state, so it moves to content_types.dart as a plain
library. WrapperServer.contentTypeFor stays as a static forwarder because the
test reaches for it by that name.
sync_merge.dart (475 -> 190): three near-independent blocks -- daylog, banks,
budget -- split along the seams the tests already use. The banks and budget
adapters move to siblings and are re-exported, so sync_service.dart and both
test files import exactly what they did before and needed no edits.
Flutter 555 tests unchanged; analyze clean; gate 52 -> 49.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK3HCZrM75sRHL1gL8usve
Same change todo just took: the route read its own configuration from
Platform.environment and $HOME, so only the "disabled" branch was reachable
from a test. Both are now constructor parameters defaulting to the
environment -- production behaviour unchanged -- and the four outcomes are
covered: 404 disabled, 200 with the account, 404 when ~/.config/crdt-sync/ is
incomplete, 404 when firebase.json has no usable email.
522 tests, analyzer clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
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
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>
A browser cannot touch the filesystem, so the desktop app needs a second
half. This local process serves the built web assets, owns an on-disk copy of
every document and photo blob the app stores, and performs the GitHub I/O the
page cannot.
Both GitHub responsibilities are load-bearing, not convenience:
- the device-flow endpoints (github.com/login/device/*) send no CORS headers,
so a page cannot call them at all;
- proxying api.github.com server-side is what keeps the token out of the
browser -- the page is told `{"status":"ok"}`, never the token itself.
The wrapper falls back to the PC gate's existing
`~/.config/diet_guard/sync_token`, so this machine needs no second setup.
Details learned the hard way in the two apps migrated before this one, kept
here deliberately: loopback-only binding (the endpoints overwrite files in
$HOME with no auth), `application/wasm` for .wasm or CanvasKit renders
nothing at all, a fixed port and Chrome profile because IndexedDB is keyed by
origin, and treating a browser exit inside ~5s as a handoff to an existing
window rather than a reason to stop serving. Mirror names are rejected unless
they are plain filenames -- a `..` segment would turn a mirror write into an
arbitrary-file overwrite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>