Commit Graph

2478 Commits

Author SHA1 Message Date
ghost
352c301e4b feat(api): expose valuation history index (#1596)
* feat(api): expose valuation history index

* fix(api): hide valuation exception details

* fix(api): reuse eager-loaded valuation entries

* fix(api): tighten valuation index contracts

* fix(api): scope valuation filter errors

* docs(api): nest valuation account filter format

* Fix merge conflict mistakes

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-05-01 19:09:56 +02:00
Guillem Arias Fauste
a7e964f8be feat(design-system): live tokens reference page in Lookbook (#1618)
* feat(design-system): live tokens reference page in Lookbook

Adds `DesignTokensPreview` at `/design-system/inspect/design_tokens/*`,
split into seven sub-pages (typography, palette, surfaces, text,
borders, controls, effects). Each reads `design/tokens/sure.tokens.json`
at request time and renders the corresponding slice with values
pre-resolved to literal hex / rgba in Ruby — Tailwind doesn't need to
keep every CSS variable alive for the swatches to render.

Also drops the `@source not "../../../design/tokens"` directive added
in #1604. Excluding the JSON tree-shook ten or so design system
utilities that aren't yet used in app views (`shadow-border-md/sm/xl`,
`button-bg-ghost-hover`, etc.). The preview references each utility
through dynamic ERB, which Tailwind's scanner can't follow, so those
swatches were rendering blank. Letting Tailwind scan the JSON keeps
every declared utility available, which matches the intent of a design
system. Compiled CSS grows by about 3 KB.

Stacked previously on the `refactor/design-system-tokens` branch behind
#1604; rebased onto `main` once that landed.

* style(design-system): apply rubocop indented_internal_methods to preview

CI lint flagged the private helpers in DesignTokensPreview because the
project's RuboCop config uses `indented_internal_methods` style (methods
after `private`/`protected` get an extra 2-space indent). Auto-fixed
with `bin/rubocop -A`.

* fix(design-system): pre-resolve utility token values for the preview

CodeRabbit caught: collect_utilities was passing raw `{ref}` strings
(e.g. `{color.gray.50}`) as light_value/dark_value, while the rest of
the class pre-resolves to literal hex / rgba. The four templates that
display them (surfaces, text, borders, controls) showed the unresolved
template strings to users.

Adds `light_resolved` / `dark_resolved` fields to each utility entry,
populated via the same `resolve_template` helper the other collectors
use. Templates display `:light_resolved || :light_value` so plain class
strings (e.g. `border-tertiary`, `bg-gray-800 fg-inverse`) and compose
cases still fall through correctly.
2026-05-01 16:06:25 +02:00
Guillem Arias Fauste
05b5dba445 feat(tokens): add $version field + document versioning policy (#1620)
Adds $version: "1.0.0" at the root of design/tokens/sure.tokens.json so
external consumers (Tokens Studio, future Figma sync, AI design tooling,
etc.) have a stable signal for when their cached snapshot of the token
API is out of date.

The build script's walker already skips top-level $-prefixed keys, so no
code change is needed and _generated.css is unchanged.

README documents semver semantics scoped to the token contract:
- Major: breaking (token removed/renamed, type changed, dark dropped).
- Minor: additive (new tokens, new $extensions.sure.* keys, new groups).
- Patch: cosmetic value tweaks.

Bump on commit.
2026-05-01 16:05:35 +02:00
Guillem Arias Fauste
6aa7adb931 fix(design-system): give cyan-900 a darker value than cyan-800 (#1619)
Both `cyan-800` and `cyan-900` were defined as `#155B75` since the
original commit that introduced the v4 design system, leaving the cyan
scale plateaued at the dark end. Setting `cyan-900` to `#164E63`
(Tailwind v3's default for that step), so the scale progresses
monotonically.

The token has zero current usage (`bg-cyan-900`, `text-cyan-900`,
`border-cyan-900` aren't referenced anywhere outside the design system
source), so this change is invisible in the running app today.

Closes #1605.
2026-05-01 16:05:21 +02:00
ghost
cc043b5caf feat(api): expose complete account export state (#1597)
* feat(api): expose complete account export state

* fix(api): handle malformed account identifiers

* fix(api): tighten account export contracts

* fix(api): correct account id OpenAPI format

* fix(api): tighten account docs auth contracts

* docs(api): document balance sheet auth errors

* docs(api): clarify account scope fixture
2026-05-01 15:22:28 +02:00
github-actions[bot]
cfa4dfd035 Bump version to next iteration after v0.7.1-alpha.1 release 2026-05-01 12:59:55 +00:00
Guillem Arias Fauste
e250d266e8 refactor(design-system): single-source design tokens via DTCG JSON (#1604)
* refactor(css): rename maybe-design-system → sure-design-system

Rename design system CSS file and directory to match the project name
post-rebrand. Update internal imports plus references in CLAUDE.md,
copilot instructions, and Junie guidelines. No CSS rules change; Tailwind
compiled output is byte-identical.

* build(tokens): introduce single-source tokens.json + build script

Make the design system a tool-agnostic single source of truth.

- tokens/sure.tokens.json: every primitive, semantic alias, and Tailwind
  utility token in one W3C DTCG-flavored file.
- tools/tokens/build.mjs: ~120 LOC plain Node script (zero deps) that
  resolves token references and emits Tailwind v4 source CSS.
- app/assets/tailwind/sure-design-system/_generated.css: build output —
  the @theme block, dark-mode overrides, and 50 @utility blocks.
- Hand-written CSS split into base.css (element resets), components.css
  (form-field/checkbox/tooltip/qrcode), and prose.css (prose dark
  overrides). The 5 maybe-design-system/*-utils.css files are removed —
  their contents now live inside _generated.css.
- application.css gains `@source not "../../../tokens"` so Tailwind's
  content scanner ignores the JSON file (it would otherwise treat token
  keys like `bg-surface` as "used" classes and skip tree-shaking).
- package.json: `npm run tokens:build` and `npm run tokens:check`.
- .gitattributes: _generated.css marked linguist-generated.

Functional parity verified: compiled `tailwind.css` has the same 378 CSS
variables and byte-identical non-:root rules as before. The only diff is
which of Tailwind's internal `:root,:host` blocks each variable lands in,
which is invisible to the browser.

* build(tokens): wire tokens build into bin/setup

Run `npm install && npm run tokens:build` after bundle so a fresh
checkout reaches a runnable state with one command.

* docs(css): explain @source not exclusion of tokens dir

Adds a comment so future readers know why tokens/ is excluded from
Tailwind's content scanner (utility keys in the JSON would otherwise
be treated as used classes and bypass tree-shaking).

* docs(tokens): add tokens/README

Schema overview, workflow, custom $extensions reference, and a list of
the edge cases the build script handles. Lands as a follow-up commit on
the same branch so reviewers landing on the diff have something to read
before opening sure.tokens.json.

* Update tokens/README.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Guillem Arias Fauste <gariasf@proton.me>

* docs(tokens): swap em-dashes for colons in README

* refactor(tokens): move tokens to design/, build script to bin/

Per PR review feedback (jjmata):
- tokens/ → design/tokens/ — top-level design/ namespace leaves room for
  future design assets (Figma exports, design docs, etc.) without
  cluttering the repo root.
- tools/tokens/build.mjs → bin/tokens.mjs — keeps all developer-facing
  scripts in one place (bin/) regardless of language.

Path references updated in:
- bin/tokens.mjs (TOKENS / OUT / generated header)
- package.json (tokens:build, tokens:check)
- app/assets/tailwind/application.css (@source not directive)
- app/assets/tailwind/sure-design-system.css (comment)
- app/assets/tailwind/sure-design-system/_generated.css (regenerated)
- design/tokens/README.md (workflow examples)

bin/tokens.mjs gains a +x bit. Tailwind compile verified.

* docs(tokens): normalize README paths to repo-root style

Files section was mixing relative-to-README paths (`../../bin/...`)
with repo-root paths (`design/tokens/...`) used elsewhere in the same
README. Switching everything to repo-root style for consistency.

* fix(tokens): validate {ref} placeholders against the known token set

CodeRabbit caught: resolveTemplate() and refToClass() would happily emit
var(--foo-bar) or bg-foo-bar for any {foo.bar} input, so a typo in
design/tokens/sure.tokens.json would silently ship broken CSS.

Now build() pre-computes the set of valid token paths from the walker,
and resolveTemplate() / refToClass() throw a clean "[tokens] Unknown
token reference ..." error when a placeholder doesn't match. Top-level
catch surfaces just the message and exits 1, no Node stack trace noise.

Smoke-tested both directions:
- Valid JSON: builds.
- {color.gray.NONEXISTENT|5%}: fails with clear message, exit 1.

* docs(tokens): humanize README prose

* One more refenrece to `maybe-design-system`

---------

Signed-off-by: Guillem Arias Fauste <gariasf@proton.me>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
v0.7.1-alpha.1
2026-05-01 14:46:33 +02:00
Will Wilson
2cff2065eb fix: use ProviderLoader for AuthConfig.sso_providers when DB providers enabled\n\nAuthConfig.sso_providers only read from YAML config, so self-hosted\nsetups with DB-configured SSO providers (e.g. Authentik via admin UI)\nhad no SSO button on the login page.\n\nWire it to ProviderLoader.load_providers when FeatureFlags.db_sso_providers?\nis true, falling back to YAML config otherwise. (#1614) 2026-05-01 13:31:32 +02:00
ghost
fbdcfdcab7 fix(imports): preserve Sure opening balance history (#1595) 2026-05-01 12:24:41 +02:00
Juan José Mata
7dc9afe5c4 Add KiloClaw barge 2026-05-01 01:58:37 +02:00
Guillem Arias Fauste
5220bd527b fix(budgets): stop auto-matched transfers leaking into category cards (#1059) (#1588)
Refs #1059.

When you auto-match a $500 expense from your checking account against
the matching deposit on your credit card, the resulting transfer pair
was leaving traces in the per-card "Recent transactions" list under
each budget category card, even though the aggregate
`Budget#actual_spending` (via `IncomeStatement`) already excluded
`BUDGET_EXCLUDED_KINDS` (funds_movement / one_time / cc_payment) from
the totals. The user saw $X under the card while the totals showed
$X less.

Fix: extend the same exclusion to the drilldown list. The aggregate
and the list now agree.

```ruby
# app/controllers/budget_categories_controller.rb
@recent_transactions = @budget.transactions
                              .where.not(transactions: { kind: Transaction::BUDGET_EXCLUDED_KINDS })
```

`loan_payment` and `investment_contribution` are intentionally NOT in
`BUDGET_EXCLUDED_KINDS`, so those transfers still appear (they are
budget-tracked).

What this PR does NOT do:

  - It does not clear the matched transactions' `category_id` in the
    matcher itself. An earlier draft of this PR did, but codex
    correctly flagged that doing so causes data loss when a user
    rejects an incorrect auto-match: `Transfer#reject!` resets `kind`
    to `standard` but does not restore the previously-cleared
    category, permanently dropping the user's original
    categorisation. The controller filter alone is sufficient to fix
    the user-visible bug, and the inconsistency between
    `kind = funds_movement` and a retained category is harmless because
    every relevant view filters one or the other.

  - The mortgage scenario in #1059 (a `loan_payment` match showing as
    "Uncategorised" in the budget) isn't a leak; it is a missing
    feature. The matcher doesn't auto-assign a category to
    `loan_payment` rows the way #924 does for
    `investment_contribution`. The natural follow-up is a parallel
    `loan_payments_category` plus matcher / import-adapter
    auto-assignment, which deserves a maintainer signoff first.

Tests:

  - `BudgetCategoriesControllerTest#show drilldown excludes
    BUDGET_EXCLUDED_KINDS transfers from recent transactions`: a
    matched depository <-> CC pair does not appear in the
    Uncategorised drilldown after the matcher runs.
  - `BudgetCategoriesControllerTest#show drilldown still lists
    loan_payment transfers (intentionally budget-tracked)`: a matched
    depository <-> loan pair stays visible in the drilldown.

Suite: 3239 / 0 / 0 / 24 on the latest upstream/main. Lint clean.
2026-05-01 00:59:48 +02:00
ghost
072f92c715 fix(imports): preserve account status from backups (#1603) 2026-04-30 23:53:55 +02:00
Sure Admin (bot)
43a500d9fa fix(mobile): stop app icon source overwrite (#1594)
* fix(mobile): use colored iOS app icon source

* Revert "fix(mobile): use colored iOS app icon source"

This reverts commit fc646f6377.

* fix(mobile): provide iOS dark launcher icon variant

* fix(ci): stop overwriting mobile icon source for TestFlight

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-04-30 19:32:55 +02:00
wps260
c91b730122 Performance improvements in balance sync cache (#1581)
* Performance improvements in balance sync cache

Balance::SyncCache#converted_holdings called account.holdings.map { |h| h.dup }
which duplicated every holding record into a new ActiveRecord object, converted
its currency, and stored the full object in a holdings_by_date array hash.
For an investment account with years of history this allocates 100,000+
AR objects on every sync - one per holding row - creating proportional GC
pressure that scaled with account age.

The only consumer of get_holdings(date) was BaseCalculator#holdings_value_for_date,
which immediately discarded the objects after calling .sum(&:amount). The
individual holding objects were never accessed for any other attribute.

Replace the dup-and-group approach with a single aggregation pass that stores
only the per-date sum:

  holdings_value_by_date: account.holdings.each_with_object(Hash.new(0)) do |h, totals|
    converted = Money.new(h.amount, h.currency).exchange_to(account.currency, date: h.date).amount
    totals[h.date] += converted
  end

Interface change: get_holdings(date) -> get_holdings_value(date) returns a
Numeric directly rather than an Array. BaseCalculator#holdings_value_for_date
is updated accordingly, and its own per-date memoization layer is removed
since holdings_value_by_date is already fully memoized at the SyncCache level.

* fall back to 1:1 rate in SyncCache when holding exchange rate is missing; update tests to use investment class
2026-04-29 21:47:01 +02:00
francmart514
dfe1977938 Fix invite code being consumed on failed registration in invite-only mode (#1576)
* Fix:Bug:invite-code-inproper-burn

* added docstring according to coderabbitai warning

* updated feedback for merge
2026-04-29 18:54:23 +02:00
maverick
ee352dada4 Added ability to bulk-edit transaction names for multiple selected transactions (#1553)
* Added ability to bulk-edit transaction names for multiple selected transactions.

* Added ability to bulk-edit transaction names for multiple selected transactions.

* Added ability to bulk-edit transaction names for multiple selected transactions.

* Lint, minimize changes

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-04-29 18:35:00 +02:00
Xing Hong
475dbbfb8d fix: Enable and persist notes and tags on split child transactions (#1535) (#1552)
* fix: enable and persist notes on split child transactions (#1535)

* fix: enable tags on split child transactions and new tests for split child notes + tags

* Update app/components/DS/dialog_controller.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Xing Hong <39619359+xingxing21@users.noreply.github.com>

* fix(transactions): only stream notes frame when notes params are submitted

* fix(transactions): address PR review issues in notes stream and tests

---------

Signed-off-by: Xing Hong <39619359+xingxing21@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 18:17:36 +02:00
Juan José Mata
d49250826b Improve error handling with user-friendly messages and classification (#1591)
* Improve chat LLM error messages

* Fix chat visibility regression in tests

* Harden chat error handling for review feedback

* Fix rubocop private method indentation

* Fix nil presentable_error_message, i18n strings, bare rescue

- Guard `presentable_error_message` with `return nil if error.blank?` so
  chats with no error return nil instead of the fallback string; this
  prevents the API serialisers from emitting a spurious error message and
  stops the mobile polling guard from firing on every successful chat
- Move all hardcoded user-facing error strings into
  config/locales/models/chat/en.yml and reference them via I18n.t()
- Replace bare `rescue` in `error_message_for` with `rescue StandardError`
  to avoid swallowing system-level exceptions
- Update tests to reference I18n keys instead of raw strings, and add
  tests for the nil-error case and the unrecognized-error fallback

https://claude.ai/code/session_01YFMjEds5WVyKPL42xBqMCX

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-29 17:51:06 +02:00
Will Wilson
ad23820a2e fix: change postal_code column from integer to string (#1585)
* fix: change postal_code column from integer to string

Allows non-numeric postal codes such as UK format (e.g. SW1A 2AA).
The integer column was silently dropping any alphanumeric input.

The migration is marked irreversible — once alphanumeric postal codes
exist, they cannot be safely cast back to integer.

* fix: update schema.rb, quote fixture postal_code, and add alphanumeric test

* fix: use conventional migration timestamp
2026-04-29 15:30:04 +02:00
Thiago Diniz da Silveira
c9f9e04071 fix: currency being ignored for properties (#1556)
* fix: add property with different currency is not updating

* fix: add property with different currency test

* fix: code review

* fix: code review
2026-04-29 13:47:32 +02:00
Sure Admin (bot)
9b2c80768c fix: run TestFlight upload on v* tags (#1582)
* fix(ci): trigger TestFlight upload from v tags

* fix(ci): archive iOS app with manual signing

* fix(ci): avoid applying iOS profile to CocoaPods targets

* fix(ci): apply provisioning profile to Runner target only

* fix(ci): fix Runner signing patch script path

* fix(ci): place App Store auth key where altool expects it

* fix(ci): build TestFlight uploads with Xcode 26.4 on macOS 26

* fix(ci): generate unique iOS build number for TestFlight uploads

* fix(ci): read iOS marketing version from .sure-version

* refactor(ci): remove hardcoded iOS team id anchor

* fix(ci): strip prerelease suffix from iOS marketing version

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-04-29 13:26:16 +02:00
GermanDZ
7c14c80444 Fix SimpleFIN inverting Loan account balances (#1574)
* Fix SimpleFIN inverting Loan account balances

SimplefinAccount::Processor#process_account! routes every liability
through OverpaymentAnalyzer + normalize_liability_balance. That path
is built around credit-like liabilities, where transaction history
distinguishes debt vs. credit. For a Loan account with only the
opening anchor (no payment history), the analyzer returns :unknown
and the fallback negates the observed value:

    def normalize_liability_balance(observed, bal, avail)
      ...
      -observed
    end

That's wrong for loans: the bank reports the principal outstanding
as a positive number from its own books. Negating it stores the loan
balance as negative, so BalanceSheet#net_worth = assets - liabilities
ends up _adding_ the loan instead of subtracting it (off by 2× the
loan amount). Example with a hypothetical mortgage:

  raw_balance       = 100000.00  (positive — bank's own report)
  Sure stored       = -100000.00 (negated by the fallback)
  Net worth shown   = inflated by 2 × 100000

Short-circuit Loan accountables straight to observed.abs and skip the
analyzer/fallback entirely. Loans don't have credit-vs-debt
ambiguity — if the loan is paid off the balance is 0, not negative.
Credit cards still go through the existing heuristic.

* Add observability for the SimpleFIN loan sign branch

Mirrors the logging + Sentry breadcrumb the credit-card branches emit
when the OverpaymentAnalyzer classifies as :credit / :debt, so the
loan short-circuit shows up in production traces too. Per CodeRabbit
review on #1574.

* Test that positive bank-reported loan balances are preserved

The existing "inverts negative balance for loan liabilities" test only
covers a bank that reports the loan as negative — both the old (buggy)
fallback and the new short-circuit produce the same +50000 there, which
is why the inversion bug went undetected. Add a sibling test where the
bank reports +50000 (the common mortgage convention); under the old
code that became -50000 and inflated net worth.

* Redact monetary amounts from SimpleFIN liability info logs

Move raw observed/stored amounts and metric totals from `Rails.logger.info`
and `Sentry.add_breadcrumb` payloads to a `Rails.logger.debug` line.
The info-level message and breadcrumb data now carry only identifiers
(`sfa_id`) plus the classification (`loan` / `credit` / `debt` /
`unknown`) and `tx_count`, so log aggregators and Sentry no longer
receive raw monetary values for any of the four liability branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:00:38 +02:00
Juan José Mata
d96e551c01 Move versioning from Rails initializer to .sure-version file (#1587)
* Extract version to .sure-version file and add Sentry release tracking

Move the hardcoded version string to a `.sure-version` file at the repo
root so it can be read by both the Rails version initializer and other
tooling. Configure `config.release` in the Sentry initializer to tag
errors with the app version.

https://claude.ai/code/session_01KfUgF42B3exoU2vpErqJyW

* Use .sure-version as single source of truth in Helm CI workflows

Update chart-ci, chart-release, and publish workflows to read the app
version from .sure-version instead of regex-parsing version.rb. The
pre-release bump job now writes directly to .sure-version and stages it
for commit.

https://claude.ai/code/session_01KfUgF42B3exoU2vpErqJyW

* Guard empty .sure-version fallback

* fix: sync Helm chart version with .sure-version

* Moving on to `v0.7.1-alpha.*` with this

* Defensive rescue

* Getting fancy with versions now

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-04-29 11:36:04 +02:00
wps260
33bb1f524d fix: skip DemoFamilyRefreshJob in self-hosted mode (#1578)
DemoFamilyRefreshJob runs daily via cron and recreates a demo family from
scratch. This is a managed-mode-only feature (the public hosted instance),
but the job had no app_mode guard, so self-hosted instances were also
creating and refreshing a demo family every day.

This results in every self-host instance of sure getting the demo family
with the well-known credentials created.

It may be worth considering a separate one-time fix that deactivates the
demo family users, and/or entries in the release notes or a separate
security advisories alerting users that they need to deactivate the demo
users.
2026-04-28 18:01:15 +02:00
github-actions[bot]
3960582fd2 Bump version to next iteration after v0.7.0-alpha.7 release 2026-04-27 19:31:48 +00:00
github-actions[bot]
0d5dcfaa1b Bump version to next iteration after v0.7.0-alpha.7 release 2026-04-27 18:21:29 +00:00
GermanDZ
c5503320af Fix IndexaCapital sync, account setup, and balance/type bugs (#1562)
* Add missing IndexaCapitalItem::SyncCompleteEvent

Syncable#sync_broadcaster instantiates self.class::SyncCompleteEvent,
which is implemented for every other provider (Plaid, Lunchflow,
Mercury, etc.) but was missing for IndexaCapitalItem. The error was
swallowed by Sync#perform_post_sync's rescue, so syncs appeared to
succeed but post-sync UI broadcasts never fired:

  Error performing post-sync for IndexaCapitalItem (...):
  uninitialized constant IndexaCapitalItem::SyncCompleteEvent

This adds the class, modeled on LunchflowItem::SyncCompleteEvent,
restoring per-account and per-item Turbo broadcasts after Indexa
Capital syncs.

* Fix IndexaCapital account setup never creating accounts

complete_account_setup read params[:accounts], but the form in
setup_accounts.html.erb submits account_ids[] (array) and
sync_start_dates[<id>] (hash). The hash was always empty, so every
submit hit the empty-config branch and bounced back with
"No accounts to set up." — accounts were never created.

The controller also branched on config[:account_type] / config[:subtype]
even though the form has no account-type picker (Indexa Capital is an
investment-only broker). Rewrote complete_account_setup to consume the
form's actual params and infer the accountable type as Investment from
indexa_capital_account.account_type.

* Fix IndexaCapital balance double-count and account type

Two more issues in the IndexaCapital flow that surfaced once accounts
could actually be created (see prior commit):

1. Accountable type was inferred from indexa_capital_account.account_type
   ("mutual" / "pension"), but infer_accountable_type doesn't recognize
   those values and falls through to "Depository". The result: every
   imported Indexa account showed up as a Cash depository account
   instead of an Investment account, hiding holdings/trades surfaces.
   Indexa Capital is investment-only, so hard-code the accountable
   type to Investment.

2. Account::Processor#calculate_total_balance summed every row in
   raw_holdings_payload. Indexa returns a time series — one row per
   security per date — so the naive sum double-counts (observed:
   reported €91,633 became stored balance €180,039). Trust the API's
   current_balance when present, and if we have to fall back to a
   computed total, dedupe by instrument and take the latest-dated
   amount per security.

* Fix IndexaCapital holdings reflecting oldest snapshot per security

HoldingsProcessor#process iterated every row in raw_holdings_payload.
Indexa returns a time series (many rows per security across dates),
and each iteration upserts the same (account, security, today) holding
row, so the LAST row processed wins. The payload is ordered with
newer dates first, so the last row processed is the OLDEST snapshot —
the holdings shown in the UI reflected tiny early positions instead
of the current ones (e.g. 3.8 shares of US 500 stored vs 62.34 actual).

Reduce the payload to one row per security (latest date) before
processing. The cost-basis update is now also driven by the latest
snapshot for the same reason.

* Fix IndexaCapital holdings using per-lot detail instead of totals

Importer#normalize_holdings_response read data[:fiscal_results], which
the Indexa API returns as per-tax-lot detail — many rows per security
covering each subscription_date, plus virtual sell/buy rows generated
by rebalances. Iterating it produced wildly wrong stored holdings:
e.g. 9.61 shares stored for Vanguard US 500 vs 62.34 actual; total
weights summed to ~10% instead of 100%.

The same response also includes data[:total_fiscal_results] — one
aggregated row per security with current titles/amount/cost matching
the Indexa UI and the user-downloadable positions CSV. Prefer it,
falling back to the per-lot field only when the totals are absent.

* Address CodeRabbit review on IndexaCapital fixes

Four review items, all fixed:

* Share instrument-key extraction
  HoldingsProcessor#extract_ticker and Processor#calculate_holdings_value
  used different fallback orders (one looked at :isin, the other at
  :isin_code), so they could disagree on which rows referred to the same
  security. Moved a single extract_instrument_key helper into
  IndexaCapitalAccount::DataHelpers and routed both callers through it.

* Simplify Processor#calculate_holdings_value
  The date-based dedupe was a workaround for the bug already fixed in
  the importer (which now stores total_fiscal_results — one row per
  security). Replaced the date comparison with a per-security map
  populated via the shared key extractor. Same end result, fewer
  moving parts, no fragile string-date comparison.

* Drop dead config key passed to create_account_from_indexa_capital
  create_account_from_indexa_capital only reads :subtype and :balance
  from its config arg. Passing :sync_start_date there was inert.

* Don't mark created accounts as skipped on post-create errors
  In complete_account_setup, ensure_account_provider! and
  update!(sync_start_date:) ran inside the same begin/rescue as the
  Account.create!. If either raised after the Account row was already
  persisted, control jumped to the rescue with created_count not yet
  incremented and the account was wrongly counted as skipped. Now:
  parse the form-supplied sync_start_date up front (a malformed value
  is silently dropped instead of bubbling out of the loop), bump
  created_count immediately after persisted?, and isolate the post-
  create steps in their own rescue so failures there are logged but
  don't desync the success counter.

* Fall back to /portfolio so pension plans get holdings imported

Indexa's /accounts/{id}/fiscal-results endpoint returns
{fiscal_results: [], total_fiscal_results: []} for pension plan
accounts (e.g. type "pension"). The same positions are exposed via
/accounts/{id}/portfolio in instrument_accounts[].positions[] for
both mutual funds and pensions, so use it as a fallback when
fiscal-results is empty.

The portfolio response uses the same field names HoldingsProcessor
already understands (instrument, titles, price, amount, cost_amount)
plus a derived cost_price (cost_amount / titles) added during
adaptation. No HoldingsProcessor changes needed.

Verified against the user-downloadable "Posiciones" CSV for an
SH71ZPMY pension account: two positions (N5138 Acciones, N5137
Bonos) and balance €8,273.56 match exactly.

* Fix CI: update tests for new IndexaCapital flow + rubocop blank line

* Lint: drop trailing blank line before `end` in
  IndexaCapitalAccount::Processor (Layout/EmptyLinesAroundClassBody).

* Controller test: complete_account_setup#creates was posting
  params: { accounts: { id => { account_type:, subtype: } } } against
  the old controller schema. The new endpoint reads
  params[:account_ids] and infers Investment for Indexa Capital, so
  switch the test to that shape (and update the matching skip-already-
  linked / no-selected-accounts cases).

* Processor test: "updates account balance from holdings value" set
  current_balance: 38905.21 alongside holdings summing to 27093.01
  and asserted the latter wins. After the fix
  (calculate_total_balance prefers the API-reported current_balance
  when present), the API value is the right answer. Renamed to
  "trusts API current_balance over holdings sum when present" and
  added a sibling test that nils current_balance to exercise the
  holdings-sum fallback path explicitly (still asserts 27093.01).

* Wrap account creation+linking in a transaction to avoid orphans

complete_account_setup created the Account row first, incremented
created_count, and only then called ensure_account_provider! / the
sync_start_date update inside an inner rescue. If the link or the
sync_start_date update raised after the Account was already persisted,
control fell into the inner rescue: the orphaned Account row stayed
in the database, the failure was silently logged, and the success
counter was inflated.

Wrap creation, ensure_account_provider!, and the optional
sync_start_date update in a single ActiveRecord::Base.transaction.
Increment created_count only after the transaction commits; on any
exception the outer rescue rolls the whole step into skipped_count
with a clear log line tagged with the indexa_capital_account id.
2026-04-27 18:33:22 +02:00
Xing Hong
cd2a0bffd0 fix(android): remove dead buildscript block and kotlin_version reference (#1567)
* fix(android): remove dead buildscript block and kotlin_version reference

* fix(android): use fully-qualified kotlin plugin id to match settings.gradle
2026-04-27 16:06:19 +02:00
sentry[bot]
1442d01f95 fix(transfers): Set accounts when rendering new form after conversion or argument errors (#1561)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-04-27 15:10:27 +02:00
Juan José Mata
3a4371f809 Add Repobeats analytics image to README
Added a Repobeats analytics image to the README.

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-26 21:49:25 +02:00
Xing Hong
9d10ab3309 Fix budget donut chart hiding center content on segment hover (#1551)
* Fix budget donut chart hiding center content on segment hover

* Preserve segment hover in center unless leaving the unused arc

The unused arc has no interactive link in its center template, so
moving from it into the center content should restore default content(including the Edit Budget link) immediately. All other segments have a DS::Link in their center template that must remain clickable after the cursor moves from the arc into the center.

Pass the D3 datum into the mouseleave handler to read d.data.id, then clear hover when either (a) the departing segment is the unused arc, or (b) the cursor is not heading into contentContainerTarget.
2026-04-26 09:32:20 +02:00
Brian Richard
162caf0e9f fix(localization): update API usage instructions to include product name placeholder (#1555)
* fix(localization): update API usage instructions to include product name placeholder

* Fix: Update show and created views to use dynamic usage_instructions per CodeRabbit

* fix: update usage instructions translation key for API key usage
2026-04-26 09:29:23 +02:00
Juan José Mata
f51f1c317b Add automated Google Play Console upload for mobile releases (#1548)
* Add Google Play upload workflow for mobile releases

* Fix Google Play workflow reuse and track input

* Gate Play upload on AAB artifact availability

* Harden Play release notes input handling
2026-04-24 23:39:00 +02:00
Tristan Katana
9458261249 Upgrade Android Gradle Plugin to 8.9.1 and Gradle to 8.12 (#1547)
Dependencies androidx.browser:1.9.0 and androidx.core:1.17.0 now
require AGP >= 8.9.1. Bumps the Gradle wrapper to 8.12 to satisfy
the AGP compatibility requirement.
2026-04-24 20:00:05 +02:00
Sure Admin (bot)
43460664c4 feat(ci): improve LLM eval visibility in GitHub Actions (#1546)
* feat(ci): improve LLM eval visibility in GitHub Actions

- Add step summary output for each eval run (shows in GH UI)
- Add new 'summarize_evals' job that aggregates results from all matrix runs
- Generate markdown table with accuracy, cost, and duration for all evals
- Add threshold checking (fails workflow if accuracy < 70%)
- Include status icons (/) for quick visual assessment
- Show overall pass/fail status at the end of summary

* Fix LLM eval workflow summary

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-24 11:18:45 +02:00
Tristan Katana
3e36fae751 feat(mobile): lock chat input while bot is responding + 20s timeout (#1538)
* feat(mobile): lock chat input while bot is responding + 20s timeout

- Add _isWaitingForResponse flag to ChatProvider; set in _startPolling,
  cleared in _stopPolling so it covers the full polling lifecycle not
  just the initial HTTP POST
- Add _pollingStartTime + 20s timeout in _pollForUpdates; if the bot
  never responds the flag resets, errorMessage is surfaced, and input
  unlocks automatically
- Gate send button and keyboard shortcut on isSendingMessage ||
  isWaitingForResponse so users cannot queue up multiple messages
  while a response is in flight

(adding an interrupt like with other chat bots would require a larger rewrite of the backend structure)

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

* fix(mobile): make polling timeout measure inactivity not total duration

Reset _pollingStartTime whenever assistant content grows so the 20s
timeout only fires if no new content has arrived in that window.
Prevents cutting off a slow-but-streaming response mid-generation.

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

* fix(mobile): lock input for full polling duration, not just until first chunk

- Add isPolling getter to ChatProvider (true while _pollingTimer is active)
- Gate send button and intent on isPolling in addition to isWaitingForResponse
  so users cannot submit overlapping prompts while a response is still streaming
- Also auto-scroll while polling is active

* Fix chat polling timeout race and send re-entry guard

Polling timeout was evaluated before the network attempt, allowing it
to fire just as a response became ready. Timeout check now runs after
each poll attempt and only when no progress was made; network errors
fall through to the same check instead of silently swallowing the tick.

Added _isSendInFlight boolean to prevent rapid taps from re-entering
_sendMessage() during the async token fetch window before provider
flags are set. Guard is set synchronously at the top of the method and
cleared in a finally block.

* fix(mobile): prevent overlapping polls and empty-placeholder stop

Add _isPollingRequestInFlight guard so Timer.periodic ticks are
skipped if a getChat request is still in flight, preventing stale
results from resetting state out of order.

Fix empty assistant placeholder incorrectly triggering _stopPolling:
stable is only declared when a previously observed length exists and
hasn't grown. An initial empty message keeps polling until content
arrives or the timeout fires.

* fix(mobile): reset _lastAssistantContentLength in _stopPolling

Prevents stale content-length state from a prior polling session
bleeding into the next one.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 21:48:28 +02:00
Copilot
6f195c6c9c Hide nested budget categories in the Budget spent donut (#1544)
* Initial plan

* Hide nested budget categories in spent donut

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/aea0de69-f123-4417-ba31-d08300fb852d

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Harden budget donut segment test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-23 21:28:38 +02:00
Juan José Mata
6fa3be7d87 Trim message length 2026-04-22 10:10:18 +02:00
Roger Saner
b3c88e09f3 Feature: remember value of chart period selector (#1528)
* feat: remember chart period by last selection not user preferences

* feat: schema update

* fix: revert unnecessary parts of schema.rb update

* fix: check period key is valid before setting it

* revert: no database changes and keep the UI setting

* refactor: don't store the default period in the session, just use the user

* fix: migration

The migration uses the User model directly, which loads all current enums
including ui_layout which doesn't exist yet at that point in migration history.
Fix it with raw SQL.

* revert: not relevant to this PR
2026-04-21 19:02:41 +02:00
Alessio Cappa
30481fbc07 fix: create new partial for tabs rendering. Address mobile UI issues. (#1534) 2026-04-21 12:43:37 +02:00
Ellion Blessan
3a5353c97d fix(storage): properly fallback to GCS_KEYFILE when GCS_KEYFILE_JSON is blank (#1531) 2026-04-20 13:17:39 +02:00
Roger Saner
1297078f7e Feature: improve transfer matcher UI copy (#1526)
* refactor: improve UI copy for transaction matcher drawer

* refactor: transfer matcher UI copy

* fix: improvement

* feat: use i18n
2026-04-20 08:17:38 +02:00
Ellion Blessan
9a8b3be0c4 Add Google Cloud Storage Support for Active Storage (#1523)
* feat: add gcs for activestorage service provider

* docs: add gcs keys to .env.example
2026-04-19 22:05:15 +02:00
Alessio Cappa
5339794c50 fix: Table divider display issue on Safari (#1522)
* fix: use different approach that is compatible with Safari to display table dividers

* fix: add right color for dark theme and remove unnecessary class
2026-04-19 21:45:55 +02:00
Juan José Mata
2a708168c4 Fix LLM demo message 2026-04-19 16:50:48 +00:00
Copilot
3199c9b76d Prevent long category labels from overflowing or crowding adjacent controls (#1498)
* Initial plan

* Fix category delete dialog dropdown overflow

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/200da7a4-fd59-4ae4-a709-f631ccf21e8c

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Tighten category deletion regression test

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/200da7a4-fd59-4ae4-a709-f631ccf21e8c

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Fix deletion button text overflow

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/e802e01f-079e-4322-ba03-b222ab5d4b84

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Preserve category menu spacing on mobile

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/74b5dd1e-7935-4356-806a-759bff911930

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Prevent account activity label overlap on mobile

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/e94027d6-e230-44c8-99a1-6e5645bec82b

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Fix wide account activity category overflow

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/4ad79894-2935-47a3-8d37-037e2bd14376

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Linter

* Fix flaky system tests in CI

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/3507447f-363f-4759-807c-c62a2858d270

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Reset system test viewport between tests

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/357a43b1-11c5-49be-972d-0592a37d97b1

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-19 18:40:50 +02:00
David Gil
7f17fbf6da security: sanitize exception messages in v1 API responses (FIX-11) (#1521)
* fix(security): sanitize exception messages in API responses (FIX-11)

Replace raw e.message/error.message interpolations in response bodies
with generic error strings, and log class+message server-side. Prevents
leaking internal exception details (stack traces, SQL fragments, record
data) to API clients.

Covers:
- API v1 accounts, categories (index/show), holdings, sync, trades,
  transactions (index/show/create/update/destroy), valuations
  (show/create/update): replace "Error: #{e.message}" with
  "An unexpected error occurred".
- API v1 auth: device-registration rescue paths now log
  "[Auth] Device registration failed: ..." and respond with
  "Failed to register device".
- WebhooksController#plaid and #plaid_eu: log full error and respond
  with "Invalid webhook".
- Settings::ProvidersController: generic user-facing flash alert,
  detailed log line with error class + message.

Updates providers_controller_test assertion to match sanitized flash.

* fix(security): address CodeRabbit review

Major — partial-commit on device registration failure:
- Strengthened valid_device_info? to also run MobileDevice's model
  validations up-front (device_type inclusion, attribute presence), not
  just a flat "are the keys present?" check. A client that sends a bad
  device_type ("windows", etc.) is now rejected at the API boundary
  BEFORE signup commits any user/family/invite state.
- Wrapped the signup path (user.save + InviteCode.claim + MobileDevice
  upsert + token issuance) in ActiveRecord::Base.transaction. A
  post-save RecordInvalid from device registration (e.g., racing
  uniqueness on device_id) now rolls back the user/invite/family so
  clients don't see a partial-account state.
- Rescue branch logs the exception class + message ("#{e.class} - #{e.message}")
  for better postmortem debugging, matching the providers controller
  pattern.

Nit:
- Tightened providers_controller_test log expectation regex to assert on
  both the exception class name AND the message ("StandardError - Database
  error"), so a regression that drops either still fails the test.

Tests:
- New: "should reject signup with invalid device_type before committing
  any state" — POST /api/v1/auth/signup with device_type="windows"
  returns 400 AND asserts no User, MobileDevice, or Doorkeeper::AccessToken
  row was created.

Note on SSO path (sso_exchange → issue_mobile_tokens, lines 173/225): the
device_info in those flows comes from Rails.cache (populated by an earlier
request that already passed valid_device_info?), so the pre-validation
covers it indirectly. Wrapping the full SSO account creation (user +
invitation + OidcIdentity + issue_mobile_tokens) in one transaction would
be a meaningful architectural cleanup but is out of scope for this
error-hygiene PR — filed it as a mental note for a follow-up.
2026-04-19 18:38:23 +02:00
Copilot
cb842d0d9b Close privacy mode gaps on accounts, budget editing, and account activity (#1495)
* Initial plan

* Hide missed values in privacy mode

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/ba225f77-fcf1-4d79-8f89-da446e77fab6

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Tighten privacy mode test coverage

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/ba225f77-fcf1-4d79-8f89-da446e77fab6

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Polish privacy mode assertions

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/ba225f77-fcf1-4d79-8f89-da446e77fab6

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Refine privacy mode tests

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/ba225f77-fcf1-4d79-8f89-da446e77fab6

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

* Restore budget privacy mode form interactivity

Agent-Logs-Url: https://github.com/we-promise/sure/sessions/f3c51447-290c-421f-9cad-e8ff88c91d2f

Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jjmata <187772+jjmata@users.noreply.github.com>
2026-04-19 18:12:03 +02:00
Sophtron Rocky
b32e9dbc45 Add Sophtron Provider (#596)
* Add Sophtron Provider

* fix syncer test issue

* fix schema  wrong merge

* sync #588

* sync code for #588

* fixed a view issue

* modified by comment

* modified

* modifed

* modified

* modified

* fixed a schema issue

* use global subtypes

* add some locales

* fix a safe_return_to_path

* fix exposing raw exception messages issue

* fix a merged issue

* update schema.rb

* fix a schema issue

* fix some issue

* Update bank sync controller to reflect beta status

Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* Rename settings section title to 'Sophtron (alpha)'

Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* Consistency in alpha/beta for Sophtron

* Good PR suggestions from CodeRabbit

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Signed-off-by: Sophtron Rocky <rocky@sophtron.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: soky srm <sokysrm@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-04-19 11:16:04 +02:00
Roger Saner
5965604359 Feature: AI sidebar hidden by default for members and guests if AI is disabled (#1510)
* feat: new guest and member has a hidden AI sidebar if AI is disabled

* test: show_ai_sidebar state when adding new users

* test: covers guests
2026-04-19 08:25:49 +02:00