Budgets - #748
Merged
Merged
Conversation
Debug builds pinned objectbox-android-objectbrowser 5.1.0 and excluded objectbox-android, while the Dart objectbox package is 5.3.2 and requires native >= 5.3.2. Every debug build therefore died in ObjectBox.initialize(): Unsupported operation: ObjectBox platform-specific database library not compatible: is 5.1.0, expected 5.3.2 or newer Removed rather than bumped, because nothing could ever have used it: ObjectBox Admin only starts if the app constructs Admin(store), and there is no such call anywhere in lib/. The gradle line was the artifact's only mention in the repo. Dropping the exclude lets objectbox_flutter_libs supply objectbox-android 5.4.2 for debug, exactly as it already did for release -- which is why release builds were never affected. Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
Budget.range is now an anchor that is never rewritten. currentPeriod()
derives the live period by paging the anchor to "now" with moment_dart's
PageableRange, so renewDueBudgets() -- which overwrote the period that had
just ended -- is gone, along with its test.
Every past period is therefore just currentPeriod(budget, asOf: earlier),
and the transactions behind it were never touched. That is what the new
computeHistory()/historyPeriods() are built on. History is clamped to
Budget.createdDate: spend in a watched category predates the budget, and
showing it as that budget's own past performance invents a history the
user never set.
A budget anchored to a future period stays there for the live period --
paging backward would silently start it early. An explicit asOf still
pages back, which is what history needs.
computeAllProgressAsync()/computeHistoryAsync() move the transaction scan
to Isolate.run, attaching to the existing store via Store.fromReference.
Only the scan moves; the entity graph, Intl, prefs and BuildContext all
stay on the main isolate.
Two things worth knowing:
- The spawn must stay a top-level function. Isolate.run's closure
captures its entire enclosing lexical scope, not just what it names,
so an inline version dragged in a store-bound List<Budget>, the send
threw, and the try/catch quietly degraded to a fallback that returns
identical numbers. Six correctness tests passed against it. Only a
test that calls the isolate directly caught it.
- The isolate catches per-spec failures individually, so "every spec
threw" arrives as a successful empty list. computeHistoryAsync treats
that as a failure so the fallback actually fires.
ObjectBox.attachIsolate throws when a store is already open instead of
silently no-opping, and detachIsolate only closes a handle it opened --
otherwise the documented pairing could close the app's live store.
Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
/budgets/:id now opens BudgetDetailPage; the form moves to /budgets/:id/edit. Opening a budget to check on it should not put a Delete button under your thumb. The page shows the live period's progress, the insight sentence, the category chips, a strip of the last 6 periods -- free, now that renewal no longer overwrites them -- and every transaction behind the number. progress is null both while the first compute is in flight and when it failed (fetch swallows its errors so the rest of the page still renders), so both degrade to a message rather than asserting non-null. Asserting would red-screen a budget with unparseable data on the very page meant to explain it. Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
A widget extension runs in its own process: it cannot reach ObjectBox, call Flutter localization, or format money. So BudgetWidgetSync computes everything here and hands over display-ready strings -- including the labels, resolved in the app's locale, because the extension has no access to Flow's translations. One JSON blob rather than flat keys per field: the widgets render a variable-length list (the pinned widget resolves an arbitrary id, and its configuration screen enumerates them all), which flat saveWidgetData keys cannot express without inventing an index convention on both platforms. It carries a version so an extension updated independently by the store falls back to its placeholder rather than mis-rendering fields it does not understand. Both amounts and percentages always ship. Whether a given widget shows amounts is a per-widget setting the user picks on the platform side, so the decision -- and the state -- belongs there. This is shoulder-surfing privacy, not security: the amounts sit in the shared container either way. A sync can fire before FlowLocalizations has loaded, since the transaction listener is wired ahead of runApp, and an unloaded lookup returns "". Publishing that would paint a blank where a label belongs, so empty labels are omitted and the widget falls back to its own default instead. HomeWidgets.syncAll() replaces four scattered WidgetSummarySync.sync() call sites, and watchForChanges() adds a Budget box listener: editing a budget touches no transaction, so nothing else would have triggered a sync. buildPayload() is split out from sync() so the contract two native implementations depend on can actually be tested -- a shape regression is otherwise invisible from Dart, surfacing only as a widget rendering "---" on someone's home screen. Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
Two widgets against the payload contract: a systemMedium roll-up (how many budgets need attention, plus the worst one) and a systemSmall pinned widget for one budget, or whichever needs attention most. The pinned widget uses AppIntentConfiguration so the budget is picked from a live EntityQuery over the payload rather than baked in at compile time, and "Any budget that needs attention" resolves to summary.worstId at render time. Both offer a Hide amounts toggle, which removes every monetary value while leaving name, status, period, percentage and days-left in place. Status is never carried by colour alone: iOS 18 tinted rendering flattens every hue to one, so the widgets render the status as a word. No pbxproj edit -- "Flow Widgets" is a PBXFileSystemSynchronizedRootGroup, so files in the folder join the target automatically. Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
Glance implementations of the same two widgets, against the same payload
contract: a 4x2 roll-up and a 2x2 pinned widget, each with a configuration
activity carrying the Hide amounts toggle, and a budget picker on the
pinned one whose first option resolves to summary.worstId at render time.
Status is never carried by colour alone -- themed icons flatten it -- so
the status ships as a word, like on iOS.
Two things found by actually placing the widgets on a device:
- The configuration activities have to be exported for the launcher to
start them, which means any app on the device can start them too, with
an id it chose. That got the caller the budget picker -- the user's
budget names, on screen, on demand -- and a Save would rewrite a real
widget's privacy setting. They now check the id belongs to one of our
own providers first.
- targetSdk 36 forces edge-to-edge, so Theme.DeviceDefault's action bar
drew over the un-inset ScrollView: the screen's own title was hidden
behind it and the first section header sat under its bottom edge, with
no way to scroll it into view. The activity draws its own title, so the
action bar is now off and the content insets for the system bars.
Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
Claude-Session: https://claude.ai/code/session_01J6iVLcwaKJ2xLf86pYRK3T
A /budgets/:id route reached with an id that no longer resolves — a deleted budget, or an id that drifted after a data reset — rendered the bare "page not found" error page. That id arrives from three surfaces that can outlive the budget: the in-app budget alert, home-screen widget deep links (flow-mn:///budgets/:id), and donated Siri shortcuts. Fall back to the budgets list so these entry points degrade gracefully rather than dead-ending. Applies to the edit route too. Claude-Session: https://claude.ai/code/session_01FCFsJYn8tQ7Liofe7XCqPR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Budgets
Brings the budgets feature to
internal. 7 commits, budgets-only surface area.What's in it
budget_spec.dart,computeAllProgressAsync/computeHistoryAsync), with a graceful fallback to the main isolate so a platform quirk degrades to jank, not an empty screen.budget_detail_page.dart) — opening a budget to check on it doesn't put a Delete button under your thumb.budget_widget_sync.dart) since the extensions can't reach ObjectBox or localize.budget_insight_row.dart,budget_history_strip.dart).test/unit/budget_*_test.dart).Notes
pubspec.locktransitive bump and an l10n key re-sort from a localflutter run) — those were discarded, not committed.https://claude.ai/code/session_01FCFsJYn8tQ7Liofe7XCqPR