Commit Graph

41 Commits

Author SHA1 Message Date
0e9acb9e18 Wire theme.dart into the app; replace remaining raw Colors.* literals
main.dart now builds MaterialApp's theme from buildAppTheme() instead of
ColorScheme.fromSeed(Colors.teal) -- theme.dart existed but was never
actually used until this commit. photo_viewer_screen.dart,
settings_screen.dart, and day_status_calendar.dart's raw Colors.*
literals (Colors.black, Colors.red, Colors.white38, Colors.grey.shade800,
...) now read from Theme.of(context) instead.

Also rewrites CLAUDE.md's Visual Design & Theming section, which
previously contradicted the unified-design-system skill (noise texture,
multi-layered shadows, ColorScheme.fromSeed -- all things that skill
explicitly forbids).

This was the other prerequisite the on-fill/warm-palette fixes actually
needed: theme.dart existing but unwired meant the app was still running
Flutter's default seeded theme.
2026-07-21 21:50:16 +02:00
26f95d42f5 Fix stale adaptive_icon_background left over from the warm-palette icon regen
pubspec.yaml's adaptive_icon_background is a separate literal from
python_pkg/app_icons' own style.BACKGROUND -- the icon regen updated
every generated PNG but missed this one, so the actual on-device home
screen icon (Android's adaptive icon, not the legacy mipmap) stayed on
the old cool charcoal. Caught on the physical device, not by file checks.

testsAndMisc's generator now syncs this automatically going forward.
2026-07-21 21:42:32 +02:00
2a772d624f app: warm-tint the shared palette, fix Snack chip + on-fill contrast
- theme.dart: warm unified-design-system palette, on-fill for
  onPrimary/onError, and inputDecorationTheme/dividerTheme (previously
  unset -> every TextField/Divider fell back to Material defaults).
- slot_selector_row.dart: the Snack ChoiceChip had no explicit
  backgroundColor/side/labelStyle like the hour chips, so it fell through
  to Flutter's own default -- a literal pure-white border.
- Regenerated launcher icon assets (warm charcoal + warm gold accent) via
  python_pkg/app_icons.
- Bump gatelock pin to the released v0.1.1 carrying this same palette.

Verified: flutter analyze clean, diet_guard pytest 443/443 @ 100% coverage,
phone-installed APK pixel-checked (Snack chip border, on-fill contrast).
2026-07-21 21:16:26 +02:00
3bb21c00d6 Add a design-rules audit against the new safe-design-rules checklist
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
Report-only findings for a future cleanup pass; no code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:25:20 +02:00
d631f8a141 Add change explanation: desktop-web-build
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
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:27:42 +02:00
50f311b6b8 Gate the companion app in the push hook, and document it properly
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
`ci_mirror.sh` -- the pre-push gate -- ran only the Python side: a clean-venv
pytest and `pre-commit --all-files`. Nothing ran the app's suite, and
`.pre-commit-config.yaml` has no Dart hooks at all, so a red `app/` could be
pushed with a fully green gate. It now runs `flutter analyze` and
`flutter test` too, and *fails* rather than skips when `app/` exists but
flutter is missing: a gate that quietly passes without its toolchain is the
same as no gate. Verified by breaking a Dart file on purpose and watching the
push gate refuse it.

app/README.md was still the stock "A new Flutter project" template. It now
says what the two targets are, why there is no `linux/`, what the wrapper
owns, why the port and Chrome profile are fixed, and which behaviours the
desktop genuinely gives up.

Drops MIGRATION_PROMPT.md, whose content now lives in CLAUDE.md and the
migration commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:01:40 +02:00
2231850bfc app: test the IndexedDB stores instead of shipping them unverified
The IndexedDB stores are the desktop device's primary copy of the food log and
the *only* copy of its photos -- photos never sync -- and their wrapper-mirror
fallback is what makes a wiped Chrome profile recoverable. All of that was
invisible to the suite: the files import `idb_browser.dart`, which cannot be
imported outside a web compile, so they were simply absent from the VM test
run rather than covered.

Splitting the store classes from the browser-factory `open…()` functions lets
them run against `idb_shim`'s in-memory backend on the plain VM, which is what
the flutter-desktop-to-chrome skill recommends the shim for. Twelve tests now
pin the round-trip, the mirror write, the recover-from-mirror path, and the
no-wrapper-running case (a plain browser tab, where the app must still work).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:56:15 +02:00
c12faa3920 app: package the desktop app as the web build, and delete the Linux embedder
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
Completes the move: `linux/` is gone, replaced by the web build served by the
wrapper in a Chrome `--app` window. The icons and the .desktop installer move
out of `linux/` first -- they are not Flutter scaffolding and are still
needed; the entry now points at the packaged launcher and matches the
`--class=diet_guard_app` the wrapper passes, so the taskbar shows the app's
icon rather than a browser's.

install_arch.sh carries over the three traps each of which produced an
install that looked fine until launched: `dart build cli` rather than
`dart compile exe` (which refuses any package whose dependency graph has
native build hooks), `options=('!strip')` (makepkg would otherwise discard
the AOT snapshot, leaving a binary that runs as the bare Dart VM printing a
usage message), and no browser in `depends` (this machine's policy uninstalls
`chromium` seconds later, failing dependency resolution).

Also adds the frame-cadence instrument the performance claim is measured
with, armed by `--dart-define=DIET_GUARD_FRAME_STATS=1` and read with
`String.fromEnvironment` -- `bool.fromEnvironment` only accepts the literal
`true`, so `=1` would evaluate to false and a whole run would silently
produce no data. It reports frame-to-frame cadence rather than build/raster
durations, since work that fits the budget says nothing about the frame rate
actually achieved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:32:24 +02:00
aeb9376391 app: add the desktop wrapper that serves the web build and fronts GitHub
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>
2026-07-20 16:31:47 +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
10364dc1ff app: one-tap repeat-last-meal FAB with optional temptation-bundling reward
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
Applies Atomic Habits' two-minute rule: a FAB on the home screen repeats
the most recent logged meal in a single tap, mirroring _onLogMeal's
existing post-write sync sequence. An optional device-local reward
(label + URL, configured in Settings) surfaces after a repeat and opens
externally via url_launcher.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:09:36 +02:00
f6abf802c9 app: replace the stock Flutter launcher icon, add a Linux desktop icon
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
Adopt the shared icon family (python_pkg/app_icons in testsAndMisc): a
charcoal #1B1D21 field carrying a teal shield-and-cutlery glyph, teal being
this app's own colorSchemeSeed. The fork is paired with a knife on purpose -
a lone three-tine fork reads as a trident at launcher size.

Android gains the adaptive-icon layers the project never had, including an
Android 13 monochrome layer.

Linux needs separate handling: Flutter's GTK runner never sets a window icon,
so my_application.cc now calls gtk_window_set_icon_name and
install_desktop_entry.sh populates the hicolor theme from the pre-rendered
PNGs in linux/icons (committed, so the installer needs no image tooling).
StartupWMClass is the APPLICATION_ID rather than the binary name - verified
with xprop against a running window, where WM_CLASS is the GApplication id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:00:37 +02:00
ce68dcd82c Add change explanation: calendar-streaks-budget-sync
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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dHac1iYQKTMZEtL7xDJvA
2026-07-13 21:45:11 +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
09cf5b726a app: use crdt_sync's shared GitHubClient; drop the local copy
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
crdt_sync v0.3.0 adds canAccessRepo(), so the settings 'Test connection'
button uses the shared client and the local lib/services/github_client.dart
(+ its test) is deleted. flutter analyze clean, 299 tests pass. On-device
foreground check deferred: the active night curfew disables the app (a
phone-policy thing, not a code issue); build + install -r succeeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
2026-07-11 23:54:03 +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
16ca5227f6 app: push immediately when a meal is logged
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
The phone only pushed its log on screen-init, app resume/pause, or manual
"Sync now" -- never on the log action itself -- so a meal could sit locally
for many minutes before reaching GitHub (and thus the PC gate). Fire the
existing best-effort _autoSync right after a meal is logged, from both the
quick-log path (_onLogMeal) and the meal-builder return (_onBuildMeal), so an
online device propagates a new meal within seconds.

_autoSync is already single-flight and swallows offline/transient failures, so
this is a fire-and-forget unawaited call with no new machinery. Offline logging
still waits for reconnect + a lifecycle event (a WorkManager backstop is a
future follow-up).

Verified on device (BL9000EEA0000102): logging a meal advanced the phone's
remote food_log.json commit within seconds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 18:55:06 +02:00
8a9c7713b9 Review fixes: specific sync exceptions, clearer names
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
Address code-review feedback on the sync-before-lock change:

- pull_shared_log now catches (SyncError, GitHubSyncError, OSError) instead
  of a blanket `except Exception`. crdt_sync wraps every requests transport
  error in GitHubSyncError, so those three are the whole realistic failure
  surface; the gate still fails closed on all of them, and a genuine bug now
  surfaces instead of being hidden as "sync unavailable". Drops the
  broad-exception pylint disable and the per-file BLE001 ignore in pyproject.
- Rename _cli_gate._is_due -> _should_lock: too easily confused with the pure
  local predicate gate_is_due it wraps. The new name says what it decides.
- Rename the list `satisfied` -> `satisfied_slots` in _reconcile_after_fetch;
  it's a list of slots, not a boolean.

No behaviour change; 100% coverage retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 18:33:43 +02:00
0976c84254 Add CI-mirror pre-push gate + requirements-sync check
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
Mirrors the CI-green safeguards rolled out repo-wide (see screen-locker):
- scripts/check_requirements_sync.py: pre-commit hook that fails if a
  package import isn't declared in requirements.txt. On rollout it caught
  a real latent gap — diet_guard/_mcp.py imports pydantic but it was only
  present transitively (via mcp); now declared explicitly.
- scripts/ci_mirror.sh: pre-push gate = requirements.txt-only venv
  (hash-gated) running `pre-commit run --all-files` + `pytest`, replacing
  the plain pytest-coverage pre-push hook.

Only the tooling files + the pydantic declaration are committed; unrelated
in-flight work in the tree is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:58:49 +02:00
be94b4e147 Sync before locking the gate + manual "Fetch from sync" button
The PC gate forced a re-log when a meal was already logged on the phone
but the 15-min sync had not yet propagated it. Two ways to close that gap:

- The gate now pulls the shared log right before it would lock -- and only
  when a lock is otherwise due, so the network cost is paid on the rare
  tick that matters. A meal already pushed by the phone then unlocks
  without re-entry. Fails closed: any sync error (no token, network, disk
  write-back) falls through to the local lock decision, never skips it.
- A "Fetch from sync" button on the lock screen runs the same pull on
  manual request: it auto-unlocks when the pull satisfies every pending
  slot, advances to the next meal on a partial pull, or reports no change.
  Disabled in --demo so the sandbox never triggers a real sync/push.
- Both paths share one fail-closed wrapper, _sync.pull_shared_log(), which
  also keeps the single broad-except in one place.
- _write_log is now atomic (temp file + Path.replace): the gate and the
  15-min sync timer can now both write the log, so a torn read must be
  impossible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 17:54:38 +02:00
e6812f7b4f Fix pre-existing CI failures (deps, lint, whitespace)
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
CI on main was already red before the crdt_sync migration. Make the
pre-commit --all-files run and the test job green:

- requirements.txt: add crdt-sync (now a runtime dep) and mcp so CI's
  `pip install -r requirements.txt` can import diet_guard._sync and
  collect test_mcp.py (both were ModuleNotFoundError in CI).
- .pre-commit-config.yaml: add mcp + pydantic to the pylint hook deps so
  it can resolve diet_guard._mcp's imports (was E0401).
- pyproject.toml: raise pylint min-similarity-lines to 8 — the shared
  nutrition-field keys in _foodbank/_mcp mirror one domain schema, not
  real duplicate code.
- scripts/setup_mcp.sh: split declare/assign for SCRIPT_DIR/REPO_DIR
  (shellcheck SC2155).
- codespell: fix "packge"/"unparseable" typos; trailing-whitespace
  auto-fixes across app/.agents/skills docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
2026-07-10 23:55:56 +02:00
59d1c29dd2 Migrate sync to shared crdt_sync library (Python + Dart)
Replace the bespoke GitHub-Gist sync with the shared crdt-sync /
crdt_sync_dart libraries so diet-guard uses the same CRDT transport as
the other apps.

- Python: delete _sync_github.py (+ its test); rewrite _sync.py,
  _cli_sync.py, _sync_merge.py against crdt-sync. Add the crdt-sync git
  dependency (pyproject + pylint deps) and S107 to the test ignore list.
- Dart: rewrite app sync_service.dart / sync_merge.dart against
  crdt_sync_dart (new git dep + crypto); update log_meal and settings
  screens and their tests.

Verification: pytest 347 passed at 100% coverage; flutter analyze clean;
flutter test 306 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
2026-07-10 23:49:43 +02:00
06999274d7 Add MCP server exposing diet status + a gated log_meal
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
Expose diet_guard's read surface and one gated write action as typed MCP
tools (diet_guard._mcp) so Claude Code and its subagents can query today's
intake and -- with explicit confirmation -- log a meal without shelling out
to the interactive gate CLI.

- _mcp.py: FastMCP stdio server. Read tools get_status, list_today,
  get_slots expose today's CONSUMED kcal/macros and the qualitative
  consumption band only -- never the raw daily budget, remaining number,
  body weight, or protein target (those would back out the deliberately
  hidden budget). Gated write log_meal previews unless confirm=True and
  degrades gracefully on write failure. Nutrition is grouped into a Macros
  model (kcal + optional protein/carbs/fat) to keep the tool under the
  arg-count limits with no lint suppression, mirroring the CLI's grouping.
  Logs to stderr; no secret (HMAC key / sync token) is ever returned.
- .mcp.json + scripts/setup_mcp.sh: registration via a dedicated
  ~/.venvs/diet-guard-mcp interpreter; mcp is an optional-dep, kept out of
  the systemd/CLI system-python path.
- 100% coverage of the new module; verified end-to-end over stdio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQGQj8Ne6TriBExv91nG8v
2026-07-10 21:36:46 +02:00
a121da211d Point sync at the consolidated utils/syncs monorepos
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
gatelock and diet-guard-sync were folded into shared monorepos:
gatelock -> github.com/kuhyx/utils (subdirectory), diet-guard-sync ->
github.com/kuhyx/syncs (subdirectory). Update the gatelock pip URL,
SYNC_REPO_NAME, the devices-dir path prefix (PC and phone), and add a
persisted-value migration so an already-configured phone picks up
'syncs' on next load.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 14:12:16 +02:00
c43e37b09d Compact LogMealScreen so it fits without scrolling on-screen keyboard
Merge the slot-status bar, slot-selector chips, and "Logging for
HH:00" caption into one selectable, status-colored SlotSelectorRow.
Add an opt-in compact mode to MacroInputRow (single row, abbreviated
labels), AutocompleteSuggestionList (top-3 + "N more" bottom sheet),
and PhotoAttachField (icon-only + badge thumbnail), used only by
LogMealScreen so MealBuilderScreen/EditEntryScreen keep their default
rendering. Verified on-device (BL-9000) that all fields stay visible
with the keyboard open.

Also fixes an unrelated time-bomb in history_screen_test.dart's date
range picker test, which hardcoded an expected "2026-06-01" label
assuming "today" was in June; the picker's displayed month and
selectable range depend on the real current date, so the assertion
now computes its expectation from DateTime.now() instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UorgLvWJ4huH55tmXoUAZ
2026-07-04 05:19:23 +02:00
4c6083b768 Add food bank, edit-entry, and app settings screens; history redesign
Work-in-progress feature set accumulated ahead of the log-meal compact
layout change: a food bank browser, a shared entry-edit screen, an
app-wide settings service, and a substantially reworked history screen
with filtering/sorting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UorgLvWJ4huH55tmXoUAZ
2026-07-04 05:18:32 +02:00
7d421c1d8b Add R8 keep rules and re-enable release minification
The previous commit disabled isMinifyEnabled/isShrinkResources as a crash
workaround (AGP 9 default minification stripped WorkDatabase_Impl's no-arg
constructor). This commit adds the proper fix:

- new android/app/proguard-rules.pro: keeps the no-arg constructor on all
  RoomDatabase subclasses (the gap the Room AAR's own consumer rule misses
  — it keeps the class name but not the reflectively-called constructor),
  plus WorkManager worker/InputMerger rules re-stated from the work-runtime
  AAR in case transitive consumer-rule propagation is incomplete through the
  Flutter plugin wrapper.
- build.gradle.kts: re-enables isMinifyEnabled=true + isShrinkResources=true
  with proguard-android-optimize.txt + proguard-rules.pro.

Verified on the real phone (BL9000): minified release APK launches without
crash, MainActivity foreground, food-bank suggestions and slot chips render
correctly. APK size 52.1 MB (vs 56.9 MB unminified).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:34:15 +02:00
adbfb20e9a Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4)
M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect
GitHub" button that runs the device-code flow; tapping with no client id
now opens a setup dialog (instructions + inline paste field) rather than
a buried inline hint. Bakes in the app's own OAuth App client id so fresh
installs work with zero manual config. Auto-syncs immediately after
connect. Verified end-to-end on the real phone: OAuth flow → token saved
→ PC's 48-entry log merged in (confirmed via food-bank vs manual source
labels in History).

M4 – Background meal-slot notifications: WorkManager periodic task (15 min
floor) checks for overdue slots and posts/cancels notifications via
flutter_local_notifications. New permissions: POST_NOTIFICATIONS,
WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
INTERNET (was missing — latent sync bug). "Disable battery optimization"
button in Settings. Verified on real phone: WorkManager registered, forced
run posted a real notification ("Meal not logged / You haven't logged your
16:00 meal yet."), isolated to background path (only caller is the
WorkManager dispatcher, not any foreground lifecycle hook).

AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources
to true for release even with no proguard config; R8 stripped
WorkDatabase_Impl's reflection-only constructor, crashing every launch
with NoSuchMethodException. Explicitly disabled both flags in
build.gradle.kts. Verified via dexdump (constructor present) and on-device
launch (no crash). Proper R8 keep rules are the long-term fix; tracked.

177 tests, flutter analyze clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00
0ddad00ab9 Add Flutter/Dart agent skills and AI rules for Claude Code
Installs the official flutter/skills and dart-lang/skills packs into
app/.agents/skills/ and appends Flutter's AI rules.md to CLAUDE.md, so
Claude Code has task-specific playbooks alongside the dart MCP server
(registered separately at user scope).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NqCvrbFnoNNmqCwZKntBK
2026-06-22 22:53:06 +02:00
95acf8a376 Fix setuptools flat-layout package discovery broken by app/
Adding the Flutter app/ directory made setuptools' auto-discovery see two
top-level packages and refuse to build, breaking install.sh's pip step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH2BHCKbDTiYJUMG3rb9nq
2026-06-22 22:51:02 +02:00
a82047502f Add Flutter half of cross-device sync (Milestone 3)
Ports github_client.dart and sync_settings.dart from ~/todo (PAT-paste
instead of OAuth device flow), and writes a new (non-CRDT) sync_merge.dart
and sync_service.dart matching diet_guard's Python _sync_merge.py/_sync.py
algorithm exactly. Adds a settings screen for the PAT plus manual "Sync
now", and wires lifecycle-triggered auto-sync (launch + resumed/paused)
into the main logging screen, silent on failure per plan decision 4.

Also adds Linux desktop platform scaffolding so this and future UI changes
can be visually verified without a connected phone.

Verified end-to-end against the real kuhyx/diet-guard-sync GitHub API on a
Linux desktop build: Test connection and Sync now both round-trip to GitHub
and surface real auth errors correctly via SnackBar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH2BHCKbDTiYJUMG3rb9nq
2026-06-22 22:42:27 +02:00
e5b80fd610 Add cross-device log sync (Python half of Milestone 3)
Pulls every other device's pushed log from GitHub-backed dumb storage,
merges it with the local log, and pushes this device's own merged copy
back -- the PC half of the diet-guard-app sync plan.

- _sync_merge.py: pure union-by-id merge, tombstone always wins, legacy
  (time, desc) dedup for pre-id entries. Commutative and idempotent.
- _sync_github.py: minimal GitHub Contents API client (list/get/put),
  distinguishing a 404 on an unused path from the repo itself being
  unreachable.
- _sync.py: orchestration -- pull, merge, re-sign every persisted entry
  regardless of origin, write, rebuild the food bank, push. Re-signing
  unconditionally is load-bearing: an unsigned phone-origin entry would
  otherwise be silently dropped on the very next read once a machine
  holds the shared HMAC key.
- _foodbank.rebuild_food_bank(): the "replay a full log into a fresh
  bank" entrypoint the Python side was missing (the Dart port already
  had its equivalent). Backs sync's bank-rebuild step.
- New diet-guard-sync.service/.timer (15-minute cadence, headless, a
  separate unit from the gate so a held lock can't stall sync) and a
  new install.sh step to install them.
- Created the private kuhyx/diet-guard-sync GitHub repo for storage.

Incidental to this feature: adding the `sync` subcommand pushed _cli.py
past the repo's 500-line cap, so `gate`'s CLI glue moved out alongside
sync's into _cli_gate.py/_cli_sync.py -- same split pattern already used
for the gate window logic itself, not a sync-specific design choice.

338 tests, 100% branch coverage. Verified importing and running cleanly
under /usr/bin/python (the production interpreter), not just the dev
venv -- the gap that caused the earlier 3-day outage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 19:36:27 +02:00
feef5984f8 Add photo attach, full-size viewer, and a minimal history screen
Milestone 2 of the diet-app-as-wise-balloon plan, plus feedback from
manually testing it on-device:

- PhotoAttachService wraps image_picker and copies the picked photo into
  <app documents>/images/<uuid>.<ext>, so the file survives after the
  picker's own (possibly cache-cleared) temp copy is gone. Phone-local
  only, per the sync plan: imagePath is never synced.
- PhotoAttachField is a shared attach/preview/remove widget, used
  identically by both the single-item and composite-meal logging
  screens, so logging a multi-item meal can now carry a photo too.
- PhotoViewerScreen gives a full-screen, pinch-to-zoom view of an
  attached photo -- the 64x64 inline thumbnail was too small to
  actually check the photo.
- HistoryScreen lists every logged entry across all days, newest first,
  with a thumbnail when one is attached. There was previously no way to
  confirm what got logged (or whether a photo actually attached) short
  of inspecting food_log.json directly.

Verified on a physical device (BL9000): built, installed, and the user
confirmed the photo-attach flow logs a real entry with a real photo,
visible afterward in the new history list. 88 Flutter tests passing,
`flutter analyze` clean against very_good_analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:57:58 +02:00
ee5a7660cb Add Flutter companion app skeleton with local meal logging
Milestone 1 of the diet-app-as-wise-balloon plan: a phone-native way to
log meals away from the PC, sharing the exact on-disk JSON shape
diet_guard already uses (same field names, no translation layer).

- lib/models/: 1:1 Dart mirrors of the Python dataclasses (Nutrition,
  FoodEntry, MealItem, FoodBankRecord, Slot), including the per-100g/
  amount-eaten portion scaling that matches _resolve.resolve_nutrition's
  semantics exactly.
- lib/services/log_storage_service.dart: plain-JSON persistence to
  food_log.json's exact shape (no sqflite -- the canonical format
  already is this JSON).
- lib/services/foodbank_service.dart: ports _foodbank.py's upsert/fuzzy
  search logic for autocomplete.
- lib/screens/: log_meal_screen.dart (single-item logging) and
  meal_builder_screen.dart (composite multi-item meals, logging full
  per-component macros via the new components field).

Verified end-to-end on a physical device (BL9000): built, installed,
logged a real meal through the UI. 77 Flutter tests passing, `flutter
analyze` clean against very_good_analysis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:22:42 +02:00
888c877048 Add sync-ready log schema: stable id, tombstone undo, composite components
Lays the Python-side groundwork for the phone companion app's eventual
log sync (Milestone 0 of the diet-app-as-wise-balloon plan):
- log_meal() stamps every entry with a UUID id for union-by-id merging
  across devices.
- undo_last_today() now tombstones (deleted: true, re-signed) instead of
  popping the entry, so a sync merge can't resurrect a stale copy of
  something already undone on another device.
- log_meal() accepts components, the full per-item macros for a
  composite meal, so a food bank rebuilt purely by replaying the log can
  recover every component's standalone nutrition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
2026-06-22 18:22:24 +02:00
8ed2082854 Document the durable-clone editable-install requirement
A one-off `pip install "diet_guard @ git+..."` from a scratch
directory was used during the original cutover; that's a non-editable
snapshot that a later git push silently never reaches. Document the
correct deployment convention (install.sh from a durable ~/diet-guard
clone) so this doesn't drift again.
2026-06-22 12:54:10 +02:00
843f5e0221 Extract diet_guard from testsAndMisc as a standalone repo
Rewrites python_pkg.diet_guard imports to diet_guard, vendors the
shared as_float coercion helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
scaffolds standalone lint/test config matching testsAndMisc's real
enforced bar (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage).
2026-06-22 12:18:39 +02:00
2de8f5d122 Fix non-hermetic diet_guard gate tests depending on a real X display
test_demo_opens_window and test_bare_gate_due_opens_window never mocked
wait_for_display(), so they silently relied on a real, reachable X
display being present. On a dev machine this is always true, so they
passed; on a true headless CI runner (no X server at all) each test
blocked for the real 60s timeout before failing, explaining the CI
run's 131s duration. Mocking it to True (matching the existing
test_gate_due_but_display_not_ready_defers pattern for the False case)
restores hermeticity with no production code change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7vbgtFfZmfxJtN5DdtJky
2026-06-22 08:12:02 +02:00
205b214d78 Migrate diet_guard to the shared gatelock backend
MealGate now composes gatelock.GateRoot + gatelock.LockWindow instead of
inheriting the deleted _GateWindow/_GateRoot, and its HMAC signing goes
through gatelock.log_integrity. This is the first of three migrations
(diet_guard -> screen-locker -> wake_alarm) extracting the lock-window
mechanics that diet_guard's own _GateWindow proved out into a shared,
reusable package. Window-mechanics tests moved with the code; diet_guard's
suite now only tests its own wiring (LockConfig choice, hook delegation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XCdT46zV8hESDvbgYMGDLt
2026-06-21 18:16:45 +02:00
84898c0272 feat: split oversized modules for 500-line limit, fix kasa coverage gap
Split diet_guard/_gatelock.py, wake_alarm/_alarm.py, and the
usage_report.py/_usage_report_parsing.py pair into focused
sub-modules so every Python file is <= 500 lines, satisfying
test_file_length.py. Install python-kasa into .venv (declared in
requirements but missing after the 3.13->3.14 venv upgrade),
fixing 8 failing smart_plug tests and restoring 100% coverage.

Also includes prior in-progress work from the working tree: the
wake_alarm Progress/View/Hardware field-grouping refactor,
brother_printer query module + tests, diet_guard foodbank/state/cli
updates, new shared coerce/logging_setup helpers, morning_routine
orchestrator tweaks, dwm window-manager config, gaming scripts, and
misc maintenance/digital-wellbeing script updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 07:19:37 +02:00
400f89b469 feat(diet_guard): add meal-logging screen-lock gate with trigger fix
Add the diet_guard package: a screen-locking meal-logging gate that fires
on 4-hour slots (08/12/16/20) and records calories/macros, persisting an
autocompleting food bank.

- Trigger fix: the systemd timer fires at session start (Persistent=true)
  before lightdm has written ~/.Xauthority, so the gate crashed with a
  TclError instead of locking the screen. Add wait_for_display() /
  _display_is_ready() in _gatelock.py and wire it into _cli._cmd_gate so the
  gate retries on the next tick instead of crashing; add
  Environment=XAUTHORITY=%h/.Xauthority to the service as belt-and-suspenders.
- Food-bank hardening: a transiently corrupt food_bank.json was warned about
  on every keystroke and then silently overwritten (data loss). _read_bank
  now quarantines it via _quarantine_corrupt_bank() (warn-once + timestamped
  backup) before starting fresh.
- Multi-item meals: new _meal.py (MealItem, meal_total, MEAL_SOURCE),
  remember_meal() + _upsert() in _foodbank.py, and a "+ Add item" control in
  the gate that logs both the individual items and the composite meal.
- Bundle resolve_nutrition's manual macros into a ManualMacros dataclass to
  stay within the argument-count limit.

diet_guard at 100% branch coverage; full pre-commit suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:32:39 +02:00