The periodic diet-guard-sync.timer paid a network round-trip every ~15 min to approximate the two moments the shared log actually changes on this device. Sync now fires on those events instead: * before a lock -- already present in _cli_gate._should_lock, which pulls only when a lock is otherwise due, so a phone-logged meal clears the lock and a not-due tick never touches the network; * after a meal is logged here -- new _sync_events.publish_after_log, wired into all three write paths (_cli_log, _cli_gate, _mcp). This is what stops the PHONE nagging for a slot already logged on the PC, the mirror image of the rule that keeps the phone's own tick unconditional. The gate's publish sits in cmd_gate AFTER MealGate.run() returns, never in the unlock handler: that runs on the Tk thread with the window still up, so a hanging network call would keep the user locked in behind a successful log. It also covers the "Synced from another device" unlock, which hooking log_meal would miss. Accepted trade: a PC that is on but neither logging nor locking no longer pulls, so an inbound phone edit lands at the next log or lock rather than within 15 min. Bounded by slot spacing -- an unlogged day raises a lock. install.sh now disables and removes the installed timer (guarded, since a fresh machine has no such unit and `disable` would abort under `set -euo pipefail`); the .service stays for manual one-off ticks. Every test that logs a meal would otherwise reach the network through the new call sites, so conftest patches publish_after_log at each of the three call sites -- not on _sync_events, so test_sync_events still exercises the real helper. Suite stays at ~8s, which is the actual leak detector. Two files had to be split to commit at all, since file-length passes filenames and both were already over the 250-line cap: _cli.py 466 -> 207, + _cli_args.py, _cli_init.py, _cli_log.py _mcp.py 370 -> 194, + _mcp_server.py (singleton + annotations), _mcp_read.py Their conftest/test patch targets moved in the same commit; a stale target redirects nothing and fails open. Verified: 701 python tests at 100% branch coverage, 555 flutter tests, `python -m diet_guard sync` -> 402 entries/exit 0, `gate --check` -> exit 0, all five MCP tools still register, live ~/.local/share/diet_guard byte-identical. Over-cap files: 44 -> 42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC |
||
|---|---|---|
| .. | ||
| .agents/skills | ||
| android | ||
| assets/icon | ||
| bin | ||
| desktop | ||
| lib | ||
| test | ||
| web | ||
| .gitignore | ||
| .metadata | ||
| analysis_options.yaml | ||
| install_arch.sh | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
| run.sh | ||
| skills-lock.json | ||
diet_guard_app
Companion app for diet_guard: log meals on the phone, and on the Linux
desktop, sharing one food log through GitHub-backed sync.
Two targets, and only two
| Target | What it is |
|---|---|
| Android | a normal Flutter app (flutter build apk) |
| Linux desktop | the Flutter web build, served by a local wrapper and shown in a Chrome --app window |
There is no linux/ directory. Flutter's GTK 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 (~/todo/docs/desktop-performance-findings.md). Running
flutter create --platforms linux here would silently restore the slow path.
Commands
flutter test # the app's suite
flutter analyze
bash run.sh # build the web bundle and launch the desktop app
bash install_arch.sh # build + install the pacman package and .desktop entry
flutter build apk --release # the phone build
adb install -r build/app/outputs/flutter-apk/app-release.apk
Never adb uninstall or pm clear this package: both wipe the on-device log.
The desktop wrapper
bin/diet_guard_desktop.dart is the other half of the desktop app, because a
browser cannot touch the filesystem. It:
- serves the built web assets from
build/web(or/opt/diet-guard-app/webonce packaged); - keeps an on-disk copy of every document and photo under
~/.local/share/diet-guard-desktop/, which is what makes a wiped Chrome profile recoverable -- for photos it is the only copy, since they never sync; - holds the GitHub token (
~/.config/diet_guard_app/sync_token, falling back to the PC gate's~/.config/diet_guard/sync_token) and proxiesapi.github.complus the device flow, whose endpoints send no CORS headers and so cannot be called from a page at all.
The port (8732) and the Chrome --user-data-dir must stay fixed.
IndexedDB is keyed by origin and lives inside that profile, so changing either
looks to the app like a device with no history.
Platform seams
dart:io does not fail a web compile; it becomes a stub that throws at
runtime, so the symptom of a missed seam is a blank white window rather than a
build error. Every platform edge is a conditional export -- document_store*,
blob_store*, background_tasks*, notification_backend*, token_vault*,
github_client_factory*, attached_image*. Branch on kIsWeb before any
Platform.is…, which itself throws on web.
What the desktop genuinely loses, by design rather than omission:
- Background reminders. A browser runs nothing once its window is closed,
so the due-slot check runs in-page every 5 minutes while the window is open.
On the PC the real backstop is
diet-guard-gate.timer, which locks the screen. - Camera capture. The browser picker is a file input.
- Photo sharing between devices -- unchanged:
imagePathwas always stripped before push.
Sync identity
Three devices push under diet-guard-sync/devices/: pc (the Python gate),
phone, and desktop. Each must keep its own id -- two devices sharing one
would overwrite each other's pushed log on every tick.