Commit Graph
3134 Commits
Author SHA1 Message Date
Guillem Arias Fauste 0eae9b2f53 fix(goals): keep the projection chart's saved line inside the plot (#2971)
The y-domain was built from the target, the projection, the required path
and today's balance — everything except the series actually drawn across the
plot. A goal whose linked account had ever held more than the target mapped
that history above the top of the scale, and with no clip region the line
painted straight out of the plot and over the legend until the SVG edge cut
it off.

Two changes.

The domain now accounts for the saved history, with 5% headroom so the peak's
stroke isn't shaved in half. It is not allowed to follow that history without
limit, though: every value the chart reasons about sits at or below the goal's
own scale, so headroom above it buys nothing but taller spikes. A goal funded
from a current account sees the whole balance as saved, and a €700 target
against a salary landing and clearing is an order of magnitude apart — scaled
to that peak, the target and both projection lines collapse into the floor and
the chart stops answering "am I on track". Past twice the goal's scale the
history is clipped instead, which keeps the target line at or above half the
plot height.

The clip region is the structural half. Series paths and the hover dot are
confined to the plot box, so anything outside the domain — a peak past the
ceiling, or a linked current account that went overdrawn and dipped below
zero — stops at the edge instead of painting over the legend above or the
date axis below. Clipped peaks stay readable: the tooltip still reports their
real amount.

Behaviour by how far the history overshoots the goal's scale:

  0.6x  ->  domain 1050, no clipping
  1.4x  ->  domain 1470, no clipping (autoscaled)
  2.0x  ->  domain 2100, no clipping (at the ceiling)
  8.0x  ->  domain 2100, clipped inside the plot
2026-08-09 02:05:24 +02:00
Brandon WolfandClaude Fable 5 eb928ad1fe fix(settings): give API key copy buttons success feedback (#2948)
The key-reveal partial binds clipboard#copy on a DS::Button but never
set copied-text-value, so the controller's flashLabel path bailed out
and copying gave no visible confirmation. Add the value (mirroring the
MCP copy button fix from #2314) so the button label flashes "Copied!".

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 20:34:54 +02:00
Kenrick Tandrian 6148cd4639 fix(pages): set breadcrumbs for changelog and feedback pages (#2889)
* fix(app): set breadcrumbs for changelog and feedback pages

* feat(test): add test to assert breadcrumbs

* fix(test): remove changes

* feat(app): update breadcrumbs to use semantic nav element

* feat(test): add breadcrumb assertions to changelog and feedback pages

* fix(app): replace breadcrumb nav element with div containing data-breadcrumbs attribute
2026-08-07 07:49:52 +02:00
Sure Admin (bot) d5039fb5de fix(ci): avoid bumping prerelease versions on PR branches (#2886)
* fix(ci): avoid bumping prerelease versions on PR branches

* fix(ci): quote prerelease tag source branch lookup
2026-08-06 23:08:01 +02:00
William Wei MingandCursor 00d11dd77c fix(ui): wrap redbark disclosure summary in w-full (#2919)
Satisfy DS Drift Patrol Rule 4 for DS::Disclosure(variant: :card)
summary content so justify-between rows stretch across the card.

Closes #2912

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 20:18:28 +02:00
William Wei MingandCursor 62fd47def9 Add “Is not equal to” operator for transaction amount rules (#2922)
* Add not-equal operator for transaction amount rules

Enable excluding a specific amount in rule conditions without
needing paired greater/less than workarounds (#2882).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Strengthen amount not-equal absolute-value coverage

Include a -100 transaction so != 100 proves both signed amounts are excluded.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:55:15 +02:00
William Wei MingandCursor 35c0b08f11 Add tags support for transfer transactions (#2921)
* Add tags support for transfer transactions

Expose TagSelect on transfer create/edit so users can classify
fund movements; apply the same family-scoped tags to both sides.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Require annotate permission on both transfer sides for tags

Prevent tagging a read-only destination transaction when the user
only has write access on the outflow account.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore transfer tag selections on create form errors

* Localize transfer create validation error messages

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:47:34 +02:00
sure-admin f4ace12177 Allow PWA assets without forgery checks 2026-08-04 23:10:36 +00:00
github-actions[bot]github-actions[bot] <github-actions[bot]@users.noreply.github.com>Juan José MataJuan José Mata
84f05e36c4 Bump version to next iteration after v0.7.4-alpha.2 release (#2916)
* Bump version to next iteration after v0.7.4-alpha.2 release

* alpha.3

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-08-05 01:03:20 +02:00
sure-admin a043ac9eef Raise iOS minimum deployment target 2026-08-04 22:57:50 +00:00
sentry[bot]sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>sure-admin
521946b9d6 fix(messages): handle chat not found during message creation (#2896)
* fix(messages): handle chat not found during message creation

* test(messages): cover missing chat race

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-08-05 00:57:43 +02:00
Juan José Mata 705878807b Version bump 2026-08-04 15:40:19 -07:00
UnamedRusandClaude Opus 4.8 8c52e20906 fix(binance): correct base_url for futures api endpoint, start_time param (#2839)
* Update binance.rb

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>

* Update binance.rb

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>

* Update binance.rb

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>

* Fix parameter naming for get_spot_trades and get_futures_trades

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>

* Update processor_test.rb

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>

* reset startTime after fromId

* Fix Binance trade sync param conflict with windowed initial fetch

Binance rejects fromId combined with startTime/endTime, and an unbounded
startTime (default sync ~1yr) exceeds the window cap (spot 24h, futures 7d),
so the initial sync could fail or miss trades.

Split fetch_new_trades into two non-mixing paths:
- incremental (cached trades): fromId-only pagination
- initial sync: walk forward in fixed windows (24h spot / 7d futures),
  clamped to the 6-month futures lookback

Add endTime param to get_spot_trades/get_futures_trades. Add tests covering
multi-window initial sync and multi-page fromId pagination.

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

---------

Signed-off-by: UnamedRus <UnamedRus@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 23:55:18 +02:00
efb7cc3935 Tooling for the wealth + tax agent harness (#2848)
* Expose the Statement Vault to external agents over MCP

A user wants to manage patrimonial history — a document-backed record of a
family's wealth where every figure traces back to the statement it came from —
by pointing an external agent harness at Sure. That model belongs in the
harness, not in Sure: it needs numbered build deltas, golden tests and closed
periods that a mutable Postgres row cannot provide.

What Sure was missing was the seam. The Statement Vault already does most of
the work — original bytes retained, SHA-256 dedup, period detection, account
matching with a confidence score, reconciliation against ledger balances, and a
month-by-month coverage map — but it is reachable only from the web UI. An
agent could not archive a document, cite one, or check for gaps.

Adds five preview MCP tools over what already exists, plus a citation grammar
for values the agent writes:

- upload_account_statement, list_account_statements, get_account_statement,
  get_statement_coverage
- record_valuation, whose source citation is parsed rather than trusted:
  ["estimated: "] citation [" (grade: A|B|C)"]. An uncited or free-styled
  value is rejected at the write boundary instead of landing in the ledger
  looking authoritative.

link and reject are deliberately not exposed. Attaching a statement to an
account is the human's decision, and the vault UI is where it is made; the
agent reports the suggested match and stops there.

Assistant.function_classes now takes a user so preview tools stay out of the
default surface. They are hidden from tools/list and not callable by name
without the preference enabled, and the vault tools re-check the manager role
and per-account permissions, since MCP calls never pass through a controller.

Docs: the blueprint this implements, and a guide covering which side owns which
layer, the vocabulary map between the two, the monthly runbook, and the gaps
(non-user holders, non-statement documents, one value per date).

No migrations, no API endpoints, no UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* Address review feedback on the vault MCP tools

Two non-blocking items from the review pass:

Document why get_statement_coverage reads through accessible_by rather than
writable_by. It reports which documents exist and writes nothing, so read
access is the right bar — and tightening it would hide coverage gaps from
people who can already see the figures those gaps sit behind. The comment
exists so a future refactor doesn't "fix" it.

Close the acknowledged verification gap with tests rather than a one-off
manual check. The review noted that nothing proved a real vault payload
serializes cleanly out through tools/call — vault responses are richer than
the other tools' output, with nested account hashes, decimal balances, dates
and a compacted hash. Two integration tests now drive the real /mcp endpoint
end to end against a real AccountStatement: one listing it, one uploading
bytes and reading back the SHA-256. Permanent regression coverage instead of
a smoke test someone has to remember to repeat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* docs(llm-guides): replace patrimonial blueprint with its final revision

Swap the embedded early draft for the authoritative final revision of the
wealth + tax modelling blueprint (MIT © 2026 diegomarino):

- rename the domain vocabulary: patrimonial -> wealth, fiscal -> tax
  (tax_data/, the tax layer, tax_runner)
- add §9.5 (the intel file: shape, generation, and the capture loop)
- tighten worked examples down to placeholders
- add the MIT header; keep the in-repo NOTE block (adapted to the new
  vocabulary) and the filename untouched so cross-links don't break

* docs(llm-guides): align agent-harness guide with blueprint + fix reconcile semantics

Follow the blueprint rename (patrimonial -> wealth, fiscal -> tax,
fiscal_data/ -> tax_data/, "Phase 7 (fiscal layer)" -> "(the tax layer)")
so the two docs stop disagreeing on vocabulary.

Correct the reconciliation mapping, which conflated two different invariants:

- blueprint reconcile-or-abort (§7 pass 3) is parse-integrity (parsed parts
  == the document's own printed total); Sure's reconciliation_checks is
  ledger agreement (statement balances vs the ledger). Sure has no
  parse-integrity check and never aborts.
- opening_balance / closing_balance are user-entered, not auto-extracted, so
  over MCP reconciliation is "unavailable" until a human fills them.
- tolerance differs: blueprint 1.00/account-period vs Sure's fixed 0.01.

State in the ownership table, the invariants section, the vocabulary map and
the monthly runbook that parse-integrity and the abort belong to the harness
extractor.

* Correct the vault tools' reconciliation claims and citation parsing

Review findings from @diegomarino, all verified against the code before
changing anything.

The reconciliation claim was the serious one. get_account_statement told
agents the checks were "the trustworthy part" and returned "the balances read
off it" — but nothing reads balances off a document. MetadataDetector never
touches them and create_from_prepared_upload! never sets them; they are
user-editable fields in the Statement Vault UI. So a statement archived over
MCP always came back with an empty check list, which an agent could easily
read as "the document agrees with the ledger" when it means "nobody has
entered the figures". The description now says so, and the payload carries a
reconciliation_note spelling it out for anything reading only the JSON. Also
noted that these checks are ledger agreement, not parse integrity: nothing
here verifies a document's parts sum to its printed total.

Provenance::Citation had two patterns disagreeing about spacing. GRADE_SUFFIX
allowed "(grade:A)" but FORMAT required exactly one space, so that citation
passed the pre-check and then parsed as ungraded with the grade swallowed into
the text — silently discarding the reliability the caller supplied, which is
the one thing this parser exists to prevent.

list_account_statements downcases content_sha256 before querying. The column
is constrained to lowercase hex, so uppercase input could never match, and an
agent would read the empty result as "not archived" and upload a duplicate.
Its period filters are renamed overlapping_from / overlapping_until, since
they match on overlap and the old names claimed otherwise to anyone reading
the schema without the descriptions. has_more now explains that there is no
cursor and the way forward is a bigger limit or narrower filters.

record_valuation no longer overwrites the entry's notes. Re-recording a date
would destroy a note a person had written there. Nothing is removed now: an
identical citation is a no-op, a changed one is appended, and the trail of
what was cited when survives. Detecting "did this tool write that line?" is
not possible — almost any prose parses as a valid ungraded citation — so the
code does not guess.

Minor: accept urlsafe base64 on upload, and explain in the code why
record_valuation checks the account ACL rather than the vault manager role, so
nobody "tightens" it into the wrong permission later.

Tests cover each: the grade-spacing cases both ways, uppercase SHA lookup,
overlap window boundaries, note preservation and no-stacking, the unavailable
reconciliation note appearing and disappearing, and — per the review — that
the download URL's signed id actually expires, rather than trusting the
description's claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* Repair a bad merge in the MCP controller test

The merge of main spliced the incoming `tools/call executes
update_transaction` test into the middle of the upload round-trip test,
before its closing `end`. That left the file one `end` short, so it did
not parse — taking out both `ci / lint` (Lint/Syntax) and `ci / test_unit`
(the whole file failed to load).

Restores the missing `end`. Both tests are kept as their authors wrote
them; nothing else changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* docs: use wealth history wording (#2885)

* Stop the vault tools promising verification they don't perform

Three findings from the automated review passes, all confirmed against the
code before changing anything.

The download URL was dead on arrival for the caller it was built for. Sure
serves stored files through Active Storage controllers that
config/initializers/active_storage_authorization.rb gates on
`viewable_by?(Current.user)` — a signed-in browser session. An MCP client has
a bearer token and no session, so following the URL would have redirected to
sign-in. Removed it rather than leaving a link that cannot work, and the
description now points at search_family_files or the vault UI.

Coverage called a month `covered` when a document merely existed. An
unreconciled statement is not mismatched, so it took the `covered` branch, and
the payload carried nothing to correct the reading — the same "advertised
verification that never happened" bug fixed last round in
get_account_statement, in a second place. Months now carry their own
reconciliation_status, and the description says covered means presence, not
agreement.

Listing filtered visibility after limiting. Beyond underfilling a page, with
no cursor and a 100-row cap an accessible statement behind enough newer
invisible ones was unreachable. Visibility now lives in the query, mirroring
viewable_by? for a statement manager.

Also: rescue unexpected upload failures into a tool error instead of a raw
exception string, derive the documented size limit from MAX_FILE_SIZE, list
every coverage status in mcp.md, and cover the failed-reconciliation and
base64-normalisation branches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* Keep storage exception detail out of the MCP response

The upload_failed message interpolated the exception text, which crosses out
to an external agent. A storage failure can carry bucket names, object keys,
paths or request details, so the agent now gets a fixed message and the
exception stays in the server log. The test asserts the absence of detail
rather than pinning the leaked string into the contract.

Also fixes a test that did not test what it claimed: the urlsafe-base64 case
used a fixture encoding to plain base64, so it exercised the padding branch
and never the "-_" translation. It now uses content whose encoding contains
both characters and asserts that up front.

Renames "rejects content that decodes to zero bytes" to "rejects blank
content", which is what it actually covers — Base64.strict_encode64("") is
"", which is blank and returns before the decoder runs, so invalid_content is
correct and empty_file is not reachable from this path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

* docs: align wealth blueprint review feedback

* Correct the harness runbook: parse before publishing

The guide told an implementer to archive each document to Sure first and
work from there. That strands them. Sure never returns a document's bytes
over MCP — Active Storage serves stored files only to a signed-in browser
session — and there is no text fallback either, because statements archived
through upload_account_statement never enter the vector store, so
search_family_files cannot see them. A statement in Sure is metadata to an
agent and nothing more.

That blocks exactly three blueprint steps, all of them operating on bank and
broker statements: the extractors, the parts-vs-printed-total check, and the
glyph decoder. Everything else it parses — tax returns, capital accounts,
annual accounts — the harness already holds locally.

So the order inverts: the harness ingests into its own vault, extracts there
with the whole file in reach, and publishes to Sure afterwards. This restores
principle 8 rather than bending it — the recurring pipeline reads from the
canonical store, and treating Sure as canonical forced a re-fetch the
architecture never sanctioned. Both sides hash the same bytes, so the SHA-256
verifies Sure holds the identical document without moving it.

Writes down the two consequences: a statement uploaded straight into Sure's
UI can be known but never parsed (reliability C or PENDING until a copy
reaches the harness), and neither vault backs up the other.

Also drops a stale tools-table row still advertising the 15-minute download
URL removed earlier, corrects get_account_statement's description where it
suggested search_family_files as a fallback it cannot be, and disambiguates
"the vault" in the MCP tool table, which is what misled me in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFDp9HhXDeswadu4cxFojn

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: diegomarino <diegomarino@users.noreply.github.com>
Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com>
2026-08-04 23:33:01 +02:00
William Wei MingandCursor af2ddca4ce Preload transfer counterparty associations on transactions index (#2819)
* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Preload transfer counterparty associations on transactions index

Transfer#categorizable? walks inflow_transaction.entry.account during list
render, which N+1'd transactions, entries, and accounts per transfer row.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Assert transfer rows render in transactions index N+1 test

* Broaden transactions index N+1 SQL matchers for lazy loads

* Drop unused outflow transfer preloads on transactions index

* Treat only equality SQL lookups as N+1 in index test

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 23:21:11 +02:00
Guillem Arias FausteandJuan José Mata 7907a27a55 fix(list-filter): match accented text regardless of typed diacritics (#2814)
* fix(list-filter): match accented text regardless of typed diacritics

"prestecs" didn't match a category literally named "Prèstecs" — the
filter only lowercased before comparing, and lowercasing doesn't strip
diacritics ("è" and "e" are different codepoints).

Normalizes both the typed value and each item's data-filter-name via
NFD decomposition + \p{Diacritic} stripping before comparing, so a
search matches regardless of which side has the accent.

This is one shared Stimulus controller behind every filterable list in
the app (category dropdown, transaction search filters for category/
merchant/tag/account, DS::Select, DS::tag_select, settings preferences,
family merchants merge, split category select) — fixing it here covers
all of them, not just categories.

Verified live: typing "prestecs" and "prèstecs" both correctly filter
down to a category named "Prèstecs" in the transaction categorize
dropdown, out of ~20 sibling categories.

* fix(list-filter): use \p{Mark} instead of \p{Diacritic}

Codex review on this PR: \p{Diacritic} is broader than combining marks
— it also covers standalone characters (ASCII ^ and backtick, the
middle dot, modifier letters like the Hawaiian ʻokina). Since this
controller filters arbitrary user-defined names, a query consisting of
just one of those normalizes to "", and "".includes() matches
everything, so that search silently shows every row instead of
filtering.

Verified in node against the exact characters raised: \p{Mark} leaves
"^", "`", "·", and "ʻ" untouched while still stripping real combining
diacritics (café → cafe, prèstecs → prestecs).

* fix(list-filter): don't match everything when query normalizes to empty

A non-empty query that normalizes away to "" (e.g. a lone combining mark)
previously matched every row, since "".includes("") is true. Only a
genuinely empty raw input should mean "show everything" now.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-08-04 23:20:55 +02:00
Maurício Pólvora 93ba2b8c08 Fix chained assistant tool calls (#2767)
* Fix chained assistant tool calls

* Preserve chained tool context
2026-08-04 23:19:20 +02:00
Will WilsonandClaude Opus 4.8 6cf9999dbe fix(design-system): resolve DS drift patrol findings (#2836) (#2867)
- redbark_items/_redbark_item: render via DS::Disclosure(variant: :card)
  instead of a hand-rolled <details>, matching the lunchflow_items pattern
- replace dead btn/btn--primary/btn--sm classes (defined nowhere) with
  DS::Link in select_existing_account and DS::Button in _redbark_panel
- add functional bg-subdued token (= gray-400, mirroring text-subdued) and
  use it for the money-flow expense legend dots in place of literal
  bg-gray-400, keeping them in sync with the chart's gray expense bars

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 23:12:04 +02:00
AtlasandJuan José Mata 53a87a7645 fix(mcp): assign OAuth clients read_write scope (#2884)
* fix(mcp): assign OAuth clients read_write scope

* fix(mcp): default registered OAuth scope

---------

Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-08-04 23:10:39 +02:00
Guillem Arias Fauste 23e7db4f4d feat(transactions): surface recently-used categories in the category picker (#2829)
* fix(transactions): don't crash the rule-prompt flash when clearing a category

needs_rule_notification? only checked saved_change_to_category_id? and
eligible_for_category_rule?, neither of which accounts for category_id
being nil. Clearing a category (Clear category / entryable_attributes
category_id: nil) satisfies both, so the caller went on to read
transaction.category.name against a nil category and crashed.

A rule prompt only makes sense when a category was assigned, not
cleared, so bail out early when there's no category to build a rule
around.

* feat(transactions): surface recently-used categories in the category picker

Reframes the recency-vs-muscle-memory question as additive, not
either/or: a small "Recent" section pinned above the existing
alphabetical list, which stays exactly where it always was below it.
Precedent for reordering the primary list by frequency (Office's old
adaptive menus, browser-history-style resorting) is a well-known
anti-pattern — position drifts under the user's hand. Every picker
that does recency well (VS Code's command palette, Spotify, Slack's
emoji picker) adds a small separate recent cluster instead.

- Category#last_used_at, touched only in
  TransactionCategoriesController#update — the one place a category is
  actually hand-picked by a person, as opposed to a rule or import
  auto-assigning one.
- Category.recently_used_for(family:, excluding:, limit:) batches the
  family-scoped query; dropdowns_controller excludes the already-
  selected category from the Recent section since it's already pinned
  to the top of the main list.
- "Recent" hides itself the moment a search query is typed — it's a
  pre-search shortcut, not a second copy of search results. Its rows
  are force-hidden (not just filtered) so keyboard nav can't land on a
  row that's invisible only because its ancestor section is hidden.

* fix(categories): address review feedback on recent-categories picker

- Track last_used_at from every manual assignment path (transaction edit
  form, categorization wizard bulk-update, create-and-assign), not just
  the category-picker endpoint. Centralized as Transaction#record_category_usage!,
  called explicitly from each manual controller action rather than wired
  to a blanket after_save callback, since rule/import auto-assignment
  must not count as a "recent" pick.
- Give recent-section rows a distinct DOM id (recent_category_option_<id>)
  from their canonical-list counterpart so aria-activedescendant can't
  resolve to a hidden duplicate during keyboard nav.
- Fix migration to ActiveRecord::Migration[7.2] to match the rest of the repo.
- Materialize @recent_categories with .to_a to avoid a redundant query.
2026-08-04 23:03:12 +02:00
Sure Admin (bot) 248f6a7716 fix: update Claude MCP connector instructions (#2915) 2026-08-04 22:49:01 +02:00
5f0f5ec89d feat(mcp): Add MCP transaction update tool (#2719)
* Add MCP transaction update tool

* Fix MCP transaction authorization

* Ignore Pipelock false positive on SnapTrade token lookup

Pipelock scan-diff flags `token = oauth_refresh_token...` as
"Credential in URL" even though these are ActiveRecord attribute
names, not embedded secrets. Add the established inline ignore.

Co-authored-by: Martin Molcrette <Mart1M@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Martin Molcrette <Mart1M@users.noreply.github.com>
2026-08-01 20:57:43 +02:00
Guillem Arias Fauste 9d3879a859 feat(plan): unify Budgets and Goals under a single Plan tab (#2687)
* feat(plan): unify Budgets and Goals under a single Plan tab

Preview users get one "Plan" nav entry (compass icon) in place of the
separate Budgets and preview-gated Goals items. It fronts a new /plan
hub with two summary cards — this month's budget (spent vs budgeted,
days left, top categories) and active goals (total saved vs targets,
behind/pending counts, per-goal rows) — each drilling into the existing
/budgets and /goals pages, whose breadcrumbs now start Home > Plan.

The two features share a home, not a model: no schema changes, no URL
changes. Users without preview features keep exactly the pre-Plan nav
(Budgets entry, Goals hidden), and /plan falls through to /budgets for
them.

Supporting changes:
- Goal.active_prepared_for: index-style sorted active goals with the
  family-wide pooled-allocations + market-flows injection reused
- Goal::FUNDABLE_ACCOUNT_TYPES and Goal::ACTIVE_DISPLAY_STATUS_RANK
  extracted from GoalsController
- Budget#days_remaining (same day math as suggested_daily_spending)
- Breadcrumbable#plan_breadcrumb_prefix for the conditional Plan crumb
- New BudgetsController web tests (previously untested) + Plans tests

* fix(plan): address review feedback on the Plan hub

- Keep the "All goals" footer link rendered when the family has only
  completed/archived goals — the hub is a preview user's only route to
  the goals index now that the Goals nav entry is gone (Codex P2)
- Replace the two hand-rolled footer button-links with DS::Link
  (variant secondary, full_width, right icon) per DS Drift Patrol
- Fix DS::Link template comparing icon_position against the string
  "right" — the initializer symbolizes it, so right-positioned icons
  never rendered on links (DS::Button already compared symbols);
  existing callers passing icon_position now get the layout they asked for
- Clamp progress-bar percentages to 0..100 instead of capping only the
  upper bound (CodeRabbit)

* refactor(plan): single source of truth for goal loading, sorting, and counts

Addresses jjmata's draft review notes:

- GoalsController#index now builds on the shared Goal loaders instead
  of hand-rolling its own copy: Goal.prepared_for (preloads + family-
  wide backing-math injection, scope-able) and Goal.active_display_sort
  carry the algorithm once; active_prepared_for composes them for the
  hub. The controller-side constant alias is gone
- One definition of "behind pace": Goal#behind_pace? (excludes paused —
  pausing stops the pace clock on purpose). Both the Plan hub summary
  and GoalsController#kpi_payload's behind/needs-this-month figures use
  it, so adjacent pages can't disagree. While there, the kpi on-track
  numerator also excludes paused goals — it was counted against a
  paused-excluding denominator, so the "X of Y" fraction could exceed
  its own total
- BudgetCategory#suggested_daily_spending calls Budget#days_remaining
  instead of keeping an inline copy of the day math
- Per the fat-model convention, the hub's aggregation moved off the
  controller: Budget#top_spending_categories(limit:) and
  Goal.summary_for(goals, currency:)

* fix(plan): move Edit budget/New goal into their own cards

Both actions lived in the hub's shared page header, unlinked to either
card and, on mobile, wrapping above all content before any real data
appeared. Each now lives in its own card's header instead: Edit budget
as a compact icon-only control next to the status pill (only when a
budget exists — the uninitialized state already has its own "Set up"
CTA), New goal as a small outline button next to the goals count (only
once there's a goal to sit beside; the empty state keeps its own CTA).

Also swaps the edit icon from "pencil" to "square-pen" — at the sizes
these header controls render, lucide's plain pencil is a thin diagonal
stroke that reads noticeably smaller than a neighboring bold glyph like
"plus", even in the same size box. square-pen carries more visual mass
and reads clearly at the same footprint.

* fix(plan): match established DS precedent for the card header actions

Edit budget was a bare icon-only button; verified against the app's
own precedent for this exact action (app/views/budgets/_budget_donut.html.erb,
the budget card already shipped on /budgets) and it's a labeled
secondary link with a trailing pencil, not icon-only and not a
three-dot menu. Matched that: DS::Link, variant secondary, size sm,
icon right. New goal gets the same treatment for consistency between
the two cards' header actions, rather than the full-page-scoped
"primary" weight goals/index.html.erb uses for its own create button —
that's calibrated for a whole page's sole CTA, not a compact card.

Adding a labeled button (wider than the bare icon this replaces)
crowded the header row on mobile enough to wrap "This month" onto two
lines and truncate the "· July 2026" meta away entirely. Header rows
now wrap as a whole (flex-wrap) with the title pinned (shrink-0) so
the action cluster drops to its own line instead of squeezing the
title and meta text.

Also drops the hub's footer note ("Budgets cap your spend; goals track
what you're saving toward...") — redundant with the subtitle right
above the cards.

* fix(plan): lead the budget card header with status, not the edit action

On Track/Over/Warning is what a glance at the card wants first; Edit
budget is the secondary action. Swapped their order so status leads
and the edit control trails, gap-2 unchanged.

* fix(plan): put the status pill on the left, next to the title

Meant the left side of the card, not just left of the edit button. On
Track/Over/Warning now sits beside "This month · July 2026" in normal
flow; ml-auto carries only the Edit budget link, alone on the right —
matching the goals card's own left-meta/right-action split ("· 7
active" left, "New goal" right).

* fix(plan): lead Edit budget with its icon, matching same-shape precedent

Wrong axis on the earlier match: _budget_donut's trailing pencil labels
the VALUE itself ("$12,850 ✎"), not a static action. Our button's label
is a static "Edit budget", and that shape takes a leading icon
everywhere else it appears — the categories "Edit" on budgets/show.html.erb
(icon: settings-2) and "Edit split" in transactions/show.html.erb both
lead with their icon. Drops icon_position: :right so it defaults to
left, matching New goal's shape in the sibling card.

* fix(plan): use the divider token for row separators, not border-primary

Traced against the dashboard outflows list (pages/dashboard/_outflows_donut.html.erb),
which renders its row separators via shared/_ruler → border-divider
(border-tertiary: black/8%, white/10%). Our category and goal rows used
border-b border-primary instead (black/15%, white/30%) — 2-3x heavier
than the established row-separator weight elsewhere in the app. Swapped
both to border-divider.

* fix(plan): lift the duplicated card shell into DS::Card

Codex P1: _budget_card.html.erb and _goals_card.html.erb hand-rolled
the identical "bg-container rounded-xl shadow-border-xs p-5 flex
flex-col" shell twice, with no DS:: card primitive to reach for
instead. Extracted a minimal wrapper — content-only, no header/footer
slots — matching what both cards actually need right now; the roadmap
cards (envelopes #2153, retirement #2044) can adopt it too instead of
copying the class string a third time.

Verified pixel-identical in a browser: same classes, same DOM shape,
just rendered through the component.

* fix(plan): batch pace queries before sorting goals

Codex P2: active_display_sort calls goal.status per goal to build the
sort key; Goal#status reaches Goal#pace for any goal with a
target_date, which fired its own Entry.sum(:amount) query per goal.
The /plan hub renders only the first 5 of active_prepared_for's list,
but paid the full O(N) query cost sorting all of them.

Adds Goal.pace_for(family) (account_id => 90-day net inflow), grouped
in one query and injected via inject_backing_math! alongside the
existing pooled_allocations/market_flows pattern. #pace now sums from
that shared map instead of firing its own query — same math, same
90-day window, same exclusions, just computed once per family instead
of once per goal.
2026-08-01 08:51:08 +02:00
Guillem Arias Fauste 59852ca0f3 fix(insights): respect recurring_transactions_disabled in subscription_audit (#2831)
* fix(insights): respect recurring_transactions_disabled in subscription_audit

SubscriptionAuditGenerator queried family.recurring_transactions
directly, so disabling recurring-transaction detection (Settings ->
Recurring Transactions) never stopped already-identified rows from
surfacing "recurring charge overdue" insights on the Insights feed —
the family-wide flag was already checked at both call sites in
IdentifyRecurringTransactionsJob, just not here.

Returning [] early is enough for existing insights to self-clean up:
produced_types is a class-level declaration, so GenerateInsightsJob
still counts subscription_audit as a succeeded type and expires any
insight whose dedup_key wasn't regenerated on the next nightly run.

* docs(insights): document why cash_flow_warning skips the recurring-disabled guard

Answers jjmata's open review question. Unlike SubscriptionAuditGenerator,
recurring transactions here are one input into a broader cash-flow
projection, not the insight's entire subject — so it intentionally
keeps using the last-known identified set rather than gating on
family.recurring_transactions_disabled?. No behavior change.
2026-08-01 08:47:50 +02:00
kai392andClaude Opus 4.8 73aac31f89 fix(exports): include merchants.csv in family data export (#2758)
* fix: include merchants.csv in family data export

The family export ZIP contained CSVs for accounts, transactions, trades,
categories, and rules — but merchants were only present inside the
all.ndjson bulk file, never as a standalone merchants.csv. Meanwhile
merchants can already be imported via CSV (MerchantImport), so backups
were lossy and the import/export cycle was asymmetric.

Add generate_merchants_csv to Family::DataExporter, wired into the
export ZIP, with headers (name,color,website_url) matching exactly what
MerchantImport expects so the exported file round-trips.

Fixes #2736

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

* test: fix merchants CSV round-trip assertion for family fixtures

The round-trip test asserted the target family's total merchant count
grew by exactly 1, but the export legitimately includes every family
merchant — including dylan_family's fixtures — so the import created 4,
failing CI. Scope the count assertion to the merchant under test.

Also assert the imported color now that merchant colors survive a save
(the set_default_color callback only backfills when no valid color is
present), giving the round-trip full name/color/website coverage.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 08:45:14 +02:00
Carlos Lindo ef61466e87 fix(enable-banking): preserve manual card limit when provider reports zero (#2841)
* Preserve manual card limit when provider reports zero

* Test non-positive provider credit limits

* Address credit limit review feedback
2026-08-01 08:44:30 +02:00
Guillem Arias Fauste c40e7f4807 fix(insights): correct the budget card's figure, badge noise and toast a11y (#2799)
* fix(insights): correct the budget card's figure, badge noise and toast a11y

Four defects found while reviewing the insights surfaces for hierarchy.

**The budget_at_risk card's focal figure argued against its own headline.**
`insight_key_figure` returned `budget_spent_pct` for both budget cards, so
"2 categories need attention in your budget" displayed "14% / of budget" —
a reassuring number as the visual focus of a warning. It now leads with the
flagged count ("2 / need attention"); budget_on_track keeps the percentage,
where overall consumption genuinely is the subject.

**The "New" pill carried no information.** Visiting /insights marks every
insight read in one `update_all`, so at first paint the pill was on every
row. On the page it becomes a dot — same signal, without an uppercase
tracked chip stealing weight from the title beside it. In the dashboard
widget it goes entirely: the well's header already counts unread ("New · 3")
and, with three rows, the pill was usually on all of them.

**The undo toast was silent to screen readers.** A card leaves the page via
a Turbo `remove`, which announces nothing, and the toast that explains it
had no live region — unlike its neighbour `_sync_toast`, which sets
`role="status" aria-live="polite"`.

**The undo toast could only be closed with a mouse.** Its close affordance
was a bare `icon "x"` with a click action: not focusable, not named. Now a
real `DS::Button`, matching `_sync_toast`.

The controller test asserting a per-row badge is updated to assert the
header count that replaces it, and to lock in the pill's removal.

* feat(insights): acknowledge instead of dismiss, on both surfaces (#2800)

Two complaints about the insight feed: the close (×) control felt wrong,
and clearing an insight was only possible on /insights — not on the
dashboard widget, which is the surface people actually look at.

**The × was lying.** Dismissal has never been permanent. GenerateInsightsJob
resurfaces a row whose bucketed metadata changes materially "even if the user
had read or dismissed the stale version" (its own comment), and 6 of 8
generators scope dedup_key to a month token, so dismissing July's budget card
says nothing about August's. A destructive-looking control was performing a
non-destructive act. It is now "Got it", and the contract is statable:
acknowledgement covers the numbers you saw; new numbers are a new insight.

No migration. The DB value stays "dismissed" and dismissed_at keeps its name;
only the enum key and the vocabulary the code speaks change, so existing rows
stay hidden and become undoable under an honest label.

**The action pyramid was inverted.** The escape hatch was a chromed icon
button in the card's top-right — the strongest secondary scan position — while
the card's actual purpose ("View budget") was a borderless ghost link under
the body text. Both now sit in a footer strip: the subject action gets the
chrome, acknowledging is quiet labelled text beside it, and the key figure
gets the corner to itself instead of competing with a control.

**The widget can clear its own rows.** Each row gains an acknowledge control,
revealed on pointer hover, on keyboard focus, and shown unconditionally on
touch where there is no hover. No gesture, so the section's drag-to-reorder
handlers are untouched. The row becomes a stretched link plus a sibling
button, because button_to renders a <form> and a form cannot nest in an <a>.

The group is named (group/insight). The dashboard <section> is itself a
`.group` for its header controls, and a bare group-hover: matches any ancestor
group — hovering one row, or the section header, revealed every row's control.

Acknowledging re-renders the well rather than removing a row, so the next
insight is promoted into the freed slot; Insight::FEED_LIMIT is now shared
between the two controllers that render it so they cannot drift. Undo restores
the row on both surfaces, and carries autofocus so it is one keystroke away
after the acknowledged card leaves the DOM.

* fix(insights): guard unacknowledge! against non-acknowledged insights

CodeRabbit, Major: an arbitrary/stale PATCH /unacknowledge (e.g. an old
undo-toast link clicked after GenerateInsightsJob has since expired or
resurrected the insight) could force it back to :read regardless of
its actual current state — including pulling an :expired insight back
into visible view.

Guards the transition to only reverse an actual acknowledgement, per
CodeRabbit's suggested fix.

* test(insights): fix stale dismiss_insight_url route from main merge

main's preview-gate test used the pre-rename dismiss/undismiss route names;
this branch renamed those to acknowledge/unacknowledge earlier.
2026-08-01 08:39:04 +02:00
Gerald 9313ad4cba fix: tolerate Trade Republic Enable Banking pagination/PDNG errors (#392) (#2828)
* fix: tolerate Enable Banking pagination/PDNG errors for Trade Republic

Trade Republic (available via Enable Banking since ~2026-07-22, see #392)
fails to sync with two distinct errors on its own side:

1. The BOOK transaction fetch issues a continuation_key on page 1 that its
   own API then rejects on page 2 as mismatched with transaction_status
   (422 WRONG_REQUEST_PARAMETERS: "transactionStatus in request is not the
   same as in continuationKey"). This previously discarded every page
   already fetched. Once at least one page has succeeded, a validation
   error is now treated as pagination exhausted and the partial result is
   kept instead of raising. A validation error on the very first page still
   propagates as a real failure.

2. The PDNG (pending) fetch is rejected with a plain 400 (:bad_request)
   instead of the 422 (:validation_error) other ASPSPs use for the same
   "transaction status not supported" case. Both error types are now
   treated as "ASPSP doesn't support pending transactions".

Verified against a live Trade Republic connection through Enable Banking.

* fix: surface Enable Banking pagination truncation as a debug log entry

Add a DebugLogEntry.capture call when a mid-pagination validation
error truncates the transaction fetch (e.g. the Trade Republic
continuation_key bug from the previous commit). This follows the
project convention of using DebugLogEntry for support-relevant sync
diagnostics rather than only Rails.logger, so a truncated sync is
visible in /settings/debug instead of only in container logs.

Currently harmless for narrow incremental sync windows (the account
observed in production has under 100 transactions per window, fitting
entirely on page 1), but a wider historical resync could otherwise
lose data past page 1 with no visible indication.

* fix: address CodeRabbit review feedback on PR #2828

- Add a DebugLogEntry when the PDNG fetch is skipped as unsupported,
  matching the pattern already used for pagination truncation — this
  was a partial-degradation case that was previously only visible via
  Rails.logger.
- Replace the OpenStruct#define_singleton_method provider fakes in the
  three new pagination tests with sequenced Mocha stubs
  (expects(...).twice.returns(...).then.raises(...)), per the
  project's "use Mocha for stubs and mocks" guideline.

* fix: don't swallow WRONG_TRANSACTIONS_PERIOD as pagination truncation

The mid-pagination validation-error handling treated any 422 after page
one as "ASPSP rejected the continuation key" and kept the partial result
as a success. WRONG_TRANSACTIONS_PERIOD is a different, real failure (an
invalid date range, already retried once with a corrected date_from at
the provider level) and must still propagate instead of silently
dropping the remaining pages.

Addresses CodeRabbit review feedback on PR #2828.

* fix: address jjmata's review feedback on partial-result asymmetry

- fetch_paginated_transactions now tolerates :bad_request the same way
  it already tolerates :validation_error mid-pagination, matching the
  PDNG-unsupported rescue in fetch_and_store_transactions which already
  accepts both error types. Without this, a :bad_request on PDNG page 2+
  would discard the already-fetched PDNG page 1 instead of keeping it
  like the BOOK path does. Trade Republic only 400s on PDNG page 1
  today, so this was latent, not currently observed.
- Bump the pagination-truncation log (Rails.logger + DebugLogEntry)
  from warn to error: this now discards data for any ASPSP/scenario
  matching the tolerated error types mid-pagination, not just the
  specific Trade Republic case it was written for, so it deserves
  higher visibility.
- Add a regression test for :bad_request interrupting PDNG pagination
  on page 2+, pinning down the now-symmetric behavior with BOOK.
2026-08-01 08:27:59 +02:00
af3100c0a5 fix(insights): keep rolling period labels on month boundaries (#2880)
* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* fix(insights): pin meta-line forward-window test mid-month

Date.current..Date.current+30 is a full calendar month on the 1st of
31-day months (e.g. Aug 1..31), so insight_period_label prefers the
month name over "Next 30 days" and flakes CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix rolling insight period labels on month boundaries

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-08-01 08:10:00 +02:00
Sure Admin (bot) 1e800e2f93 Include uncategorized spending in budget UI (#2877) 2026-08-01 07:50:09 +02:00
Sure Admin (bot) 2bdaa7a171 Bump version after v0.7.3 release (#2879) 2026-08-01 04:46:24 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 708819b1b4 chore(deps): bump msgpack from 1.8.0 to 1.8.2 (#2866)
Bumps [msgpack](https://github.com/msgpack/msgpack-ruby) from 1.8.0 to 1.8.2.
- [Changelog](https://github.com/msgpack/msgpack-ruby/blob/master/ChangeLog)
- [Commits](https://github.com/msgpack/msgpack-ruby/compare/v1.8.0...v1.8.2)

---
updated-dependencies:
- dependency-name: msgpack
  dependency-version: 1.8.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 23:38:46 +02:00
Will WilsonandClaude Opus 4.8 b6cd8437a5 fix(oidc): honor http issuer scheme during OIDC discovery (#2853)
* fix(oidc): honor http issuer scheme during OIDC discovery

Self-hosted IdPs served over plain HTTP (no SSL) failed OIDC login with
"Failed to open TCP connection to <issuer>:443 (Connection refused)".

The openid_connect/swd gems hardcode discovery to HTTPS: SWD.url_builder
defaults to URI::HTTPS, and OpenIDConnect::Discovery::Provider::Config::Resource
drops the issuer's scheme, rebuilding the .well-known URL from host/port only.
So an http:// issuer is upgraded to https:443 and never connects. (This is why
the in-app "Test connection" passes -- it uses Faraday against the raw issuer
URL and never goes through the gem.)

Patch Config::Resource to remember the issuer's scheme and build the discovery
endpoint with URI::HTTP or URI::HTTPS accordingly. Per-request, no global mutable
state, so mixed http/https providers coexist. Only discovery needs patching: the
endpoints it returns are absolute and rack-oauth2 preserves an existing scheme,
so the token/userinfo/jwks calls follow over http automatically.

Verified against openid_connect 2.3.1 / swd 2.0.3.

Fixes #2844

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

* fix(oidc): scheme-aware port and discovery cache key

Address automated review feedback on the OIDC http-discovery patch:

- Preserve explicitly configured non-default ports (http on 443, https on 80)
  by omitting only the scheme's own default port instead of both 80 and 443.
- Override the discovery cache_key to include scheme/port/path (the gem keyed
  on host alone), so an http:// issuer can't reuse an https:// issuer's cached
  metadata on the same host. Latent today (default SWD cache is a no-op) but
  removed to keep the override self-consistent.

Adds regression tests for both cases.

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

* test(oidc): cover port and path as cache-key components

Extend the discovery cache-key test to assert scheme, port, and path each
produce a distinct key on the same host, per review feedback.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 03:24:28 +02:00
Sure Admin (bot) 078c49b35b Stabilize net worth breakdown series test dates (#2863) 2026-07-31 03:19:33 +02:00
Maximus Barbare 4c1bc774e5 Fix SnapTrade account setup and reconnection (#2858)
* Infer SnapTrade account types from categories

* Allow choosing SnapTrade account types

* Reauthorize SnapTrade connections needing attention

* Clear stale SnapTrade reconnect state

* Always schedule SnapTrade reconnect syncs

* Recognize SnapTrade credit card accounts

* Recognize SnapTrade crypto account types

* Schedule SnapTrade syncs after active imports

* Fix SnapTrade account type CI tests

* Normalize SnapTrade card account types
2026-07-31 02:20:03 +02:00
8e6035c62c Wire @kraken_items into the accounts index (#2770)
app/views/accounts/index.html.erb never referenced @kraken_items.
Kraken support was added upstream but left out of the accounts index
in two places: the top-level empty-state condition that decides
whether to render the "empty" partial, and the provider render
section. AccountsController#index also did not assign @kraken_items
at all.

As a result, a family whose only connections are Kraken items saw the
blank empty state instead of their Kraken accounts.

Assign @kraken_items in the controller with the same eager-loading
shape used for the other crypto providers, add @kraken_items.empty?
to the empty-state condition, and render the items in the provider
section in the correct order.

Fixes #2577

Co-authored-by: agentloop <agentloop@localhost>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-07-30 09:12:42 +02:00
pro3958andagentloop 785f9a19c5 fix(accounts): skip digest on sidebar fragment cache (#2776)
The account sidebar fragment renders DS::* view components. Rails' ERB
dependency tracker parses `render DS::Foo.new` as a dynamic render
dependency named DS and inflects it into the nonexistent partial "Ds/D".
When the cache helper computes the template digest, ActionView::Digestor
fails to resolve that partial and logs "Couldn't find template for
digesting: Ds/D" on every sidebar cache miss.

The fragment's cache key is already manually versioned and invalidated
via account_sidebar_tabs_cache_key ("account_sidebar_tabs_v2",
invalidate_on_data_updates: true), so automatic template digesting adds
nothing. Pass skip_digest: true to the cache block so the digestor never
runs for this fragment. Rendered output is unchanged.

Fixes #2516

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:25:21 +02:00
pro3958andagentloop 8a441582f9 fix(layout): persist sidebar collapse preference across reloads (#2764)
The app-layout Stimulus controller read this.userIdValue when saving
sidebar state but never declared a static values block, so Stimulus
did not bind data-app-layout-user-id-value. this.userIdValue was
undefined, and every toggle PATCHed /users/undefined, which could not
update show_sidebar or show_ai_sidebar. The preference was never
stored, so the layout reset on the next load.

Declare static values = { userId: String }. String is correct for
both UUID and integer ids since the value is only interpolated into
the request URL. The layout already renders the attribute and
UsersController#user_params already permits both fields, so the
update now completes.

Fixes #2473

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:20:32 +02:00
pro3958andagentloop 150e659846 fix(tags): return 404 for missing or cross-family tag deletion (#2703)
Tag::DeletionsController looked up the tag and its replacement with
find_by, so a missing id or a tag belonging to another family left
@tag nil. create then called @tag.replace_and_destroy!, raising
NoMethodError and returning a 500 instead of a 404.

Use find in set_tag and set_replacement_tag so an out-of-scope id
raises ActiveRecord::RecordNotFound, which is rendered as 404. The
replacement lookup keeps a presence guard so deleting without a
replacement still works. This mirrors Category::DeletionsController.

Fixes #2469

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:15:50 +02:00
213bb0d6e8 design-system(mobile): polish Dashboard with Sure tokens (#2457)
* feat(mobile): add SureSpacing + SureTypography scale tokens

Introduce hand-authored spacing and type-scale constants mirroring the
Tailwind defaults the web design system relies on, so widgets reference a
named step instead of a raw numeric EdgeInsets/SizedBox/fontSize.

- SureSpacing: xs..huge mapping to Tailwind space-1..space-8 (4..32px).
- SureTypography: xs..xxl mapping to Tailwind text-xs..text-2xl font sizes.

Both are hand-written rather than generated from sure.tokens.json because
spacing and the type ramp come from Tailwind's built-in scale, not the
canonical token file (consistent with the tracker's guidance).

Adopt them in the existing primitives (card padding, button metrics +
gap, chip/segmented/list-group gaps and padding, text-field padding +
label gap). All migrations are value-preserving — each token equals the
literal it replaces — so there is no layout change; off-scale one-offs
(control heights, hairlines, deliberate 14px field padding) stay literal.

flutter analyze: no new issues; full suite (166) green.

* design-system(mobile): polish Dashboard with Sure tokens

Align the dashboard with the Sure design system (no behavior changes).

NetWorthCard: the hero card adopts the canonical Sure card chrome — container
fill, hairline borderSecondary, radiusLg, and the subtle DS shadow (mirroring
SureCard/AccountCard) instead of Material surfaceContainerHighest/outline with
an ad-hoc radius and no elevation. Dividers, the Net Worth label/value, the
Outdated badge, asset/liability totals, and the currency-breakdown sheet all
resolve from the active SureColors palette (brightness-aware).

dashboard_screen.dart: empty/error states use SureButton + palette colors; the
account-type group header badge uses surfaceInset/textSecondary + SureTypography;
the sync success banner and sync/refresh snackbars use palette.success/
palette.destructive; spacing moves onto the SureSpacing scale.

Adds net_worth_card_test.dart asserting the hero card chrome resolves Sure
tokens in light and dark.

Builds on the SureSpacing/SureTypography scale tokens (#2438).

* fix(mobile): readable foreground on tokenized dashboard snackbars + keyed chrome test

Address review feedback on #2457:

- Snackbar contrast: the success/error snackbars switched their background to
  palette.success/palette.destructive but kept a white icon + default white
  text. In dark theme palette.success is a bright green (#32D583), so white was
  low-contrast. Set the icon and text foreground to palette.textInverse, which
  flips with the theme (#FFFFFF light / #171717 dark) and stays readable on both
  semantic fills.
- Test robustness: key the NetWorthCard chrome Container ('netWorthCardChrome')
  and look it up with find.byKey instead of the fragile first-descendant
  Container match.

* fix(mobile): SureButton owns leading-icon foreground via IconTheme

Address review feedback (jjmata): call sites shouldn't hardcode the button's
foreground on leading icons. Wrap SureButton's content in an IconTheme set to
the variant foreground, so leading icons (e.g. SureIcon) inherit it
automatically — mirroring how Material's ElevatedButton.icon propagates icon
color. Icons that pass an explicit color still win.

Drop the now-redundant `color: palette.textInverse` from the dashboard
empty/error-state button icons; they follow the button variant automatically.

Add a SureButton test asserting a leading icon inherits the variant foreground
(textInverse for primary, textPrimary for outline) via the ambient IconTheme.

* Fix net worth card mask merge regression

* Provide privacy state in net worth card tests

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-07-30 04:12:30 +02:00
ghost a21ab1946e perf(accounts): preload transfer, category, and split-parent associations on show (#2441)
* perf(accounts): preload transfer, category, and split-parent associations on show

AccountsController#show iterated over paginated entries and called
transaction.transfer (two queries via transfer_as_inflow || transfer_as_outflow),
transaction.category, and transaction.merchant individually per row, and fell
back to entry.split_parent? (child_entries.exists? per entry) because
@split_parent_entry_ids was never set.

Fix by:
- Batch-preloading transfer_as_inflow, transfer_as_outflow, category, and merchant
  on transaction entryables after pagination using Associations::Preloader (same
  API already used in accounts/index/_account_groups.erb).
- Setting @split_parent_entry_ids with a single IN query after pagination,
  matching the identical pattern already in TransactionsController#index.

Resolves Sentry issues SURE-APP-PN (60 users), SURE-APP-XE (32 users),
SURE-APP-26 (51 users) and related slow-DB reports on AccountsController#show.

* docs(accounts): note the show preload is intentionally page-scoped

Address review feedback (jjmata): add a comment clarifying that the transfer/
category/merchant preload and the split-parent lookup operate on the current
page (@entries) by design — only this page is rendered, so a child entry whose
split parent is on another page deliberately won't resolve it. Comment-only; no
behavior change.
2026-07-30 03:42:50 +02:00
sentry[bot]andsentry[bot] <39604003+sentry[bot]@users.noreply.github.com> 32eef8e2ea Fix(exchange_rate): Handle RecordInvalid in find_or_fetch_rate race condition (#2734)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-07-30 03:05:14 +02:00
Guillem Arias Fauste cedf28a5a9 fix(accounts): make the sync toolbar and toast agree, fix toast overlap (#2813)
* fix(accounts): make the sync toolbar and toast agree, fix toast overlap

The Accounts page's own sync toolbar (refresh icon, "Cancel sync") and the
global sync-complete toast were three inconsistently-styled, disconnected
pieces of UI representing one action, and the toast overlapped the page's
own header instead of sitting near it.

- "Cancel sync" was hand-rolled markup instead of a DS::Button, unlike its
  sibling refresh icon right next to it — now both are DS::Button (:ghost).
- The refresh icon just went `disabled` with no visible "working" state —
  now shows a spinning loader-circle while a family sync is in progress,
  matching the pattern already used in provider_sync_summary.html.erb.
- The toolbar was plain server-rendered HTML with no way to know a sync
  finished, so it stayed stuck showing "still syncing" indefinitely next to
  a toast now saying otherwise. Family::SyncCompleteEvent now broadcasts a
  second replace target for the toolbar alongside the existing toast
  replace, so both resolve together.
- The notification tray was a <body>-level fixed overlay centered on the
  full viewport, but every layout that renders it has a sidebar of some
  kind — so it never actually centered on the visible content pane, and
  landed on top of the settings-layout header. It now renders in-flow at
  the top of each layout's own content region (opt-in via
  notification_tray_inline, since the simpler single-column layouts don't
  have this mismatch and are unaffected).
- Added the Catalan sync_toast/cancel_sync strings that were missing
  entirely, which is why the toast/toolbar showed English text on an
  otherwise-Catalan page.

Verified live in a real browser via a new system test covering the idle,
syncing, and cancel-flash states, plus a model test on the new broadcast
target.

* fix(accounts): keep the tray a floating overlay, sidebar-aware instead

Codex on this PR: with the tray as first-child-of-scrollable-main, a
notification delivered while scrolled down is inserted above the
viewport and stays unseen — breaking the sync toast's manual-refresh
path specifically, since sync_toast_controller.js suppresses
auto-refresh while a form is focused and relies on the toast being
visible to offer that manual refresh.

Reverts the tray to a position: fixed overlay for every layout (so it
can't be scrolled out of view), and fixes the actual bug that made it
overlap the accounts toolbar in the first place — a ResizeObserver on
<main> centers it on the real content pane instead of the viewport,
for the two layouts with a sidebar (application, settings passed via
sidebar_aware:). The five single-column layouts are untouched; for
those, viewport-center already is content-pane-center.

One trap worth flagging: this app renders turbo_refreshes_with
method: :morph, and idiomorph resets any inline style a client script
set that isn't in the freshly-fetched HTML — including the JS-set
`left`. data-turbo-permanent looked like the fix but isn't: it invokes
idiomorph's node-identity matching (same id preserved across ANY
morphed page), which broke navigation once the id existed on
structurally different layouts (app vs settings) — a real, reproduced
bug, caught by the system test before it shipped. Went with the
narrower turbo:before-morph-attribute event instead, which blocks only
the `style` attribute on this one element, with no node-identity
system involved.

Rewrote the system test's positioning assertion to match: it now
asserts the tray centers on <main> rather than sitting above the page
header, since a fixed overlay was never going to satisfy the latter by
construction.

Verified: full bin/rails test (6023 runs, 0 failures), rubocop, erb_lint,
brakeman (0 warnings) all clean. Live-verified in a browser across both
sidebar-aware layouts and a simple layout, including the full
cancel-sync -> morph -> re-render cycle.

* fix(accounts): use declarative Stimulus action for morph-attribute guard

Replace the manual addEventListener/removeEventListener pair for
turbo:before-morph-attribute with a data-action, per the repo's
declarative-actions convention. Same element, same listener — just no
manual lifecycle management.
2026-07-30 02:57:16 +02:00
Guillem Arias Fauste 07a3413250 fix(ds): keep DS::Menu/Popover panels anchored across Turbo morphs (#2812)
* fix(ds): keep DS::Menu/Popover panels anchored across Turbo morphs

The app refreshes pages via Turbo morph (`turbo_refreshes_with method:
:morph`), and same-page account actions (disable, exclude, set-default,
etc.) trigger one. Two bugs in the shared floating-ui controllers surface
as a result:

- The panel's `position: fixed` only ever existed as a JS-applied inline
  style. Idiomorph resets every menu/popover's `style` attribute to match
  the server-rendered markup (which has none), silently stripping
  `position: fixed` from every panel on the page. The next dropdown/
  popover opened before floating-ui's async recompute lands briefly
  renders in normal flex flow, shoving its own trigger sideways and
  making computePosition anchor to that phantom position instead of the
  real button. Fix: make `position: fixed` part of the static markup so
  it can never be stripped.

- `this.show` was a plain instance property. Because the morph preserves
  the Stimulus controller in place (stable-id turbo frame), `this.show`
  doesn't reset when idiomorph re-closes the content element, so it can
  desync from the DOM and swallow the next click. Fix: derive `show` from
  the content element's own class instead of tracking it separately.

Reproduced and verified against the actual Turbo/Stimulus/floating-ui
pipeline in an isolated harness before and after the fix.

* test(ds): add regression coverage for menu/popover reopen-after-morph

Simulates what idiomorph does to an open panel on a same-page Turbo
morph — resets the content element's class back to the always-hidden
server-rendered markup and strips the JS-applied inline style, without
going through toggle()/close(). Verified against the pre-fix controllers
that this fails without the DOM-derived `show` getter.
2026-07-30 02:54:42 +02:00
Otter 1c6d018b6a Fix transactions posting a day early when booked around midnight. (#2744)
* Fix 2668

* Fix CodeRabbit nitpick

* Fix Akahu parsing

* Anchor provider transaction date parsing to family timezone

* Coderabbit suggestion for Date/DateTime order

* Remove duplicate condition in wise

* Safe unless column_exists + pass family to date parse to family components
2026-07-30 02:29:13 +02:00
PrplHaz4andClaude Fable 5 16d2bc0ce9 Don’t re-create pending SimpleFIN transactions when pending sync is disabled (#2835)
* fix(simplefin): skip pending entries in processor when pending is disabled

When SIMPLEFIN_INCLUDE_PENDING/syncs_include_pending is off, pending rows
already stored in raw_transactions_payload were still (re)created as
entries on every sync - including ones the user manually deleted - because
the setting only affected the API request, not reprocessing of the stored
payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(simplefin): add rake task to prune stale pending rows from payload store

raw_transactions_payload accumulates transactions across syncs and is
never pruned, so pending rows fetched before pending inclusion was
disabled keep getting re-imported. This one-time maintenance task removes
them (dry-run by default; scope by item_id/account_id).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(simplefin): address PR #2835 review feedback on pending detection

Fix epoch-zero pending check treating non-numeric posted strings (e.g.
"unavailable") as pending via String#to_i coercion; compare against
explicit zero representations instead, matching posted_date. Dedupe the
prune_pending rake task's copy of this logic by delegating to a new
public SimplefinEntry::Processor.pending? class method. Also close a
test gap where SIMPLEFIN_INCLUDE_PENDING env var precedence over the
Setting wasn't actually exercised.

* test(simplefin): cover pending-guard precedence and add rake task tests

Add the missing mirror case for pending_enabled? precedence (env var
disabling pending over a permissive Setting) and add test coverage for
the prune_pending rake task, which previously had none: dry_run safety
default, correct pruning via the shared Processor.pending? predicate
(including the malformed-posted regression), and that it never touches
Entry/Transaction rows.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 01:13:02 +02:00
Bishal Shrestha 5ee3275f98 feat: show counterpart account in transfer transaction list row (#2643)
* Show counterpart account in transfer transaction list row

* feat: show transfer counterpart account with access and nil guards

* - Gate counterpart name behind accessible_accounts check.
- Add nested transfer includes to TransactionsController and
AccountsController to prevent N+1 queries.
- Use precomputed @accessible_account_ids Set for O(1) lookups.

* test: add view tests for transfer counterpart rendering

Cover outflow arrow, inflow arrow, and unmatched transfer fallback
using ActionView::TestCase following existing merged_badge pattern.

* Fix transfer eager loading for polymorphic entryables

* Keep accessible_account_ids as Array to fix mock test expectations
2026-07-30 01:11:56 +02:00
Sure Admin (bot) 3c1c87a9d9 Update Rails for Active Storage security advisory (#2849)
Bump Rails and Active Storage from 8.1.3 to 8.1.3.1 to address GHSA-xr9x-r78c-5hrm.
2026-07-30 00:45:43 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 15b5daa298 chore(deps): bump oauth2 from 2.0.18 to 2.0.22 (#2846)
Bumps [oauth2](https://github.com/ruby-oauth/oauth2) from 2.0.18 to 2.0.22.
- [Release notes](https://github.com/ruby-oauth/oauth2/releases)
- [Changelog](https://github.com/ruby-oauth/oauth2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ruby-oauth/oauth2/compare/v2.0.18...v2.0.22)

---
updated-dependencies:
- dependency-name: oauth2
  dependency-version: 2.0.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 22:44:03 +02:00
Oscar 0fa1fbd650 add redbark setup guide to hosting docs (#2817)
- new docs/hosting/redbark.md covering account setup, api keys, linking and sync behaviour
- listed redbark in the onboarding guide's provider integrations
2026-07-28 06:42:57 +02:00