diet-guard/app
Krzysztof kuhy Rudnicki 17a2c62f26 Automate CLAUDE.md's structural rules, then cut it to 189 lines
CLAUDE.md was 1101 lines and loaded into every session in this repo. Three
passes, in order: automate what a gate can check, rewrite what is left as
instructions rather than rationale, extract what still does not fit.

Automated. Five prohibitions were enforced by nothing but a paragraph asking a
future reader not to do something; they are now repo_invariants_test.dart:

  - no app/linux/ (a GTK embedder target manages ~20fps at 4K)
  - dart:io only in *_io.dart seams and lib/desktop/ -- this one is why the
    test exists at all, since dart:io does not fail a web *compile*. It throws
    at runtime, so a bad import is a blank white window that the widget tests
    happily pass through.
  - no meal builder / repeat-last-meal / reward prompt
  - no image_picker, no CAMERA permission
  - wrapper port 8732 and the diet-guard-desktop Chrome profile, both pinned
    because IndexedDB is keyed by origin and moving either hides the whole
    local food log

Each guard was canary-tested by breaking the invariant and confirming the
suite goes red -- a guard that cannot fail is decoration.

Rules whose behaviour was already covered (budget epoch seeding, sync merge,
food bank) shrink to a pointer at the test that owns them.

Where a rule's entire value was its rationale ("the removal is the feature"),
the why moved into the name and reason string of the gate that now enforces
it, so it lives somewhere executable instead of somewhere quotable.

Extracted. The vendored flutter/flutter rules were 789 of the 1101 lines --
72% of the file, none of it about this repo. Split into five docs/ topic files
under the cap and linked. Verified content-complete: 664 non-blank lines in,
664 out, zero missing.

Python 692 tests / 100% coverage unchanged; Flutter 548 -> 555 (+7 new).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK3HCZrM75sRHL1gL8usve
2026-08-15 16:39:57 +02:00
..
.agents/skills Fix pre-existing CI failures (deps, lint, whitespace) 2026-07-10 23:55:56 +02:00
android Build and publish a signed APK on every push to main 2026-08-04 20:55:14 +02:00
assets/icon app: warm-tint the shared palette, fix Snack chip + on-fill contrast 2026-07-21 21:16:26 +02:00
bin Finish the three sync bugs: gate, SIGKILL flag, and silent catches 2026-08-09 22:28:18 +02:00
desktop app: package the desktop app as the web build, and delete the Linux embedder 2026-07-20 16:32:24 +02:00
lib app: use the shared github_device_auth package 2026-08-15 16:15:24 +02:00
test Automate CLAUDE.md's structural rules, then cut it to 189 lines 2026-08-15 16:39:57 +02:00
web app: run the companion app's desktop target as a web build 2026-07-20 16:31:32 +02:00
.gitignore Add Flutter companion app skeleton with local meal logging 2026-06-22 18:22:42 +02:00
.metadata app: run the companion app's desktop target as a web build 2026-07-20 16:31:32 +02:00
analysis_options.yaml Persist the CI-mirror's analyzer excludes and dependency upgrades 2026-08-14 16:13:46 +02:00
install_arch.sh app: package the desktop app as the web build, and delete the Linux embedder 2026-07-20 16:32:24 +02:00
pubspec.lock app: use the shared github_device_auth package 2026-08-15 16:15:24 +02:00
pubspec.yaml Automate CLAUDE.md's structural rules, then cut it to 189 lines 2026-08-15 16:39:57 +02:00
README.md Gate the companion app in the push hook, and document it properly 2026-07-20 17:01:40 +02:00
run.sh app: package the desktop app as the web build, and delete the Linux embedder 2026-07-20 16:32:24 +02:00
skills-lock.json Add Flutter/Dart agent skills and AI rules for Claude Code 2026-06-22 22:53:06 +02:00

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/web once 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 proxies api.github.com plus 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: imagePath was 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.