Commit Graph

2884 Commits

Author SHA1 Message Date
Juan José Mata
fdcd0c79e3 Upgrade Puma to 7.2.1 (#2428) 2026-06-21 16:09:17 +02:00
Blaž Dular
974f1d8e44 feat(mcp): add get/create/update tools for tags and categories (#2374)
* feat(mcp): add get/create/update tools for tags and categories

* test(mcp): add tests for tag and category assistant functions

* fix(mcp): harden category/tag assistant functions against bad tool input

* refactor(mcp): use UuidFormat.valid? instead of local UUID_PATTERN

* chore(mcp): remove account_filter component files that don't belong in this branch
2026-06-21 12:01:39 +02:00
ghost
875b58d9ca design-system(mobile): SureFontWeights tokens (named weight tiers) (#2419)
* feat(mobile): add SureFontWeights design tokens

Introduce named font-weight tokens so widgets reference a DS tier instead
of a raw FontWeight, mirroring the web design system's Tailwind weight
utilities (font-medium / font-semibold).

- design/tokens/sure.tokens.json: add font.weight.medium (500) and
  .semibold (600) as the canonical source.
- generate_sure_tokens.mjs: emit SureTokens.weightMedium / weightSemibold
  (with a font-weight resolver + validation), regenerate sure_tokens.dart.
- sure_tokens_test.dart: assert the generated weights match canonical.
- Migrate the dashboard-area money/hierarchy sites onto the tokens
  (net_worth_card, account_card, dashboard, recent_transactions,
  transactions_list): FontWeight.w500 -> SureTokens.weightMedium,
  FontWeight.w600 -> SureTokens.weightSemibold.

Weight-only; other screens adopt the tokens in their own polish PRs.
166 tests pass; flutter analyze: no new issues; token generator --check
is clean.

* build(tokens): regenerate web _generated.css for new font weights

Adding font.weight.medium/semibold to sure.tokens.json also flows through
the web token generator (bin/tokens.mjs), which emits --font-weight-medium
and --font-weight-semibold. Commit the regenerated _generated.css so the
tokens:check gate stays green (node bin/tokens.mjs + git diff --quiet).

* test(system): de-flake account edit via account-menu

AccountsTest#assert_account_created opened the account menu and clicked
Edit immediately after visiting the account page. That page issues a
Turbo morph refresh shortly after load (turbo_refreshes_with :morph
reacting to a family-stream broadcast), which can detach the menu node
mid-click — "Selenium::WebDriver::Error: Node with given id does not
belong to the document" — or wipe the just-opened edit form. Capybara
does not auto-retry that inspector error, so the test flaked
intermittently in CI.

Extract open_account_edit_dialog, which retries the menu interaction
until the edit form is present, rescuing the transient detach/stale
errors. Mirrors the existing PropertyTest#open_account_edit_dialog
precedent for the same morph race, but also tolerates the click itself
raising while the refresh is in flight.
2026-06-21 11:54:29 +02:00
ghost
9ea8261919 feat(mobile): migrate all hardcoded UI strings to gen_l10n ARB (Phase 1 + 2) (#2384)
* feat(mobile): add Flutter i18n foundation (gen_l10n Phase 1)

- Wire flutter_localizations + gen_l10n (pubspec.yaml + l10n.yaml)
- Bump intl ^0.18.1 → ^0.20.2 for Flutter 3.32 pin
- Add lib/l10n/app_en.arb with 9 seed keys (appTitle, 3 common*, 4 chatSuggestion*)
- Generate AppLocalizations via flutter gen-l10n
- Replace MaterialApp title: with onGenerateTitle + localizationsDelegates + supportedLocales
- Migrate suggested_questions.dart from const list to BuildContext function
- Update chat_conversation_screen.dart call site (one-line change at pre-designed seam)

* feat(mobile): migrate all hardcoded UI strings to gen_l10n ARB (Phase 2)

Extends the i18n foundation from Phase 1 to cover every remaining
hardcoded user-facing string across the mobile app.

Files changed (21 source files + ARB/generated):
- lib/l10n/app_en.arb: 226 keys (+~80 new keys covering all screens)
- lib/screens/dashboard_screen.dart
- lib/screens/calendar_screen.dart
- lib/screens/transactions_list_screen.dart
- lib/screens/backend_config_screen.dart
- lib/screens/recent_transactions_screen.dart
- lib/screens/biometric_lock_screen.dart
- lib/screens/chat_conversation_screen.dart
- lib/screens/chat_list_screen.dart
- lib/screens/log_viewer_screen.dart
- lib/screens/login_screen.dart
- lib/screens/main_navigation_screen.dart
- lib/screens/more_screen.dart
- lib/screens/settings_screen.dart
- lib/screens/sso_onboarding_screen.dart
- lib/screens/transaction_edit_screen.dart
- lib/screens/transaction_form_screen.dart
- lib/widgets/account_detail_header.dart
- lib/widgets/category_filter.dart
- lib/widgets/connectivity_banner.dart
- lib/widgets/currency_filter.dart
- lib/widgets/custom_proxy_headers_editor.dart

Notable patterns:
- ICU plural for connectivity pending-sync count
- Parameterised keys for greeting (firstName), update dialog
  (version), cash chip (amount), delete confirmation (name),
  show-N menu (count), proxy-headers count, multi-delete count
- AppLocalizations captured before async gaps; dialog builders
  re-obtain from their own BuildContext
- const removed only from Text/InputDecoration/SnackBar/Row
  widgets that directly reference localisation getters; sibling
  const widgets preserved

116/116 unit tests pass; flutter analyze --no-fatal-infos: 0 errors.

* feat(mobile): migrate the remaining secondary UI strings to gen_l10n ARB

Completes the i18n migration by covering the lower-traffic strings the
earlier passes left behind — secondary dialogs, snackbars, settings
rows, validators, helper texts, and tooltips across 11 files.

- settings_screen.dart: reset/delete-account dialogs, clear-data and
  proxy-header snackbars, list-tile titles/subtitles, theme-segment
  tooltips, biometric prompt reason, debug-logs semantics label
- transaction_form_screen.dart: amount validators, session/success/
  failure/generic-error snackbars, More/Less toggle, date/name/category
  fields + helper texts, create button
- transaction_edit_screen.dart: session/update snackbars, validators,
  category/merchant/tag fallbacks, synced-only notice
- login_screen.dart: API-key dialog, demo/sign-up TextSpan, MFA info +
  validator, divider, server-URL heading, API-key + backend tooltip
- chat_list_screen.dart: multi-delete plural, result snackbars, error
  heading, relative-time fallback
- chat_conversation_screen.dart: edit-title dialog, refresh tooltip,
  load-error heading
- connectivity_banner.dart: sign-in / sync-success / sync-failure /
  auth-failure snackbars (also resolves the const removals cleanly)
- main_navigation_screen.dart: enable-AI dialog + failure snackbar
- log_viewer_screen.dart: clear-logs confirmation dialog
- biometric_lock_screen.dart, transactions_list_screen.dart: snackbar
  and edit tooltip

Adds 98 ARB keys (now 324 total), reusing common* and existing
screen keys where text matched. Interpolations use String/int
placeholders; multi-delete uses an ICU plural. AppLocalizations is
captured before async gaps and re-obtained inside dialog builders;
const removed only where a localisation getter is introduced, with
const restored on still-const siblings.

116/116 unit tests pass; flutter analyze --no-fatal-infos: 0 errors
(6 pre-existing infos unchanged). No remaining hardcoded user-facing
strings (verified by grep; only a doc-comment example remains).

* fix(mobile): address i18n review feedback and prune dead ARB keys

Resolves the still-valid CodeRabbit/Codex review findings on the i18n
migration; the majority were already handled by earlier commits.

- pubspec: switch intl to `any` so flutter_localizations selects the
  SDK-pinned version (0.19.x on <3.32, 0.20.x on 3.32+). Pinning
  ^0.20.2 conflicted with the declared `flutter: '>=3.27.0'` floor and
  would break `flutter pub get` on pre-3.32 SDKs.
- sso_onboarding: localize "Signed in as {email}" / "Google account
  verified" / the link- and create-form notes; fix the first/last-name
  validators that wrongly returned the "Email is required" message.
- backend_config: localize _testConnection / _saveAndContinue /
  _validateUrl messages and the headers helper text (validator now
  takes AppLocalizations; async methods capture it before awaits).
- recent_transactions: localize "Unknown Account"; locale-aware
  timestamp.
- account_detail_header: localize the unavailable-details message via a
  render-time flag (avoids touching inherited widgets in the
  initState-driven load path); treat empty/whitespace securityName as
  missing for the holding-name fallback.
- dashboard: wrap syncing/refreshing snackbar text in Expanded to avoid
  overflow with long translations; use the distinct dashboardSyncError
  key on the exception path.
- chat_list: localize relative-time labels (plural keys) and use a
  locale-aware fallback date; calendar: locale-aware displayed dates and
  weekday letters (map-key formats left fixed; Sunday-anchored grid
  preserved).
- connectivity_banner: capture ScaffoldMessenger before the async gap
  (removes use_build_context_synchronously); log_viewer: const Duration.
- Prune 55 unused ARB keys (never-wired scaffolding + superseded
  duplicates). ARB now 298 keys, 0 unused.

116/116 tests pass; flutter analyze --no-fatal-infos: 0 errors, only 3
pre-existing infos in the untouched intro_screen_web.dart.

* fix(mobile): localize the log viewer empty-state string

The 'No logs yet' empty state was the last hardcoded user-facing string
(the logViewerEmpty key had been pruned before this string was migrated).
Re-add the key and route the empty state through AppLocalizations.

Closes the final CodeRabbit i18n-coverage thread. 0 analyze errors,
116 tests pass, ARB at 299 keys with 0 unused.

* fix(mobile): address i18n review feedback

- sso_onboarding: the "Accept Invitation" tab and submit buttons showed
  the Terms-of-Service string (ssoOnboardingAcceptTerms) when a household
  invitation was pending. Add ssoOnboardingAcceptInvitation and use it in
  both spots; drop the now-unused ToS key (no ToS checkbox exists).
- transaction_form: restore the required-field indicator on the amount
  label ("Amount *") that the migration dropped.
- settings: localize the "a newer version" fallback used in the update
  dialog when the store version is unknown.
- ARB plurals: use the CLDR `one{}` category instead of the exact-match
  `=1{}` so future non-English locales pluralize correctly. English output
  is unchanged.

ARB at 300 keys, 0 unused. 116 tests pass; flutter analyze: no new issues.

* fix(mobile): reconcile i18n with design-system primitives after merge

The merge of main into this branch pulled in the design-system primitive
migrations (SureCard/SureChip/SureSegmentedControl/SureTextField/
SureListGroup), which collided with the i18n string migration and left
several files broken (compile errors / un-localized text).

Re-apply the i18n migration on top of the design-system versions:
- more_screen: SureListGroup/SureListRow rows now use l.more* keys
  (was a mangled mix of _buildMenuItem + SureListGroup).
- transaction_form_screen: SureSegmentedControl segments + amount/date/
  name/category labels, helpers, validators, and snackbars localized;
  add transactionFormAmountHelper ("Required") for the amount helper text.
- custom_proxy_headers_editor: SureTextField label/hint params localized.
- currency_filter: the "All" chip is a SureChip again (the merge had left
  it as a Material FilterChip) and uses l.commonAll.
- currency_filter_test / custom_proxy_headers_editor_test: add the
  localization delegates the widgets now require.

ARB at 301 keys, 0 unused. 165 tests pass; flutter analyze: 0 errors.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-21 11:53:12 +02:00
Juan José Mata
88d382f2f8 Bump version to next iteration after v0.7.2-alpha.8 release (#2423)
* Bump version to next iteration after v0.7.2-alpha.8 release

* Version bump

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 11:50:59 +02:00
dependabot[bot]
3d5a82b8de chore(deps): bump concurrent-ruby from 1.3.6 to 1.3.7 (#2426)
Bumps [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) from 1.3.6 to 1.3.7.
- [Release notes](https://github.com/ruby-concurrency/concurrent-ruby/releases)
- [Changelog](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.6...v1.3.7)

---
updated-dependencies:
- dependency-name: concurrent-ruby
  dependency-version: 1.3.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 11:49:27 +02:00
dependabot[bot]
2672b06700 chore(deps): bump faraday from 2.14.1 to 2.14.3 (#2424)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.14.1 to 2.14.3.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.14.1...v2.14.3)

---
updated-dependencies:
- dependency-name: faraday
  dependency-version: 2.14.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 11:48:14 +02:00
dependabot[bot]
c0f4c4483c chore(deps): bump nokogiri from 1.19.3 to 1.19.4 (#2425)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.19.3 to 1.19.4.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.19.3...v1.19.4)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-version: 1.19.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 11:47:22 +02:00
dependabot[bot]
916a55b613 chore(deps): bump undici and wrangler in /workers/preview (#2410)
Bumps [undici](https://github.com/nodejs/undici) to 7.28.0 and updates ancestor dependency [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together.


Updates `undici` from 7.24.8 to 7.28.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.24.8...v7.28.0)

Updates `wrangler` from 4.90.0 to 4.103.0
- [Release notes](https://github.com/cloudflare/workers-sdk/releases)
- [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.103.0/packages/wrangler)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
- dependency-name: wrangler
  dependency-version: 4.103.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 11:43:54 +02:00
dependabot[bot]
10a1bc8d1c chore(deps): bump esbuild and wrangler in /workers/preview (#2411)
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.28.1 and updates ancestor dependency [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together.


Updates `esbuild` from 0.27.3 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.3...v0.28.1)

Updates `wrangler` from 4.90.0 to 4.103.0
- [Release notes](https://github.com/cloudflare/workers-sdk/releases)
- [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.103.0/packages/wrangler)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
- dependency-name: wrangler
  dependency-version: 4.103.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 11:43:30 +02:00
Juan José Mata
19f6a84b70 Harden release workflows for iOS and version bumps 2026-06-21 11:14:47 +02:00
Anthony
109526e53d fix(settings): preserve scroll position in settings nav (#2373)
fix(settings): preserve scroll position in settings nav

The settings nav scrolled back to the top on every Turbo navigation.
preserve-scroll was applied to the nav element itself, but that element has no overflow - scrollTop was always 0, so nothing was ever saved.

Apply preserve-scroll to the actual scrollable container (the sidebar div with md:overflow-y-auto) and give it an id so the controller can key the saved position.
2026-06-20 22:08:27 +02:00
ghost
69f8a5b95f feat(mobile): add SureSegmentedControl primitive and migrate the transaction type selector (#2382)
* feat(mobile): add SureSegmentedControl primitive and migrate the transaction type selector

Adds SureSegmentedControl<T>, the segmented-control primitive completing the
mobile form-control set after SureTextField and SureChip: an inset track of
equal-width segments where the selected segment is a raised surface (container
fill + the subtle DS shadow) and unselected segments are flat textSecondary
labels, resolving from the active SureColors palette. Each segment is
keyboard/switch focusable (FocusableActionDetector, mirroring SureButton) with
button + selected semantics; the selected fill is brightness-aware because the
palette has no single raised-surface token that reads correctly in both modes.

Migrates the transaction-form Type selector off Material SegmentedButton as
proof (selection behavior preserved).

Part of #2235.

* fix(mobile): assert unique SureSegmentedControl segment values

Selection is value-based (segment.value == selected), so duplicate
segment values would render multiple segments as selected at once. Guard
against that with a debug assert in build() (a const constructor can't
host the non-constant uniqueness check). Addresses CodeRabbit review.
2026-06-20 21:57:45 +02:00
ghost
65c4487bfc feat(mobile): add SureChip primitive and migrate the currency filter (#2380)
* feat(mobile): add SureChip primitive and migrate the currency filter

Adds SureChip, the filter-chip primitive following SureTextField in the mobile
design-system sequence: a tokenized selectable pill resolving from the active
SureColors palette — a bordered transparent pill when unselected and a filled
neutral (buttonPrimary + inverse label) pill when selected — replacing the
Material FilterChip primaryContainer tint + blue primary border. Includes
selected-state semantics (announced as a button only when interactive), a 44px
min tap target, and single-line ellipsised labels.

Migrates the currency filter off Material FilterChip as proof (selection logic
preserved). The segmented control will follow as a separate PR.

Part of #2235.

* fix(mobile): normalize stale codes in currency filter All detection

Length-based All detection could misfire when selectedCurrencies holds
stale codes no longer in availableCurrencies. Intersect with the
available set before comparing, and require containsAll before emitting
the empty (All) selection. Addresses CodeRabbit review.
2026-06-20 21:56:58 +02:00
Artem Danilov
c65f4828dd fix(tinkoff): resolve the tradeable listing and price bonds via BondBy (#2413)
* fix(tinkoff): resolve the tradeable listing and price bonds via BondBy

Validated against the live T-Invest API and fixed three correctness issues in
Provider::TinkoffInvest:

- FindInstrument returns several listings per ticker (e.g. SBER on TQBR plus
  dark/non-API boards 37M/SPEQ); only the apiTradeAvailableFlag listing has live
  prices. resolve_short now ranks tradeable-first (then requested-MIC, then exact
  ticker/ISIN) instead of taking the first match, so GetLastPrices/GetCandles
  return data. Search now surfaces only tradeable instruments.
- find_instruments no longer hard-filters on apiTradeAvailableFlag, so a
  qualified-investor instrument Tinkoff lists but can't API-trade still resolves
  (for logos) and otherwise falls back to another price provider.
- Bond nominal comes from BondBy (the generic GetInstrumentBy omits it),
  returning the current amortized nominal so percent-of-par converts correctly.

Verified end-to-end: SBER 313.65, T 282.76, LQDT 2.019, and the SFO Split bond
(amortized nominal 417.71) all price; logos resolve to real CDN PNGs.

* fix(tinkoff): honor requested MIC first, guard amortizing bonds, strip suffix

Address review feedback on the resolution/pricing path:

- resolve_short now ranks a requested-MIC match BEFORE tradeability, so a
  security is never priced off another exchange's listing (e.g. a MISX security
  no longer picks up a tradeable XSPX board's price); tradeability and exact
  ticker/ISIN remain secondary tie-breaks.
- Amortizing bonds: BondBy returns only the current nominal, so applying it to
  historical percent-of-par closes would underprice them. For amortizing bonds
  we now return only the live price and skip the candle history; fixed-par bonds
  and equities keep full history. bond_info exposes nominal + amortizationFlag.
- Strip exchange suffixes (.ME/.MOEX/.MISX/.MCX) before querying T-Invest, which
  only knows the bare SECID — so a stored "T.MOEX" ticker resolves to "T".

* fix(tinkoff): don't cache nil resolution results (skip_nil) so a transient empty response isn't locked in for the 24h TTL
2026-06-20 21:46:01 +02:00
ghost
61d56a8a3c feat(mobile): add SureTextField primitive and migrate the proxy-headers editor (#2378)
Adds SureTextField, the form-control primitive following SureButton in the
mobile design-system sequence: a TextFormField wrapper that builds a complete,
brightness-aware InputDecoration from the active SureColors palette (filled
bg-container, borderSecondary hairline -> borderPrimary on focus, destructive
error border, textSubdued placeholder, radiusLg corners) with a DS-style label
rendered above the field and associated to it for screen readers (Material
labelText parity via ExcludeSemantics + Semantics(label:)).

Migrates the custom proxy-headers editor off raw TextFormField as proof.
Filter chips and a segmented control will follow as separate PRs.

Part of #2235.
2026-06-20 21:43:41 +02:00
ghost
305c11d4a6 feat(mobile): add SureListGroup/SureListRow primitives and migrate the More menu (#2376)
* feat(mobile): add SureListGroup/SureListRow primitives and migrate the More menu

Adds the grouped inset-list primitives that follow SureCard in the mobile
design-system sequence: SureListGroup (tokenized container — bg-container +
borderSecondary hairline + radiusLg corners + shadowXs, with clipped inset
dividers and an optional section header) and SureListRow (leading / title /
subtitle / trailing slots, a DS chevron disclosure affordance, and a
destructive variant). Both resolve from the active SureColors palette, so
they're brightness-aware and stay in lockstep with sure.tokens.json.

Migrates the More menu off raw Material ListTile/Divider (which fell back to
colorScheme defaults) as proof. Adds the lucide chevron-right asset and
SureIcons.chevronRight for the row disclosure indicator.

Part of #2235.

* fix(mobile): restore button semantics on SureListRow + harden grouped list

Adversarial + CodeRabbit review follow-ups on the SureListGroup primitives:

- a11y: a tappable SureListRow lost the button/enabled semantics the migrated
  Material ListTile exposed (a bare InkWell emits only a tap action). Wrap the
  tappable content in MergeSemantics + Semantics(button) so screen readers
  announce each row as a single button again. Adds a semantics regression test.
- SureListGroup collapses to SizedBox.shrink() for an empty children list
  instead of painting a 2px bordered/shadowed box.
- More menu: restore the leading icon-badge tint to textPrimary (the migration
  had silently dropped it to the lower-contrast textSecondary).
- Tests: extend divider / title / subtitle / destructive assertions to dark mode
  (CodeRabbit nitpick), plus the new semantics + empty-group cases.
2026-06-20 21:43:29 +02:00
ghost
56000d7834 feat(mobile): add SureCard primitive and migrate account cards (#2370)
* feat(mobile): add SureCard primitive and migrate account cards

SureCard mirrors the web card chrome — `bg-container` + a hairline border +
rounded corners + the subtle DS shadow (`shadowXs`, from the scale shipped in
#2349) — resolved from the active SureColors palette so it stays in lockstep with
`sure.tokens.json` in light and dark. An optional `onTap` gives a flat ink
response clipped to the card radius.

Migrates AccountCard off the Material `Card` to `SureCard`.

Refs #2235.

* fix(coinstats): deterministic wallet batch order in bulk fetches

The bulk balance/transaction fetches built the "blockchain:address" param in
linked-account query order, which isn't stable across runs — so the batched
param (and its mocked expectations) varied seed to seed, intermittently failing
CoinstatsItem::ImporterTest and red-flagging unrelated PRs. Sort the wallets
before joining so the batch order is deterministic, and align the test
expectations to the sorted order.

* fix(mobile): tokenize swipe-reveal radius + cover SureCard dark theme

Address PR review:
- The account-card swipe-reveal background still used a hardcoded radius (12)
  that mismatched SureCard's radiusLg (10) once the card was migrated — tokenize
  it so the reveal corners line up during the swipe.
- Parameterize the SureCard chrome test over light + dark, since the card is
  brightness-aware (catches palette regressions in either mode).

* test(mobile): assert SureCard onTap ink is clipped to the card radius

Address review nit: the onTap test claimed the ink is clipped to the card but
only checked the InkWell exists. Now it asserts the InkWell's borderRadius equals
SureTokens.radiusLg (tester.widget already enforces a single InkWell).
2026-06-20 21:43:20 +02:00
Jake
dc2a565b6a feat(up): add Up Bank (AU) provider integration (#2391)
* feat(up): add Up Bank (AU) provider integration

Adds Up Bank as a per-family, token-based bank sync provider, modelled on
the existing Akahu integration. Up uses a JSON:API REST API with a personal
access token (Bearer), cursor pagination via links.next, and returns both
HELD (pending) and SETTLED transactions from one endpoint.

New:
- Provider::Up client (JSON:API unwrap, links.next pagination, retries,
  typed errors, /util/ping) + Provider::UpAdapter (Factory-registered,
  Depository + Loan).
- UpItem / UpAccount models with Provided, Unlinking, Syncer,
  SyncCompleteEvent, Importer, Processor, Transactions::Processor, and
  UpEntry::Processor (amount sign flip, HELD->pending, foreignAmount FX,
  merchant from description, stale-pending pruning).
- Family::UpConnectable, UpItemsController, routes, settings panel + connect
  flow views, accounts index wiring, initializer, en locale, and model tests.

Core wiring:
- "up" added to Transaction::PENDING_PROVIDERS, the three pending-match SQL
  blocks in Account::ProviderImportAdapter, Provider::Metadata::REGISTRY,
  ProviderMerchant/DataEnrichment source enums, ProviderConnectionStatus,
  settings provider panels, and financial data reset.

Migration create_up_items_and_accounts must be run before use. No external
API endpoints added (no OpenAPI changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): dump up tables to schema and make since filter TZ-safe

The feature commit added the up_items/up_accounts migration but never
re-dumped db/schema.rb, leaving the schema version and tables stale.
Add the two table definitions and foreign keys and bump the schema
version so a fresh DB load matches the migration.

Also format a bare Date `since` as UTC midnight instead of the server's
local zone, so `filter[since]` is deterministic regardless of where the
app runs (previously shifted by the local UTC offset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): address code review feedback

Behavior/correctness:
- Persist skipped accounts via a new up_accounts.ignored flag and a
  needs_setup scope, so skipped accounts stop resurfacing as "needs
  setup" on every sync. Linking clears the flag.
- destroy now checks unlink_all! per-account results and aborts deletion
  (alert) if any unlink failed, instead of swallowing failures.
- render_provider_panel_error redirect uses :see_other (was an invalid
  4xx redirect status).
- Up provider adapter falls back to item institution name/url when
  institution_metadata is absent (early return previously blocked it).

Resilience/security:
- fetch_all_resources guards against an API repeating the same
  links.next cursor (Set#add?), preventing infinite pagination.
- HTTP client validates absolute URLs (from links.next) against Up's
  HTTPS host before sending the bearer token, preventing credential
  leakage to untrusted hosts.

Diagnostics:
- Route provider sync/import failures through DebugLogEntry.capture
  (controller, UpItem, syncer, unlinking) with family/account context.
  Low-level HTTP client and currency-normalization warnings keep
  Rails.logger to match existing provider conventions.

Data integrity:
- up_accounts.name and currency are NOT NULL (align with model presence
  validations); account_id stays nullable (allow_nil uniqueness).

Forms:
- select_existing_account radio is required; controller guards a blank/
  unknown up_account_id with a friendly alert instead of RecordNotFound.

Tests:
- Add UpAccount needs_setup scope test, pagination loop guard test,
  untrusted-host rejection test; tighten filter[since] assertion to the
  exact UTC timestamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): address second-round review feedback

- Capture sync/import failures via DebugLogEntry so swallowed errors in
  account/transaction fetching and transaction processing surface in
  /settings/debug instead of only Rails.logger.
- Gate UP_DEBUG_RAW raw payload dump to local envs to avoid leaking PII
  (merchant names, amounts, account IDs) in managed/production logs.
- Collapse linked/unlinked/total account counts into one memoized query
  instead of 3 separate COUNTs per rendered item.
- Rename "Set Up Up Accounts" locale title to "Link Up Accounts".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(up): add method docstrings and align failed_result keys

Add docstrings to all Up provider source files (controller, models,
providers, concerns) to satisfy the 80% docstring coverage threshold.

Third-round review: failed_result now mirrors import's result shape
(accounts_updated/created/failed, transactions_imported/failed) instead
of the stale accounts_imported key, so failure results stay consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:33:09 +02:00
Josh
60d9a70aff Refresh Pipelock integration for v2.8 receipts (#2406)
* chore(pipelock): refresh integration for v2.8 receipts

* Clarify Pipelock receipt key mounts
2026-06-19 17:16:38 +02:00
Orange🍊
6e5f35b306 fix(api): prevent API auth from inheriting impersonation (#2405)
Build fresh API session contexts instead of reusing persisted web sessions that may carry impersonation state.

Reject deactivated report export API key owners and strengthen regression coverage for API key, OAuth, and report export authentication paths.
2026-06-19 17:14:43 +02:00
Sure Admin (bot)
318c7476e1 fix(coinstats): stabilize bulk wallet ordering (#2402) 2026-06-19 17:09:35 +02:00
Artem Danilov
9de9a23ce2 feat(prices): add T-Invest (T-Bank) securities + brand-logo provider (#2408)
Adds Provider::TinkoffInvest, a token-based securities provider built on the
public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves
prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand
logos via the T-Invest CDN — the authoritative logo source for MOEX
instruments, which ISS (MoexPublic) does not provide.

- Registry: register `tinkoff_invest` under the :securities concept; token via
  ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key.
- Logos independent of the price provider: Security#import_brand_logo consults
  T-Invest for a logo whenever a token is configured (after the price-provider
  metadata fetch, so it never short-circuits website_url backfill). Gated on
  token presence, not the securities checklist.
- display_logo_url: with no website domain, a stored provider logo (T-Invest)
  now beats the ticker-only Brandfetch lettermark; when a domain exists,
  Brandfetch still wins (unchanged).
- MoexPublic no longer reports moex.com as the issuer website — it's the
  exchange, not the issuer, and would make Brandfetch render the exchange logo
  for every instrument and shadow the real brand logo.
- Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9;
  bonds priced as percent-of-par x nominal (missing nominal raises, not 0).
- Settings: encrypted token field (always shown) + provider checkbox + en locale.
- Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-19 17:07:09 +02:00
sentry[bot]
84df335e7c fix(trades): guard against nil security in create_trade (#2066)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-06-19 16:54:37 +02:00
ghost
6ae9779567 perf(reports): avoid residual category lazy loads (#2255)
* perf(reports): avoid residual category lazy loads

* test(reports): reuse SQL query capture helper

Move the reports SQL capture regression helper into test/support and document the category loading and budget reload choices called out in review.
2026-06-18 20:19:36 +02:00
ghost
894d705e83 perf(dashboard): streamline investment activity totals (#2257)
* perf(dashboard): streamline investment activity totals

* fix(dashboard): skip empty investment totals cache writes

Short-circuit empty investment account totals before cache fetch and move the new SQL query capture regression helper into test/support.

* refactor(dashboard): address review on investment totals query

- Drop defensive ArgumentError guards in InvestmentStatement::Totals#initialize.
  The sole caller (totals_query) always passes correct types, and the
  empty_result early-return already handles the zero-accounts case.
- Remove the redundant accounts JOIN and its family_id/status filters from the
  aggregation SQL. account_ids is already scoped to the family's visible
  (draft/active) investment accounts, so the join back to accounts is
  unnecessary work in the query plan. Drop the now-unused family_id param.
- Add a regression assertion that the aggregate no longer joins accounts.
2026-06-18 20:18:18 +02:00
ghost
5811a8df15 perf(recurring): batch recurring transaction identification (#2239)
* perf(recurring): batch recurring transaction identification

* fix(recurring): clamp manual month-end matching

* test(recurring): guard amount-scoped lookup cache
2026-06-18 12:28:32 +02:00
Artem Danilov
b57bb938da feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider (#2394)
* feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider

Add Provider::MoexPublic, a keyless provider built on the free MOEX ISS API
(https://iss.moex.com/iss), modeled on Provider::BinancePublic.

Securities: shares, funds/ETF/БПИФ (e.g. LQDT), and bonds (OFZ + corporate).
Bonds are priced clean — LAST% × FACEVALUE / 100 in the instrument currency,
with per-row FACEVALUE for amortizing issues; NKD/accrued coupon excluded.

Exchange rates: also implements ExchangeRateConcept for RUB↔{USD,EUR,CNY} via
selt TOM instruments (USD000UTSTOM/EUR_RUB__TOM/CNYRUB_TOM); the selt quote is
X/RUB, inverted for RUB→X, nil for non-RUB-crossed pairs.

Details:
- Board/engine resolution via the ISS primary-board flag with a hardcoded
  priority fallback (TQBR, TQTF, TQOB, TQCB, …).
- Instrument currency from CURRENCYID/FACEUNIT (handles USD/CNY eurobonds &
  FX funds), normalizing legacy SUR/RUR → RUB; default RUB.
- Full history via from/till + start= pagination; current price fallback chain
  LAST → MARKETPRICE → LCURRENTPRICE → LCLOSEPRICE → PREVPRICE → latest history
  close.
- Bare SECID identity, exchange_operating_mic=MISX, country_code=nil (wildcard
  like Binance); search accepts .ME/.MOEX/.MISX/.MCX aliases and ISIN.
- RateLimitable throttling, SslConfigurable, Faraday retry/timeouts; all public
  methods wrapped in with_provider_response.

Wired into Provider::Registry for both :securities and :exchange_rates, the
hosting provider-selection UI, locales, and config/exchanges.yml (MISX).

Docker-tested (devcontainer, Ruby 3.4.9): 29 new tests green, full provider
suite + i18n green, rubocop clean; smoke-tested against live ISS (SBER price,
OFZ clean price, USD/RUB FX).

* fix(moex): address review — FX weekend lookback, dead branch, translated hints

- fetch_exchange_rate now fetches a 10-day lookback window (not just the exact
  day) so a weekend/holiday request resolves to the prior trading day's close,
  matching Yahoo's behavior (Codex P2).
- Remove dead identical if/else branches in history_row_price (CodeRabbit).
- Translate moex_public_hint into ca/fr/hu/vi/zh-CN instead of English copy
  (CodeRabbit).
- Add a test covering the FX prior-trading-day lookback.

* fix(moex): guard ISS date parsing; doc TQTE in board priority

Address maintainer review (jjmata):
- parse_iss_date wraps Date.parse so a malformed ISS TRADEDATE skips just that
  row (with a contextual log warning) instead of failing the whole history/FX
  fetch. Used in history_row_price and fx_history.
- Add TQTE to the BOARD_PRIORITY doc comment (it was in the constant but missing
  from the comment).
- Add a test covering the unparseable-date skip.
2026-06-18 10:36:20 +02:00
Orange🍊
59b2e59a4a fix(ds): 给仪表盘尺寸控件加焦点环 (#2364) 2026-06-17 11:54:44 +02:00
Artem Danilov
fffcaf2345 fix(prices): resolve dashed crypto tickers (BTC-USD, TRX-USD) via BinancePublic (#2372)
* fix(prices): resolve dashed crypto tickers (BTC-USD, TRX-USD) via BinancePublic

Provider::BinancePublic is the dedicated crypto price provider, but its
search_securities only matched bare base assets ("BTC") or unseparated
Binance pairs ("BTCUSDT"/"BTCUSD"). The canonical "<BASE>-USD" form that
Yahoo emits and users paste (e.g. "TRX-USD", "USDT-USD") never matched, so
those holdings fell through to an unpriced offline security whenever the
stock provider didn't also return the coin.

Collapse the base/quote separator in the search query (and strip it in
parse_ticker) so "BTC-USD" / "TRX/USDT" are treated like "BTCUSD" and
resolve to live Binance pricing. Stablecoins pasted as "USDT-USD" resolve
to the synthetic USD price via their stripped base.

* fix(prices): only synthesize stablecoin search for USD quote

Address review: gate the synthetic stablecoin result on a USD quote (bare
coin or "<coin>USD" form). A non-USD quote like "USDTEUR" / "USDT-EUR"
has a real Binance pair and now falls through to normal matching instead of
being replaced by the USD synthetic. Drops the now-unused base_asset_query
helper. Adds a USDT-EUR regression test.
2026-06-17 11:38:34 +02:00
Kelvinchen03
11bba13601 Add ON DELETE CASCADE to rejected_transfers foreign keys (#2211) 2026-06-16 17:22:59 +02:00
Augusto Xavier
844cba2fd3 fix(imports): normalize CSV upload encoding when validations are skipped (#2299)
Import::UploadsController#update persists CSV uploads with
save!(validate: false), which skips the before_validation
:ensure_utf8_encoding callback. Non-UTF-8 files (e.g. ISO-8859-1 /
Windows-1252 exports from Brazilian banks) were written to the text
column as-is and rejected by Postgres with PG::CharacterNotInRepertoire,
surfacing as a 500 during upload.

Also register ensure_utf8_encoding on before_save so the existing
normalization (rchardet detection + Latin-1/Windows fallback) still runs
when validations are skipped. The callback is idempotent and no-ops on
valid UTF-8, so the validated path is unchanged.

Fixes #2294
2026-06-16 17:13:03 +02:00
SenaRinka / 陶音まの
aec851cae1 Add transaction name to get_transactions MCP tool output (#2026)
* fix(assistant): include transaction name in get_transactions MCP output

get_transactions already loads `entry = txn.entry` but only emits
`merchant`, which is null unless a merchant is assigned. As a result MCP
clients receive transactions with no identifying name. Add `entry.name`
(the description shown in the UI and returned by the REST API) at zero
extra query cost.

* fix(assistant): include transaction name in get_transactions MCP output

get_transactions already loads `entry = txn.entry` but only emits
`merchant`, which is null unless a merchant is assigned. As a result MCP
clients receive transactions with no identifying name. Add `entry.name`
(the description shown in the UI and returned by the REST API) at zero
extra query cost.
2026-06-16 16:52:39 +02:00
Will Wilson
7dbb548479 fix(cashflow): use Turbo.visit for donut chart deep-link navigation (#2166)
Replaces window.location.href with Turbo.visit for SPA-consistent
navigation, matching the pattern used across internal navigation in
the codebase (selectable_link_controller, trade_form_controller, etc.).
2026-06-16 16:51:53 +02:00
Tristan the Katana
ee89aec850 Add Mobile version check for update notifications (#2367)
* feat(mobile): in-app update prompt via upgrader package

- Add upgrader ^13.5.0 dependency for automatic update checks
- Show UpgradeAlert after authentication, with a 5-minute delay so
  users can settle in before being prompted
- Dialog shows at most once every 7 days per store version
- Only "Later" and "Update now" buttons — ignore button removed via showIgnore: false
- Custom _SureUpgraderMessages with mustache body template (v13 API)
- Add "Check for updates" tile in Settings for manual checks:
  calls updateVersionInfo() and shows an AlertDialog with store URL
  if an update is available, or a snackbar if already up to date
- Timer resets on logout so the delay restarts on next sign-in

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(mobile): address upgrader review comments

- Mount UpgradeAlert unconditionally so MainNavigationScreen is never
  remounted when the timer fires, preserving navigation state
- Dispose _manualUpgrader in SettingsScreen to remove its lifecycle
  observer when the screen is torn down
- Show update dialog even when store version metadata is incomplete
  (falls back to 'a newer version'); disable Update now button rather
  than silently no-oping when store URL is unavailable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(mobile): show snackbar when store URL cannot be opened

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(mobile): dispose manual upgrader if unmounted after initialize

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 16:49:52 +02:00
Artem Danilov
e342ac4ad1 fix(accounts): persist subtype when creating an account (#2356)
The Account#subtype= writer delegates to accountable&.subtype=, which is a
silent no-op while the accountable is nil. On create the accountable is built
from accountable_attributes via accepts_nested_attributes_for, but the form
submits subtype as a top-level account attribute. Mass-assignment applies
subtype before accountable_attributes, so the selected subtype was dropped on
create (update worked because the accountable already exists).

Build the accountable from the delegated type inside the writer when it is not
yet present, so the value is preserved; the later accountable_attributes
assignment (update_only) updates the same record.

Add regression tests covering the create flow and the assignment ordering.
2026-06-16 10:57:35 +02:00
Jonathan Chang
b9716a0485 fix: Transaction Pagination Skipping Entries (#2179)
* Fix transaction pagination regressions and CI blockers

* Rails EOL already fixed

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-06-16 10:51:51 +02:00
ghost
cfcd1e0d23 feat(mobile): generate shadow scale + focus-ring/bg-inverse tokens (#2349)
Extend mobile/tool/generate_sure_tokens.mjs to emit, from the canonical design/tokens/sure.tokens.json: a mode-aware box-shadow scale (shadowXs..shadowXl, parsed from shadow.xs..xl into List<BoxShadow>), plus the focus-ring (color.focus-ring) and bg-inverse (utility.bg-inverse) colors on SureTokenPalette. Regenerates mobile/lib/theme/sure_tokens.dart (import dart:ui -> package:flutter/painting.dart for BoxShadow/Offset); keeps the --check parity gate; adds token tests for the new values incl. the negative xl spread.

Radii are already complete (the source only defines md/lg); a type-scale is not in the token source, so both are intentionally untouched. The shadow-border composites are deferred (derivable in-widget as a shadow + a 1px border from existing border tokens). This unblocks SureCard/elevated surfaces.

Part of #2235.
2026-06-16 10:04:26 +02:00
ghost
94d5ad2151 fix(mobile): neutral Sure tokens for FAB/badge/avatar surfaces + themed logo (#2366)
On screens not yet redesigned with SureColors (the Chats list, etc.), Material's
primary/secondary *container* roles were unset, so they fell back to defaults
that read as blue on the FAB, unread-badge, and avatar surfaces.

- Pin `primaryContainer`/`secondaryContainer` to a neutral Sure surface
  (`surfaceInset` + `textPrimary`), and add a `FloatingActionButtonThemeData` so
  FABs use Sure's neutral primary action color (`buttonPrimary`). `primary` /
  `secondary` stay `link` / `info`, so existing link/accent callers are unchanged.
- Add a `SureLogo` widget that renders `logomark.svg` with the wordmark's
  `currentColor` strokes tinted to the theme's secondary text color, and route
  every logomark consumer (nav bar, login) through it — so the mark stays legible
  in dark mode (the hardcoded grey was too dim) and no caller renders the strokes
  as the flutter_svg default (black). The green brand mark keeps its fill.

Refs #2235.
2026-06-16 10:03:28 +02:00
ghost
4224ff717c perf(api): avoid transfer lookups in transaction index (#2127) 2026-06-16 09:54:57 +02:00
ghost
4eca3b5b6b feat(mobile): add SureIcon (Lucide) primitive and migrate dashboard icons (#2346)
Introduce a SureIcon design-system primitive that renders bundled Lucide SVGs via flutter_svg (already a dependency — no new dep), mirroring the web `icon` helper / DS::FilledIcon: tokenized size (SureIconSize xs–2xl = 12–32), color inherited from the ambient IconTheme by default, and accessibility that distinguishes decorative icons (excluded from the semantics tree) from meaningful ones (semanticLabel). A SureIcons registry keeps call sites typo-safe and limited to bundled assets.

Migrate the dashboard surface off Material Icons.* onto SureIcon — 24 call sites across net_worth_card, account_card, and dashboard_screen (account-type glyphs, asset/liability trend icons, sync/empty/error states, refresh, collapsible section headers, expand chevrons). Bundle the 17 Lucide SVGs the surface needs under assets/icons/lucide/ with the ISC license. The swipe Undo control and the rest of the app's icons follow in later slices.

Part of #2235.

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-16 09:49:14 +02:00
ghost
6dec201c22 feat(mobile): normalize dashboard text weight to DS Medium (500) (#2344)
Migrate the dashboard text tier off the off-convention bold (700) and semibold (600) to the design-system Medium (500): 14 weight-only sites across 5 files — net-worth headline + breakdown modal + asset/liability filter balances, account-card name + balance, collapsible-type section header + count badge, the stale "Outdated" badge, and the recent + list transaction names + amounts.

The web DS uses font-medium/500 for emphasis (essentially no bold or semibold), so this lands one uniform Medium tier and removes the inverted hierarchy where a money amount rendered lighter than its own label. Sizes, colors, and structure are untouched (zero layout shift); money keeps SureMoney.tabular and semantic color. The swipe "Undo" button affordance (w600 + hardcoded color) is intentionally left for a future button-primitive slice.

Part of #2235.
2026-06-16 09:37:55 +02:00
ghost
8ad3975759 feat(mobile): bundle Geist + Geist Mono font assets (#2342)
#2237 named the Sure font families (SureTokens.fontSans 'Geist', fontMono
'Geist Mono') from design/tokens/sure.tokens.json but deferred bundling the
fonts, so the app declared fontFamily 'Geist' yet rendered the platform system
font. Bundle the canonical Geist family so it actually renders -- the typography
half of the mobile facelift (#2235).

- mobile/assets/fonts/: Geist Regular/Medium/SemiBold/Bold + Geist Mono (.ttf),
  with SIL OFL-1.1 (OFL.txt).
- mobile/pubspec.yaml: declare the families under flutter > fonts (Geist
  400/500/600/700; Geist Mono 400).

No code/layout changes -- every text surface inherits Geist via SureTheme.

Validated: flutter analyze --no-fatal-infos clean, flutter test 95 pass,
iOS + Android builds include Geist in the font manifest; real iOS-sim
before/after x light/dark screenshots confirm system font -> Geist.
2026-06-16 09:35:09 +02:00
ghost
6ea931f3fe feat(imports): add YNAB CSV import (#2361)
Adds YnabImport (mirroring ActualImport) for YNAB "Export budget" register CSVs:

- Amount — combines the split Outflow/Inflow columns into a single signed amount
  (inflow - |outflow|), stripping currency symbols and thousands separators. A
  single signed Amount column takes precedence when present.
- Category — resolves across export shapes: the combined "Category Group/Category"
  column, the split "Category Group" + "Category", or legacy YNAB 4
  "Master Category" + "Sub Category".
- Names — falls back from a blank Payee to the Memo, then the default row name.
- Validation — requires at least one amount source (Outflow/Inflow or Amount); a
  file exposing none leaves rows un-clean instead of importing zero-dollar entries.

Enables the previously-disabled YNAB option on the imports screen (using the YNAB
logo, like Mint) with its configuration partial, and removes the now-dead
imports.new.coming_soon locale key. Documents the type in the API import-type
enums (rswag request spec + swagger_helper + generated openapi.yaml).

Closes #1255.
2026-06-16 08:32:17 +02:00
Andrew B
6945b5a296 feat(security): warn when ActiveRecord encryption is not configured (#2362)
* feat(security): warn when ActiveRecord encryption is not configured

Self-hosted instances without explicit ACTIVE_RECORD_ENCRYPTION_* keys (or Rails credentials) store sensitive columns - API keys, provider/bank tokens, the MFA (TOTP) secret, and PII - unencrypted at rest. The app boots and works normally so this plaintext at rest state is easy to miss.

Change: Make it visible:
  - log a clear startup warning (config/initializers/encryption_warning.rb)
  - show a warning banner on /settings/security when encryption is unconfigured

* refactor(security): apply review feedback on encryption warning

- list the three ACTIVE_RECORD_ENCRYPTION_* keys in the banner, rendered via the DS::Alert content block to match the log
- drop the redundant respond_to?(:self_hosted?) guard in the initializer so it matches the controller check
- add a managed-mode test asserting the banner is hidden
2026-06-16 08:11:03 +02:00
ghost
7b6b74874f feat(mobile): add SureButton primitive and migrate login buttons (#2358)
Introduce a custom (non-Material) SureButton mirroring the web DS::Button: variants (primary, secondary, destructive, outline, ghost), sizes (sm/md/lg ≈ 28/36/48), optional leading widget, fullWidth, loading, and disabled. Colors resolve from the SureColors palette and the shape from radius tokens (radiusMd/radiusLg); flat custom press feedback (no Material ripple). Dimensions mirror the web DS::Buttonish::SIZES (the web composes these from Tailwind's standard scale rather than brand tokens, so they're not in sure.tokens.json).

Migrate the three main-view login buttons onto it — Sign In (primary, lg, loading), Sign in with Google (outline + SVG leading), and API-Key Login (ghost). The two buttons inside the API-key modal dialog are left for a follow-up.

Part of #2235.
2026-06-16 08:10:44 +02:00
Juan José Mata
27501cb64c Update Ruby to 3.4.9 (#2360)
Bump the pinned Ruby version from 3.4.7 to 3.4.9 across .ruby-version,
the application/preview/devcontainer Dockerfiles, and Gemfile.lock.

https://claude.ai/code/session_01C6t7JgroFSe2Xswqma3EdL

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 23:29:36 +02:00
Augusto Xavier
09dc428136 fix(rules): make explicit re-apply override locked attributes (#2273)
When a rule is re-applied from the UI, RulesController passes
ignore_attribute_locks: true, but Enrichable#enrich_attributes still
rejected locked attributes unconditionally, so locked (manually edited
or import-locked) transactions were silently skipped and reported as
blocked.

Thread the flag through enrich_attribute/enrich_attributes as a new
ignore_locks keyword (default false, so provider syncs and AI
enrichment keep respecting locks) and pass it from the six synchronous
rule action executors.

Fixes #2051
2026-06-15 22:12:24 +02:00
ghost
94d2ee908d fix(jobs): enqueue jobs after transaction commit to fix SyncJob deserialization race (#2354)
* fix(jobs): enqueue jobs after transaction commit to fix SyncJob deserialization race

Syncable#sync_later creates a Sync row and enqueues SyncJob inside the
same transaction. Rails 7.2 deferred such enqueues until commit by
default; Rails 8.0 changed the default to enqueue immediately and 8.1
left the global config toggle non-functional, so a worker could dequeue
the job before COMMIT, fail to resolve the Sync GlobalID
(ActiveJob::DeserializationError), and have it silently dropped by
discard_on -- surfacing as stuck syncs after the Rails 8.1 upgrade.

Set enqueue_after_transaction_commit = true on ApplicationJob (the
Rails 8.2 default, inherited by every job) and drop the dead :never
symbol override on DestroyJob. Add regression and invariant tests.

* test(jobs): use OpenStruct for DestroyJob failure case

Replace the bare mock and the respond_to? expectation (an implementation
detail of how DestroyJob probes the model) with an OpenStruct that genuinely
responds to scheduled_for_deletion. Keeps only the command-facing assertions:
destroy raises and update! is called with scheduled_for_deletion: false.
Matches the repo convention of preferring OpenStruct for mock instances.

* test(snaptrade): assert connection-cleanup enqueue defers until commit

SnaptradeAccount#after_destroy enqueues SnaptradeConnectionCleanupJob, which
references the item/account by id. Before the ApplicationJob fix, Rails 8.1
enqueued it immediately inside the destroy transaction, so a worker could run
before COMMIT, see the not-yet-deleted row in its shared-authorization guard,
skip the provider call, and leak the SnapTrade connection. This regression
test destroys an account inside a transaction and asserts the job is not
enqueued until the transaction commits.
2026-06-15 21:43:08 +02:00
Guillem Arias Fauste
c29380ce57 feat(dashboard): masonry packing + per-widget size controls (#2328)
* feat(dashboard): masonry packing + per-widget size controls

In two-column mode the dashboard used a row-based CSS grid, so cards
stretched to equal row height and left dead space (e.g. the Net Worth
chart padded out to match the tall Balance Sheet table). Replace the
row-based layout with masonry packing and add per-widget size guardrails.

- Masonry: CSS grid with computed row-spans + grid-auto-flow: dense, driven
  by a dashboard-masonry Stimulus controller (ResizeObserver + turbo:frame-load).
  The DOM stays a single flat list, so drag/keyboard reorder is unaffected.
  Active only in multi-column mode; single column falls back to normal flow.
- Internal sizing: the net worth chart height is now driven by a
  --dash-widget-h CSS var (fixes an inert flex-1) so the card no longer pads
  out below the chart.
- Guardrails: per-widget layout metadata (col_span, grow, min_height,
  width_toggle) in PagesController, with per-user overrides persisted under
  preferences["dashboard_section_layout"], deep-merged so width and height coexist.
- Size menu: a hover control on size-capable cards — Width (Half/Full) for the
  cashflow sankey and net worth chart, Height (Compact/Auto/Tall) for grow
  widgets. The sankey defaults to full width.

Adds model + controller tests for preference persistence and i18n keys.

* refactor(dashboard): redesign size menu with segmented controls

The size menu used a plain radio list and a diagonal maximize-2 trigger that
collided with the cashflow sankey's modal-expand button. Replace it with a
layout-config popover: a sliders-horizontal trigger plus two labeled axis
groups (Width, Height), each rendered as a DS::SegmentedControl with the
active option filled. Clearer, more compact, and it reads as a card-layout
control. The widget-size controller now mirrors the segmented control's
active-class + aria-pressed contract.

* feat(dashboard): expose width toggle on balance sheet and investments

Tables benefit from horizontal room, so give Balance Sheet and Investments
the same Width (Half/Full) control as the sankey and net worth chart. Height
presets stay chart-only — a table sized to a fixed height would just add
whitespace or force scrolling. The Outflows donut is intentionally left out
(full width is mostly whitespace for a donut).

* fix(dashboard): address review feedback on size controls

- Only apply the full-width col-span and show the Width control when the
  two-column layout is enabled. A full widget previously leaked
  2xl:col-span-2 into the single-column grid, creating an implicit second
  column at 2xl widths and breaking the single-column preference. This also
  keeps the Width control coherent with the Appearance two-column setting
  (it now appears only where it does something). [Codex]
- Stop size-menu keydowns from bubbling to the section reorder handler, so
  keyboard users can open the menu and pick options without entering
  grab/reorder mode. [Codex]
- Harden preferences params: ignore a malformed (non-hash)
  dashboard_section_layout / collapsed_sections instead of raising a 500,
  and require section_order to be an array. [CodeRabbit]
- Localize the dashboard sections aria-label. [CodeRabbit]

* chore(settings): mention per-widget size controls in two-column copy

Surface the new per-widget width/height controls in the Appearance
"Two-column layout" description so the capability is discoverable.

* test(dashboard): assert non-mutation for malformed layout input

Addresses review feedback: asserting assert_nil made the test depend on
the fixture happening to have no dashboard height for net_worth_chart.
Capture the pre-PATCH value and assert it is unchanged, so the test
stays valid (malformed input ignored) even if the fixture later gets a
default height.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-15 20:34:34 +02:00