Skip to content

Feat/budgets - #752

Merged
sadespresso merged 14 commits into
internalfrom
feat/budgets
Aug 3, 2026
Merged

Feat/budgets#752
sadespresso merged 14 commits into
internalfrom
feat/budgets

Conversation

@sadespresso

Copy link
Copy Markdown
Collaborator

No description provided.

sadespresso and others added 14 commits July 19, 2026 15:45
- zh_CN: 中位数字消费 -> 中位数消费; the stray 字 read as "median digit"
- zh_CN/zh_TW: add missing measure word to upcomingCharges
  ({count} 笔/筆预定扣款), matching every other count string
- en: deleteCloudBackupConfirmation said "This action is not
  irreversible!" — a double negative telling users a destructive
  backup deletion could be undone

Claude-Session: https://claude.ai/code/session_01JUAzk892137fKesAsZbGnN
Routine bump, along with the transitive test/meta/matcher/vector_math
versions pub resolved with it.

Claude-Session: https://claude.ai/code/session_014o8coDD67GU2x4P5nxSrFZ
Pending transactions were excluded from budget spend outright. That makes
a budget read healthy right up until a scheduled payment clears, at which
point it lurches. Rent dated the 28th is money the period is already
committed to; leaving it out means the number answers "what has cleared"
while the user is asking "how much room is left".

So `spent` now includes pending, and everything derived from it -- ratio,
status, insights, alerts -- is measured on the committed total. The
pending slice stays recoverable as `pendingSpent`, because counting it is
not the same as pretending it has already gone: every bar fills solid to
`confirmedRatio` and continues to `ratio` as a lighter ghost.

`projectedRatio` deliberately does not extrapolate the pending part. A
scheduled lump sum is already dated, so running it through the run rate
would count it once for being committed and again for every day left in
the period -- rent on the 28th would project a 4.5x overshoot on the 2nd
with nothing actually spent. Confirmed spend is extrapolated as before,
the pending remainder is added at face value.

BulletChart is rewritten alongside it. The track had 10% headroom past
whichever of value/target was larger, so a bar sat at ~76% of its width
while the label beside it read 84%, and the same budget drew differently
depending on how far over it was. It now spans exactly 0..target: an
overrun fills the track and recolours instead of rescaling it. The old
qualitative band went with it -- it marked the target, which is now the
end of the track -- and the target tick became a pace tick, neutral
rather than `primary`, since a green accent colour made it the same hue
as a healthy bar.

payloadVersion goes to 3: `spent`/`ratio` mean something new, and a v2
extension would draw the larger figure as though all of it had cleared.
Both extensions default `confirmedRatio` to `ratio` when decoding, so the
degraded case is one solid bar rather than an all-ghost one. Glance has
no alpha modifier, so the Android ghost is three pre-multiplied colour
resources rather than a modifier on the status colour.

Claude-Session: https://claude.ai/code/session_014o8coDD67GU2x4P5nxSrFZ
The Android budget widgets built a bare `Intent(context, MainActivity)`.
That carries no action and no data, so every tap cold-opened the home
tab -- a widget showing "Groceries, 112%" dropped you somewhere with no
budget on it, and the roll-up sent you to the same place as the pinned
one. The quick-entry widgets already deep-link through the `flow-mn`
scheme the manifest accepts, so this just uses the same road.

The intent is pinned to our own package. `flow-mn` is a custom scheme,
which any installed app may register for, so without setPackage a widget
tap could raise a chooser or open a Flow-lookalike.

On the app side, the URI handler is an allowlist of three paths, not a
catch-all forward. Anything on the device can claim a custom scheme, so
forwarding an arbitrary path from one into the router would be an open
redirect into any route in the app -- including the destructive ones.
Unmatched URIs are logged and dropped. `/budgets/:id` still routes
through the existing redirect, so an id that no longer resolves lands on
the list instead of a dead page.

The iOS roll-up moves from `/budgets` to `/stats/budgets` for the same
reason the Android one points there: that widget is the overview in
miniature, so a tap should expand what it shows rather than drop you on
the adjacent list.

The Android chrome changes with it. The widgets drew a rounded card
inside an 8dp inset inside the launcher's own rounded container, which
reads as a border around a screenshot rather than as the widget itself.
One `appWidgetBackground()` surface now *is* the widget, at the system
radius, so the corners match every other widget on the home screen
instead of a hard-coded 16dp that only agrees with some launchers.

Claude-Session: https://claude.ai/code/session_014o8coDD67GU2x4P5nxSrFZ
It sat as a ListTile above the budget cards, so the one action on the
page looked like the first item in the list it creates into.

Claude-Session: https://claude.ai/code/session_014o8coDD67GU2x4P5nxSrFZ
zh_CN.json was contributed against develop, which has no budgets, so
merging this branch left it 66 keys short of en.json and the integrity
test red.

Machine-translated, like every other locale here, and worth a native pass
before release. Inserted in en.json's key order rather than appended:
json_integrity_test compares positionally, so scripts/translate_missing
alone would not have satisfied it.

Claude-Session: https://claude.ai/code/session_014o8coDD67GU2x4P5nxSrFZ
@sadespresso
sadespresso merged commit 414b818 into internal Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants