* Updated the `highlight_activity_entry_name` method to escape HTML in activity entry names before highlighting. This change prevents potential XSS vulnerabilities and ensures safe rendering of user-generated content.
* Fixed a minor syntax issue in the _split_parent_row.html.erb file by ensuring the closing tag for the link_to helper is properly formatted. This change enhances code readability and maintains consistency in the view structure.
* Introduced a new helper method `highlight_activity_entry_name` to highlight search terms in activity entry names.
* Updated various views to utilize the new highlighting method for improved user experience in displaying relevant entries.
* refactor(design-system): migrate fg-* utilities to text-* and remove namespace
The design system carried two parallel namespaces for foreground colors:
text-* (canonical, ~2,000 uses) and fg-* (32 uses). Most fg-* tokens
were 1:1 duplicates of a text-* counterpart. fg-gray was nearly
identical to text-secondary, with a one-step shade difference in dark
mode.
This PR migrates all 32 usages to their text-* equivalents and removes
the fg-* block from the design tokens. Closes#1606.
Mapping:
- fg-inverse -> text-inverse (20 usages, identical light/dark values)
- fg-gray -> text-secondary (7 usages; light values match, dark is
one step lighter: gray-300 vs gray-400)
- fg-primary -> text-primary (3 usages, identical values)
- fg-subdued -> text-subdued (2 usages, identical values)
The four other fg-* tokens (fg-contrast, fg-primary-variant,
fg-secondary, fg-secondary-variant) had zero usages despite being
defined; they are removed without replacement.
JSON / build:
- design/tokens/sure.tokens.json: $version 1.0.0 -> 2.0.0 (breaking
schema change per the policy added in #1620). 8 fg-* token
definitions removed.
- button-bg-ghost-hover's dark value still references "fg-inverse"
internally; rewritten to "bg-gray-800 text-inverse" so the cleanup
doesn't break that utility.
- _generated.css regenerated. 42 utility blocks now (was 50).
Lookbook tokens preview:
- The Text & foregrounds section dropped its split between text-*
(canonical) and fg-* (legacy). Now a single section listing the
five text-* utilities. The "(legacy)" framing is gone since there's
no legacy left.
README:
- design/tokens/README.md's button-bg-ghost-hover edge-case example
updated to reflect the new "bg-gray-800 text-inverse" dark value.
Visual review needed in dark mode:
- Anywhere icons use the application_helper#icon helper with
color: "default" (most icons in the app). The default class moved
from fg-gray (gray-400 dark) to text-secondary (gray-300 dark), so
default-color icons render slightly lighter in dark mode.
- DS::Buttonish icons in secondary buttons (same shade shift).
- DS::Link icons (same).
- Time series chart axes (same).
- All tooltips, account add flow, settings hostings buttons,
invitations, AI consent, family export, danger-zone buttons --
these used fg-inverse, which is identical to text-inverse, so no
visual change expected.
* fix(design-system): use inverse pair on tooltips for readable dark mode
* fix(lookbook): use semantic tokens in menu preview header text
* fix(lookbook): set text-primary on layout body so previews inherit theme
* fix(design-system): keep shadows dark-toned in dark mode
Inverting shadows to white|8% on dark surfaces produces a halo
effect rather than an elevation cue, and stacks redundantly with
the alpha-white 1px ring already in shadow-border-*.
Switch dark-mode shadows to black at progressively higher alpha
(25%/30%/35%/40%/50% for xs..xl) so they read as actual cast
shadows on near-black surfaces. Surface-tint differences and the
existing alpha-white border ring continue to handle elevation
hierarchy and edge definition.
Approach matches Material 3, Apple HIG, IBM Carbon, Refactoring UI,
and the dark-mode shadows used in Linear/Vercel/Stripe.
* fix(design-system): set text-primary on DS::Dialog element
Browser UA stylesheets apply color: black directly to <dialog>,
which overrides ancestor inheritance even when a body or html
ancestor sets a theme-aware color. Unstyled child content then
renders black regardless of theme.
Setting text-primary on the dialog element itself defeats the UA
override and lets descendants inherit the semantic token.
* fix(lookbook): use shadow css vars in effects preview so dark theme renders
* Revert "fix(design-system): keep shadows dark-toned in dark mode"
This reverts commit 3e9d76ed0b.
* fix(design-system): use opacity-70 instead of text-inverse/70 in value tooltip
The custom @utility text-inverse expands to @apply text-white and
isn't modifier-aware, so text-inverse/70 produced no CSS at all and
the muted labels fell through to inherited color (invisible on the
white pill in dark mode).
Replace with text-inverse + opacity-70. Same visual effect, works
with the existing utility definition.
* feat(api): expose rule run history
* fix(api): address rule run review
* fix(api): complete rule run review
* test(api): cover unauthenticated rule run show
* test(api): align rule run api key helper
* Small Sonnet nit-pick
---------
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
* feat(api): expose family settings
* test(api): assert family settings moniker
* test(api): align family settings api key helper
* fix(api): tighten family settings schema
* fix(chat): persist eager pending assistant message to fix subscribe race
When the LLM replies in ~1-2s the assistant message broadcast could
fire before the client's Turbo stream subscription was established,
leaving the UI stuck on the thinking indicator while the response was
already persisted.
Create the AssistantMessage as `pending` synchronously in
`Chat#ask_assistant_later`, so it is rendered server-side on the chat
show page with a "Thinking ..." inline placeholder. The worker then
finds and updates the existing row via `append_text!`, which flips the
status to `complete` and broadcasts updates against a DOM id that is
already in the page — no race possible. On error, the placeholder is
destroyed if no content streamed, otherwise demoted to `failed`.
Replaces the standalone thinking indicator partial and the
`Assistant::Broadcastable` thinking helpers, both now redundant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chat): bind each assistant job to its specific pending placeholder
Addressing review feedback on #1658:
1. The pending placeholder lookup based on `last pending` was racy —
back-to-back user messages would let one job fill another job's
placeholder. Pass the placeholder through the job arguments
(`AssistantResponseJob.perform_later(user_message, pending)`) so
each turn is bound to its own row.
2. In `Assistant::External#respond_to`, the configured/authorized
guards raise before the local was bound, leaving rescue cleanup
with `nil` and the placeholder visible forever. Bind the parameter
first so cleanup can destroy it on the misconfigured path.
The kwarg defaults to nil so the API#retry path
(`AssistantResponseJob.perform_later(new_message)`) and the model-level
test calls continue to work — they fall back to an in-memory new
message, restoring the original test count assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(chat): i18n the pending assistant placeholder string
Move the hardcoded "Thinking ..." indicator into the locale file per
CLAUDE.md i18n guidelines. With i18n.fallbacks enabled, non-en locales
fall back to English until translated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add thinking label translations
* Fix chat pending assistant expectations
* Fix external assistant pending test lookup
* Scope chat stream targets per chat
* Update message broadcast target tests
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Performance and bug fixes in provider price fetches
Three distinct bugs caused the price provider API to be called unnecessarily
on every investment account sync.
1. Sold securities triggered a provider call on every sync forever
import_security_prices passed end_date: Date.current for every security
ever traded. Security::Price::Importer short-circuits via all_prices_exist?
only when persisted_count == expected_count, where:
expected_count = (clamped_start_date..Date.current).count
This range increases daily, so a security closed two years ago would have
all historical prices in the DB unnecessarily. This also causes any closed
securities to fetch prices daily, forever.
Fix: separate currently-held securities (end_date: Date.current) from
historical-only securities (end_date: last holding date for that security).
Once a closed position's price range is complete through its last holding
date, all_prices_exist? becomes permanently stable and no further provider
calls occur for that security.
"Currently held" is defined as appearing in account.current_holdings, which
returns the most recent holding per security with qty != 0. On the first
sync after a sell, the pre-sale holding is still the most recent, so the
security correctly receives end_date: Date.current for one final sync before
the new qty=0 holding is materialised.
2. Offline securities were not filtered
account.trades.map(&:security) returned all securities regardless of the
offline flag. This results in fetching of securities even if the provider
cannot serve them, or if the user don't want them served for some reason
(eg when there are symbol collisions that causes the wrong prices to be
returned) The global MarketDataImporter correctly uses Security.online;
the account-scoped importer did not.
Fix: Security.online.where(id: all_security_ids) matches the established
contract. Offline IDs still pass through the pluck step but resolve to nil
in the securities hash and are skipped by the existing `next unless security`
guard.
3. N+1 queries for security loading and per-security start dates
- account.trades.map(&:security): triggered one SQL query per trade to load
the security association (N+1).
- first_required_price_date(security): issued 2 DB queries per security -
one MIN(entries.date) and one EXISTS - so S securities = 2S queries.
Fix: replace with batch queries totalling 4 regardless of security count:
- account.current_holdings.pluck(:security_id) - current security IDs
- account.trades.pluck(:security_id).uniq - traded security IDs
- Security.online.where(id: ...) - load all security records at once
- batch_first_required_price_dates: one GROUP BY security_id MIN(entries.date)
over trades, one pluck for provider-holding security IDs, one GROUP BY
security_id MAX(date) over holdings for historical end dates
* fix(market-data-importer): fetch prices through today for reopened positions
Account::Syncer runs import_market_data before materialize_balances, so
current_holdings reflects the last materialized snapshot rather than the
current trade state. If a security was previously sold (stale holdings show
qty=0) and then repurchased in the same sync cycle, it landed in
historical_ids and had its end_date capped at the old last_holding_date.
This caused all_prices_exist? to short-circuit, skipping the price fetch
through today, and leaving the forthcoming holding materialization without
a price for the repurchase period.
Fix: compare the latest trade date against the last holding date for each
historical security. If the trade is newer, the position was reopened before
holdings were rematerialized; treat end_date as Date.current for that sync.
The cap still applies on subsequent syncs once materialize_balances has
updated the holdings table.
Adds a regression test covering the repurchase scenario.
* hoist account.start_date out of per-security loop
Account#start_date issues SELECT MIN(date) FROM entries on every call.
Inside batch_first_required_price_dates it was called up to twice per
security (holding_date assignment + fallback), producing up to 2N extra
queries for an account with N provider-held securities.
Cache the result in account_start_date before the loop.
* assert offline securities are skipped
Adds a regression test verifying that Account::MarketDataImporter never
calls fetch_security_prices for a security with offline: true, covering
the Security.online filter on line 54 of the importer.
Sure uses Tailwind v4 with the design system tokens but several views
still carried Bootstrap-style class names that don't render anything
because no Bootstrap stylesheet is loaded. They're effectively dead
markup.
Replacements:
- text-muted, text-muted-foreground -> text-subdued
- bg-light -> bg-surface
- font-italic -> italic
- text-uppercase -> uppercase
- font-weight-bold -> font-bold
Touched files:
- app/views/doorkeeper/applications/_form.html.erb
- app/views/doorkeeper/applications/show.html.erb
- app/views/pages/privacy.html.erb
- app/views/pages/terms.html.erb
- app/views/pages/redis_configuration_error.html.erb
- app/views/settings/providers/_mercury_panel.html.erb
Also tightening application.css:
- The .hw-combobox__label rule used raw text-gray-500 / text-gray-400
via @apply. Now uses the text-secondary / text-subdued tokens so the
combobox label responds to the theme.
- Custom scrollbar thumbs in .windows and .scrollbar used hardcoded
#d6d6d6 / #a6a6a6 hex values. Now reference var(--color-gray-300) /
var(--color-gray-400). Slight color shift (the hex values were close
to but not identical to those tokens), so this needs a quick visual
check.
And reports/print.html.erb had four <span style="color: #666"> elements
on the metric cards. Replaced with class="text-secondary" merged into
the existing tufte-metric-card-change class, so print uses the same
secondary-text color the rest of the app uses.
prose.css already overrides <strong> and the heading family for dark
mode, but inline <code> was missing the same treatment. The Tailwind
Typography plugin's default code color (designed for the light prose
theme) fell through unchanged in dark mode, producing a near-black
foreground on the dark page background. URLs and other technical
references rendered as <code> were effectively invisible.
Adding a third override block in the same shape as the existing two:
white text on a gray-800 chip in dark mode. Light mode keeps the
plugin defaults.
Spotted on the Mercury settings panel while reviewing #1621, but the
bug applies to every .prose consumer (AI chat, GitHub release notes,
dashboard prose blocks, etc.).
Closes#1624.
* feat(api): expose rule export endpoints
* fix(api): tighten rule export contracts
* fix(api): document balance sheet auth errors
* test(api): align rule API key fixtures
* Update docs/api/openapi.yaml
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
* Quick win
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* 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>
* 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.
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.
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.
* 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>
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.
* 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
* 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>
* 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>
* 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
* 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>