mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 18:13:55 +01:00
3d82ddefd4
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d2e62bf210 |
Read the meal schedule from a forward-only history (Dart)
The phone/desktop half of the previous commit, so both devices now derive their slots the same way. Still no edit surface, so `MealScheduleService` returns the default and the derived hours are unchanged at 08/12/16/20. `slot.dart` takes the schedule as a required argument, mirroring `_slots.py`, and `slot_test.dart` gains the same every-hour x several-schedules sweep the Python suite runs. That sweep is the contract: the two must agree on which checkpoint a meal satisfies for every cell, not merely on the endpoints. Also fixes a real bug this feature would otherwise have shipped on top of. `syncToSlots` used the slot hour as the notification id but only cancelled ids in the *current* schedule, so changing 08/12/16/20 -> 08/11/14/17/20 would leave ids 12 and 16 posted with nothing that could ever cancel them: permanent reminders for checkpoints that no longer exist. It now sweeps the whole 0..23 id space. The regression test was confirmed to fail against the old loop (4 cancels vs 24) rather than passing vacuously. MealScheduleService.current degrades to the default when the singleton is absent, so `due_slot_check` deliberately does not init() it -- that would reach path_provider from widget tests and from the WorkManager isolate before main() runs. main.dart owns the real init, and background_sync_service.dart does its own, since a fresh isolate has its own static state and would otherwise nag against the default four meals. 609 Dart tests, 761 Python, both suites green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM |
|||
| cbbebee79b |
Pin meal pills to one row; drop the "Logged" restatement
Three independent cleanups ahead of the configurable meal schedule.
Pills: the row was a `Wrap`, so pills dropped onto a second line as soon as
they outgrew the width -- which they do even at four. Measured: a logged chip
is ~124px at Material defaults, so four need 522px against ~330px of phone
width. Now a `FittedBox(scaleDown)` over a `Row`, which scales the row down
uniformly, plus compact chip density so there is less to scale away.
A `Row` of `Expanded` children was the obvious alternative and is wrong: it
imposes a tight width, and a Chip narrower than avatar+padding+label clips its
label rather than shrinking -- measured at 10px per label, about one character,
while throwing no overflow error. The new regression test therefore asserts
both a shared baseline and a legible label width; it was confirmed to fail on
the old `Wrap` ("chip wrapped to a second line at 320.0") rather than passing
vacuously.
Card: the `Logged "<meal>".` line restated the description the user had just
typed. Removed, along with `TodayProgress.justLogged`, which nothing else read.
The kcal position, macros and streak stay.
refactor_claude_todo.md: the 250-line file-splitting refactor it tracked is
done -- `scripts/check_file_length.sh --all` exits 0 and an independent sweep
finds nothing over the cap. DESIGN_AUDIT_TODO.md is a separate, still-open
effort and stays.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
|
|||
| 3c67cf8c9c |
Split log_meal_screen into mixins and widget helpers
Twelfth batch of the 250-line sweep (over-cap count 5 -> 4). log_meal_screen.dart 431 -> 250 Widget/behaviour extraction only, no route changes: the four app-bar buttons push exactly the MaterialPageRoutes they did before. log_meal_sync_mixin.dart background sync + app lifecycle observation log_meal_nav_mixin.dart the four app-bar destinations log_meal_actions.dart the app-bar IconButton list log_meal_progress.dart buildTodayProgress + parseMacroField (both pure) The sync and nav halves are mixins rather than helper classes because the behaviour is inseparable from the State it drives -- it observes the widget lifecycle, calls setState, and reads `mounted`. The screen's fields stay declared on the State; `syncHttpClient` is an abstract getter because `widget` is typed `T` inside a mixin, so `widget.httpClient` does not resolve there. Names crossing the new file boundaries became public with doc comments, as `public_member_api_docs` requires. The macro-filling block moved onto `MacroControllers.fillFrom`, next to the existing `clear()`, which is where it belonged anyway. Verified: flutter test 555 (unchanged), flutter analyze clean, pre-commit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC |
|||
| 7cb09a5b85 |
Split food_bank_screen; share the filter widgets with history
food_bank_screen.dart (712) carried a near-clone of the filter sheet batch 2
had just factored out of history: the same name-search field, the same slider
captions, and four more copies of the same min/max macro range block. Splitting
it in isolation would have baked that duplication in permanently, so the shared
widgets moved to a neutral `lib/widgets/filters/` first and both screens now
import them.
food_bank_screen.dart 712 -> 238
lib/widgets/food_bank/
fb_filter.dart 156 FbFilter, FbSortField, applyFbFilter
fb_filter_sheet.dart 244 the sheet itself
fb_add_entry_dialog.dart 103 the manual-entry dialog
lib/widgets/history/history_filter_fields.dart
-> lib/widgets/filters/filter_fields.dart
FilterRangeRow gained `showEndpointLabels`, defaulting to true. History showed
endpoint captions and a selected-range readout; the food-bank sheet never did,
and switching them on there would have been a UI change smuggled in under a
refactor. The food-bank call sites pass false, so both screens render exactly
what they rendered before.
FbSortField and HistorySortField stay separate on purpose -- they are not the
same enum (`name` vs `date`/`description`), so the sort dropdown is not shared.
The test file mirrors the source split, three files under test/screens/
food_bank/ plus fb_test_support.dart for the setUp/tearDown/settle prologue,
same shape as batch 2's history_test_support.dart.
Verified: 555 flutter tests (unchanged count -- no test lost, and food bank's
own RangeSlider/sort-toggle tests still pass against the shared widget),
flutter analyze clean over lib/ and test/, live ~/.local/share/diet_guard
byte-identical. Over-cap files: 40 -> 38.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
|
|||
| 895a87cf86 |
Split history_screen and its test into widget siblings
history_screen.dart (1052) and history_screen_test.dart (1017) were a third of
the remaining over-cap work. Widget extraction only -- no route changes, since
routes are user-visible and repo_invariants_test.dart pins the navigation
surface.
history_screen.dart 1052 -> 211
lib/widgets/history/
history_filter.dart 102 filter criteria + sort enum (pure data)
history_grouping.dart 206 HistoryItem tree, apply/group/format
history_list.dart 194 GroupedList, DayHeaderTile, EntryTile
history_filter_sheet.dart 242 the sheet itself
history_filter_fields.dart 168 search field, slider labels, range row
history_filter_sort.dart 140 source chips + sort controls
Names that now cross a file boundary became public (HistoryItem, DayHeader,
EntryRow, buildGroupedItems, formatDay, dateRangeLabel, the tile widgets), each
with the doc comment public_member_api_docs requires -- 49 of them, written
rather than suppressed.
The four macro range sliders (kcal/protein/carbs/fat) were four near-identical
30-line blocks differing only in label, key, unit and which HistoryFilter pair
they read/write. They are now one FilterRangeRow taking min/max plus an
onChanged, which is most of how the sheet got under the cap.
The test file mirrors the source split one-for-one, six files under
test/screens/history/. Its setUp/tearDown and the settle() helper (needed by
every part, because HistoryScreen loads via an untracked initState future) moved
to history_test_support.dart rather than being copy-pasted six times.
Verified: 555 flutter tests (same count as before -- no test lost in the split),
flutter analyze clean over all of lib/ and test/, 701 python tests at 100%
branch coverage, `python -m diet_guard sync` -> 402 entries/exit 0, live
~/.local/share/diet_guard byte-identical. Over-cap files: 42 -> 40.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
|
|||
| 372e8a19f6 |
Remove the Snack chip from the phone's slot selector
The chip was the only control that logged a meal against no slot at all (`slot: null`). Removing it leaves the row with just the four hour chips, so every logged meal now satisfies a real slot. Storage keeps its null-slot support on purpose. Entries already on disk and arriving over sync carry no `slot` key, and the `slot != null` filters in log_storage_service.loggedSlotsToday / _state.logged_slots_today are what stop them from retroactively satisfying meal slots -- so both are documented as load-bearing rather than deleted as dead code. Also corrects the MCP tool's docstring, which claimed its slot argument "may be None early in the day"; it falls back to slot_for_log, which clamps to a real hour and never returns None. Adds the widget test the chip row never had -- being untested is why the chip read as unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPheLM86xUN9TGf34Ygtfz |
|||
| 458f10515b |
Make the phone publish on every tick, and fail closed on Firebase errors
Some checks failed
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 |
|||
| e07284d3e1 |
Show weekly and monthly average intake on both devices
The calendar already said which individual days went over budget, but not whether the week as a whole did. A run of small overages reads as four harmless yellow squares; averaged, it is the thing that actually moves the needle. Three rules are load-bearing, and are documented in CLAUDE.md because getting any of them wrong silently changes what "under budget" means: - The denominator is logged days, not elapsed days. An unlogged day is not a zero-kcal day, and averaging gaps in as zeros would make a badly-tracked week look like the best week of the year. - The yardstick is the mean of the per-day budgets over those same days, resolved through BudgetSchedule -- never today's budget, which would retroactively reclassify past weeks on every budget edit. - Today is excluded. A half-logged today drags the mean far enough to flip the band, so every period ends at yesterday. Band boundaries reuse the calendar's own OVER_BUDGET_YELLOW_CEILING by import rather than by copy, so a period and its days can never disagree about what "over" means; that constant loses its underscore to say so. The MCP get_averages tool returns the average and the band but drops the average budget, keeping the module's existing invariant that the budget number never leaves this machine. The CLI, the gate and the phone all show both, since it is hidden from the network, not from the user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjJqpcEsaV5WRhPXyxCThd |
|||
| 1702685afd |
Make the meal gate usable without a pointer on a 1366x768 screen
The Log Meal tab did not fit this laptop, and had not for some time. Measured
under a real 1366x768 X server: it required 866px inside a 739px content pane
with every field EMPTY, and 938px with a day of meals logged.
Because the content frame was place()-centred it was sheared at BOTH edges at
once -- the "Diet Gate" title and the slot header (which meal you are being
asked to log) off the top, the calorie headline and the whole dashboard off the
bottom -- with no scrollbar anywhere in the package and nothing to indicate
content was missing. This is a hard lock: overrideredirect, a global grab, VT
switching disabled. Unreachable content is unrecoverable.
Two independent causes, both needed fixing:
- Every font size was handed to Tk as *points*. Tk encodes the unit in the
SIGN of the size, and _gatelock_typography carried a comment asserting the
opposite ("Tk has no separate px/pt distinction"). At ~100 DPI that made
every string ~30% oversized. Negative sizes now; the docstring records why.
- There was no scroll container. build_layout now uses gatelock's
ScrollableSurface, which top-anchors, scrolls by keyboard, and scrolls to
follow focus.
Font sizes alone were not enough: they take the layout to 702px, still over the
571px pane at 1024x600. Both fixes are load-bearing.
The grams/items selector was mouse-only. tk.OptionMenu's Menubutton defaults to
takefocus=0, so ::tk::FocusOK rejects it and it never entered the tab ring --
verified by walking the real ring, which ran Entry -> Text -> Listbox -> Button
-> TNotebook and skipped it entirely. Switching units rescales the whole entry,
so a pointerless user could not correct "250 grams" to "3 items" inside a lock
they cannot leave without logging a meal. It is now two Radiobuttons, which are
in the ring by default and take both Space and Return -- and which post nothing,
unlike a menu, whose grab-stealing is why screen-locker bans OptionMenu on lock
surfaces outright.
Also: the focus ring was invisible (Tk's default is 1px BLACK on #211D1B) and
_build_desc actively inverted it by setting highlightbackground, the UNfocused
ring, so the box outlined while idle and went black the moment it took focus.
Enter now activates buttons (X11 Tk binds only <space>). Text widgets no longer
trap Tab. notebook.enable_traversal() is called, so Ctrl+Tab and Ctrl+PageDown
work. wraplength drops 900 -> 640, the line-length cap, which also stops a
label forcing a 900px-wide layout onto a 1024px screen.
The Flutter app had two controls with no keyboard route at all. Its desktop
target is the web build in a Chrome --app window, so a touch gesture is not a
fallback there, it is the only interaction:
- Deleting a history entry was bound solely to onLongPress. Long-press has no
keyboard equivalent whatsoever. It now has a focusable IconButton; the
long-press stays as a redundant pointer shortcut.
- Calendar day cells were a bare GestureDetector, so navigating to
date-filtered history was pointer-only and invisible to a screen reader.
Now Semantics(button: true) over an InkWell.
Plus: the filter sheet built a TextEditingController inside build() with the
caret forced to end-of-text, so every keystroke discarded it and silently undid
mid-string Home/End/arrow editing. And nothing capped width, so on the 1366px
desktop window prose ran ~180 characters per line (twice the readable limit)
and a four-digit kcal field spanned ~1334px. AppWidth adds those caps.
Tests: the render check is a failing test, not a script -- ruff runs with
--unsafe-fixes, whose T201 fix DELETES print statements, which would silently
turn a diagnostic into a no-op. test_gate_layout_fits.py was verified red on
the pre-fix code ("content 866 in viewport 768 -> NO SCROLL VIEWPORT ->
UNREACHABLE CONTENT") and green after. The Flutter suite gains its first
keyboard-reachability assertions; it previously had ~109 tester.tap calls and
zero checks that anything was focusable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| f7f06aee53 |
Remove unused features; fix reminder false alarms; share all state
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> |
|||
| c935a202cc |
Make AppStatusColors lookup null-safe; fix tests that never had it
Theme.of(context).extension<AppStatusColors>()! crashed in any context that doesn't build its theme from buildAppTheme() -- which is most widget tests, since they wrap in a bare MaterialApp(home: ...) rather than the real app theme. That crash was latent since AppStatusColors was introduced but only became reachable once main.dart actually started using buildAppTheme() (previous commit), which is when 27-47 tests started failing. Fixed at both call sites (slot_selector_row.dart, day_status_calendar.dart) by falling back to AppStatusColors.dark instead of force-unwrapping -- production code should never trust every context to fully replicate the real theme. Also fixed day_status_calendar_test.dart, which asserted against stale hardcoded Colors.* values from before the theme rewrite; it now wraps in the real buildAppTheme() and asserts against the actual tokens, so the assertions mean something again. Verified: flutter analyze clean, 436/436 tests pass (up from 389/47). |
|||
| 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. |
|||
| 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). |
|||
| 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> |
|||
| 957769ff0e |
Add budget-adherence calendar, streaks, and freely-editable synced budget
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 |
|||
| 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 |
|||
| 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 |
|||
| 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 |