Commit Graph

3114 Commits

Author SHA1 Message Date
SureBot
b338619ef3 remove test file 2026-08-01 00:02:32 -07:00
SureBot
daa5c4472b test push 2026-08-01 00:02:32 -07: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.
v0.7.4-alpha.1
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
kai392
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
William Wei Ming
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]
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 Wilson
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
pro3958
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
pro3958
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
pro3958
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
pro3958
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
ghost
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]
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
PrplHaz4
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]
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
Juan José Mata
c9a28e1aa4 Bump versions 2026-07-25 23:27:33 -07:00
dependabot[bot]
d79925da02 chore(deps-dev): bump vite from 5.4.21 to 6.4.3 in /desktop (#2810)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.21 to 6.4.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-26 08:12:41 +02:00
Max Barbare
700ad34eb1 feat: Introduce macOS app v0.1.0 (#2762)
* feat(desktop): scaffold Tauri 2 macOS shell with empty window

* feat(desktop): server store, URL normalization, and health-check helpers

* feat(desktop): IPC commands for server list/add/remove/health + active-server state

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

* feat(desktop): native onboarding server picker with health check and remembered servers

* feat(desktop): vibrancy background, overlay titlebar, and inset traffic lights

* feat(desktop): native menu bar with standard shortcuts and menu events

* feat(desktop): webview→Rust bridge with native notifications

* fix(desktop): gate bridge injection on PageLoadEvent::Finished

Prevents double-injecting the bridge IIFE (once on Started, once on
Finished), which was duplicating every native notification.

* feat(desktop): Dock badge driven by webview attention count

* feat(desktop): launch-at-login autostart commands

* feat(desktop): sure:// deep link scheme with parse tests and navigation

* feat(desktop): preferences window with server switcher and launch-at-login

* docs(desktop): README for dev, release, signing/notarization, and deferred widget

* fix(desktop): remove dead New Window menu item

* fix(desktop): correct login route to /sessions/new

Rails uses `resources :sessions` (plural), so the login page is
/sessions/new, not the /session/new the plan assumed. Fixes an
immediate 404 when connecting to a server.

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

* feat(desktop): Sure-styled onboarding, draggable titlebar, and app content offset

- Restyle onboarding + prefs to match Sure's auth page: solid surface
  background, centered logomark, .form-field-style inputs, inverse primary
  button; theme-aware via prefers-color-scheme (design-system tokens).
- Add a draggable titlebar strip on bundled pages and inject one into the
  remote page so the window drags from the top everywhere.
- Inject a top offset on the logged-in app-layout root so the sidebar logo
  clears the macOS traffic lights.

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

* fix(desktop): de-dupe login navigation to prevent CSRF token/session race

connect() navigated to /sessions/new directly AND via the
active-server-changed event, which is also handled by a second listener
injected into the page by bridge.js. One connect fired multiple concurrent
GET /sessions/new requests, each minting a fresh session + CSRF token; the
form shown and the _sure_session finally stored could come from different
GETs, so the login POST failed 'Can't verify CSRF token authenticity'
intermittently. Route all navigation through a single window-level guard so
only the first request per server wins.

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

* fix(desktop): enable window drag permission; offset only the icon rail

- Add core:window:allow-start-dragging (+ show/set-focus, event emit/listen) to
  capabilities so data-tauri-drag-region actually drags the window on macOS.
- Offset only the 84px left icon rail (logomark) to clear the traffic lights
  instead of pushing the entire app-layout down; keep main content full-height.
- Drag strip z-index lowered below Sure's sticky headers so its controls stay
  clickable.

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

* feat(desktop): persist active server and resume session on launch

- Persist the active server to the Keychain in set_active_server; active_server
  falls back to it so a relaunch knows where to go.
- On launch, auto-resume straight to the last server instead of showing the
  picker every time.
- Navigate to the server root (not /sessions/new): Rails serves the dashboard
  when the session cookie is still valid, or redirects to login when not — so a
  persisted session no longer forces a re-login.

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

* feat: desktop SSO via system browser with PKCE code exchange

Passkeys/WebAuthn don't work in an embedded WKWebView, so SSO now runs in
the system browser and hands a session back to the app securely.

Server (Rails):
- GET /auth/desktop/:provider — stashes a PKCE S256 challenge, hands off to
  OmniAuth (reusing the mobile auto-submit form). Passkeys work (real browser).
- openid_connect — for a linked identity in a desktop flow, mints a single-use,
  2-min, PKCE-bound one-time code and redirects to sure://sso/callback?code=...
  (unlinked identities are sent back with an error).
- GET /sessions/desktop_exchange — verifies the code + PKCE verifier
  (secure_compare), single-use (cache delete), then create_session_for; MFA is
  enforced at exchange time. Sets the normal web session cookie in the webview.
- Tests: happy path + single-use, wrong-verifier rejection, missing challenge.

Desktop (Tauri):
- start_sso command: generates PKCE, opens the browser, stores the verifier.
- sure://sso/callback deep link -> webview navigates to desktop_exchange with
  the verifier (never sent through the deep link, so an intercepted code is
  useless).
- bridge.ts intercepts SSO provider form submits and routes them to start_sso;
  password login stays in the webview.
- remote.json capability: minimal IPC (drag, event bridge, prefs window,
  start_sso) for the remote Sure origin — no fs/shell/http.

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

* fix(desktop): correct remote IPC capability + handle menu in Rust + drag fallback

Root cause of prefs/switch-server/SSO/drag doing nothing on the logged-in
page: the remote-bridge capability's remote.urls ('https://*') did not match
the server origin, so all IPC (event listen, invoke, drag command) was denied.
Per Tauri v2, window.__TAURI__ is injected on remote pages only with
withGlobalTauri (set) AND a matching remote.urls; patterns need a path
wildcard.

- remote.json: urls -> https://*/**, http://*/** (+ bare host) so any server
  origin matches.
- menu.rs: Preferences and Switch Server now show the prefs window directly in
  Rust (no dependency on remote-page IPC); Switch Server moved from Window to
  the App menu.
- bridge.ts: drops the menu-event listeners (Rust owns them), adds a
  startDragging mousedown fallback for the drag strip, logs diagnostics, and
  reports start_sso success/failure to the console.
- main.ts: drops the now-unused menu listeners.

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

* fix(desktop): SSO via event (remote can't invoke commands), disk-backed server store

Diagnostics confirmed window.__TAURI__ + IPC work on the remote page, but a
remote origin cannot invoke custom commands ('start_sso not allowed. Plugin
not found'). Events are permitted, so SSO now goes through an event.

- SSO: bridge emits 'sure://start-sso'; Rust listens and runs begin_sso
  (opens the system browser). start_sso command kept for local use.
- servers: mirror the server list + active server to a JSON file in
  Application Support as a fallback — Keychain items don't persist for
  unsigned builds, which was wiping the saved server on relaunch.
- remote.json: add notification:default (Sure's PWA was requesting it and
  erroring).
- menu: log whether the prefs window is present when Preferences/Switch
  Server fire, to diagnose the no-op.
- main: log the persisted active server on boot.

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

* fix(desktop): drag the top band on every page via mousedown, not a z-indexed strip

The fixed drag strip sat below Sure's sticky headers (z-10) so it worked only
on pages without a top header. Replace it with a document-level mousedown in the
top ~34px that starts a window drag unless the target is an interactive element
— so dragging works on all pages, Sure's titlebar controls stay clickable, and
main content isn't pushed down.

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

* ci(desktop): tag-triggered GitHub Actions release for universal unsigned .dmg

- .github/workflows/desktop-release.yml: on a 'desktop-v*' tag, build the
  universal (Apple Silicon + Intel) .dmg on a macOS runner via tauri-action and
  publish it to a GitHub Release with unsigned-install instructions.
- README: universal build command, the tag-based release process, and the
  Gatekeeper 'Open Anyway' / xattr steps for end users.
- Drop the unused iOS/Android icon sets (macOS build only needs icon.icns).

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

* ci(desktop): rolling desktop-latest build on desktop/ changes, not manual tags

Replaces the manual desktop-v* tag release with a path-filtered workflow that
builds only when desktop/ changes on main and publishes to a single rolling
'desktop-latest' prerelease with a stable Sure.dmg filename — one permanent
download URL, and the file changes only when the desktop code does.

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

* ci(desktop): tag-driven versioned releases; tag is the single source of version

Revert to manual version tags (desktop-v*) for explicit version control, but
derive the app/.dmg version from the tag so package.json + tauri.conf.json are
synced automatically in CI — no manual version-file edits. Each tag produces its
own versioned GitHub Release with the universal unsigned .dmg.

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

* ci(desktop): release entirely from GitHub via workflow_dispatch version input

Make the GitHub Action the single tool to version + deploy the desktop app:
Run workflow -> enter a version -> it syncs the version, builds the universal
unsigned .dmg, and creates the desktop-v<version> tag + Release. Refuses to
re-release an existing version; marks pre-release versions accordingly. Tag
push (desktop-v*) still works as a secondary trigger.

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

* ci(desktop): publish releases with make_latest:false so they don't hijack the repo's Latest badge

Desktop is a secondary artifact, not the main product. Build with tauri-action,
then publish via action-gh-release with make_latest:false so the repo's 'Latest
release' badge stays on the main app's v* release. Separate desktop-v* tag
namespace already keeps it out of the v* publish workflow.

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

* fix(desktop): address PR review feedback (security + correctness)

Security:
- workflow: pass workflow_dispatch version via env (no shell injection); pin all
  third-party actions to commit SHAs.
- SSO: gate deep-link navigation and begin_sso to servers the user has saved
  (is_known_server), so a rogue page/deep link can't drive them.
- desktop_exchange is now POST (verifier in body, not URL/logs); CSRF skipped
  since the single-use PKCE code is the protection.
- desktop_sso_start validates the code_challenge is a 43-char base64url digest.
- desktop_exchange claims the one-time code atomically (delete-and-check) to
  close the read/delete TOCTOU.
- failure: return desktop SSO errors to the app via sure://sso/callback?error.

Correctness / stability:
- prefs window hides on close instead of being destroyed, so the menu can
  reopen it.
- servers.rs: on-disk store is authoritative (file-first read), atomic writes
  (temp + rename).
- main.ts/prefs.ts: try/catch around add/set/remove/active_server and boot; add
  a shared serverErrorMessage helper (no duplicated substring checks).
- vite.config.ts: derive dir from import.meta.url (ESM has no __dirname).
- bridge.ts: coalesce MutationObserver scans to one per frame.
- README: notarization example uses the universal .dmg name.

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

* fix(desktop): scope remote IPC to server origins at runtime, drop wildcard capability

Resolves the remaining security finding: the static remote.json granted Tauri
IPC to any http(s) origin (https://*). Remove it and instead add a capability
scoped to each server's exact origin at runtime (CapabilityBuilder +
add_capability), granting only the minimal permissions the bridge needs, for
saved/active servers on startup and for the target in set_active_server. No
origin outside the user's configured servers can access IPC.

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

* fix(desktop): add runtime per-origin IPC capability (grant_server_capability)

Implements the runtime-scoped capability that replaces the removed wildcard
remote.json: CapabilityBuilder scoped to each server's exact origin, added via
add_capability for saved/active servers at startup and in set_active_server.
(Split from the previous commit, which only recorded the remote.json removal.)

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

* ci(desktop): harden release workflow (no shared caches, environment gate)

Address two release-workflow security findings:
- Remove cache: npm and the swatinem/rust-cache step so a poisoned Actions
  cache written by another workflow can't flow into a published .dmg (P0).
- Add 'environment: release' to the build job so publishing can require manual
  approval and scope secrets to release runs (P1).

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

* fix(desktop): remove transparency code and fix relaunch behavior

* fix(desktop): fix PR review findings; adjust app notarization path, use Sure theme tokens instead of hardcoding values

* ci(desktop): switch release from independant versioning to using Sure's publishing workflow, releasing and versioning with every main app release

* fix(desktop): restrict CSP as much as possible while maintaining functionality; allow bundled scripts, Tauri IPC, inline styles; deny wildcards

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 07:44:06 +02:00
Oscar
c6a240a183 feat(redbark): add australian bank sync (redbark) (#2794)
* add redbark provider integration

- per family api key provider, built like the lunchflow integration
- syncs accounts, balances and transactions from api.redbark.com
- account setup flow, settings panel, locales and routes
- tests and fixtures

* harden redbark integration based on prior provider pr feedback

- use DebugLogEntry.capture for sync/import/unlink failures
- retry 429s and 5xxs with backoff, raise on page cap instead of truncating
- keep raw response bodies out of logs and errors
- not null constraints on account columns, migration base 7.2
- persist ignored flag for skipped accounts so they stop nagging setup
- validate api key on every save, re-arm status on key rotation
- destroy aborts if unlink fails, atomic account create and link
- require_admin on mutating actions, see_other on error redirects
- single grouped query for item account counts
- i18n default connection name, blank password field value
- controller and provider tests

* fix issues found in second review sweep

- add missing syncable scope, without it every family sync raises
- kick off a sync on connection create and on key rotation
- setup dialog fetches accounts inline for fresh connections and shows api errors
- skip balance write when no balance has been fetched yet, never anchor a false zero
- exclude stale and non banking accounts from the batched balances call, per account fallback if the batch is rejected
- detect the server row ceiling and empty pages instead of silently truncating history
- user sync start date only governs the initial backfill, incremental after that
- fetch connections before the per account loop so auth errors propagate once
- drop untemplated index/show/new/edit routes and dead preload/link_accounts actions
- stable dom id on the settings panel so repeat turbo replaces keep working

* skip brokerage connections, found in live testing

- the transactions endpoint 400s for brokerage connections, they belong to /v1/trades
- only import accounts from banking and documents connections
- guard transaction fetches for any legacy linked non banking account

* address review feedback

- treat the truncation header as a pagination signal: split the date window and refetch instead of failing the account
- prune stale pending rows from the snapshot so settled pendings cant come back as duplicates
- block linking a sure account that already has another provider feed
- count setup failures separately from skips and surface an error instead of "all skipped"
- add not nulls on redbark_items name and api key
- enqueue the destroy job after the flag commits, not inside the transaction
- swap bg-gray-400 for bg-surface-inset, drop amounts from info logs, remove i18n default fallbacks
- tests for window splitting, pending pruning and encrypted payload round trip

* fix issues from convention review

- benign skips (unlinked account, blank id, unparseable rows) no longer count as failures, tracked separately so a clean batch reports success
- currency parsing goes through extract_currency so hash shaped payloads resolve instead of falling to the default
- merchant ids use truncated sha256 instead of md5
- debug log entries for import failures and account sync scheduling failures

* bound the raw transactions snapshot to the fetch window

- trim raw_transactions_payload to the current fetch window on merge, same as brex
- keep rows without a parseable date, drop settled pendings as before
- surface skipped rows in the aggregate debug log entry with imported/skipped counts
2026-07-26 07:40:25 +02:00
minion1227
e5608ca6d9 fix(settings): allow clearing an encrypted provider API key (#2544)
* fix(settings): allow clearing an encrypted provider API key

`update_encrypted_setting` skipped the write whenever the submitted value
was blank, so clearing the field (which auto-submits an empty value) never
removed the stored key — the masked "********" placeholder just reappeared
on re-render. This affected every encrypted provider key (Twelve Data,
Tiingo, EODHD, Alpha Vantage, Tinkoff).

Treat "********" as "leave unchanged" (the untouched masked placeholder)
but persist nil for an explicit blank submission, so a key can be removed
from the UI.

Closes #2465

* fix(settings): clear the remaining encrypted provider tokens on blank

Route openai_access_token, anthropic_access_token, and
external_assistant_token through update_encrypted_setting so blanking
any of them clears the stored value instead of silently retaining it —
same fix as the securities keys, completing the scope of #2465.

Add regression tests for clearing each token and for the OpenAI masked
placeholder, and reset the newly-touched keys in teardown.

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

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-07-26 07:31:25 +02:00
Juan José Mata
375dd060dc feat(insights): gate the insights feed behind preview features (#2788)
* feat(insights): gate the insights feed behind preview features

Insights shipped to everyone in #2550. Make it opt-in via Settings →
Preferences until it's proven, so users who haven't enabled preview
features see nothing and cost nothing.

Entry points gated:
- InsightsController — require_preview_features! covers all four actions,
  including the refresh action that enqueues the job
- Dashboard — the insights_feed section is omitted from the section list
  rather than left in it hidden, so the saved-order lookup and the
  insights_feed unshift special-case never fire; the feed query is skipped
- Top bar — the lightbulb entry and its unread COUNT, which previously ran
  on every page render

The job is gated too, departing from the guide's default that background
jobs keep running. That default fits a job like SweepExpiredGoalPledgesJob,
which only walks records opted-in users created and is naturally inert.
GenerateInsightsJob instead manufactures data for every family nightly —
seven generators over the income statement and balance sheet, plus paid LLM
narration — so it would have kept spending on families who can't see the
result. The fan-out filters with Family.with_preview_features (one indexed
jsonb containment query, not load-and-iterate), and generate_for_family
re-checks above the advisory lock so a gated family skips the broadcast too.

Adds Family#preview_features_enabled? and the matching scopes, keeping the
predicate name identical on User and Family so the guide's GA-removal grep
finds every call site. Verified the SQL scope and the Ruby predicate agree
for true / false / "yes" / nil. Documents the job-gating pattern in
docs/llm-guides/gating-a-preview-feature.md, which previously said the gate
does nothing for jobs.

Existing insight rows are left alone: invisible without the flag, and the
next nightly run refreshes facts and expires anything stale if a family
opts in.

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

* perf(insights): use EXISTS for the family preview rollup

Family#preview_features_enabled? is asked once per family by the nightly
job; the block form loaded and instantiated every member to answer a
boolean. Delegate to the scope instead.

The predicate now shares an implementation with the scope, so the
truthy-non-boolean test asserts against User#preview_features_enabled? —
the predicate the UI actually gates on — to keep the cross-check
meaningful rather than tautological.

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

* docs(insights): fix guide/code drift and stale cron description

Review follow-ups from @gariasf:

- The guide's family-rollup snippet still showed the block form after the
  EXISTS commit changed it. It mattered more than normal doc drift: the
  paragraph below calls GenerateInsightsJob "the reference implementation",
  so the next person writing a gated job would have copied the form
  family.rb's comment explicitly rejects.
- schedule.yml still described the job as running for "all families" — the
  string someone reads while debugging why a family got no insights.
- Document that the shared predicate name is per-user on User but
  "anyone in the household" on Family, and prohibit gating UI on the family
  form: Current.family.preview_features_enabled? reads naturally and would
  show the feature to a user who explicitly opted out. Noted in both the
  model and the guide.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillem Arias Fauste <accounts@gariasf.com>
2026-07-26 07:18:23 +02:00
kai392
6e2cbb727a fix(ai): prevent Anthropic chat crash on no-argument tool calls (#2755)
* fix: prevent Anthropic chat crash on no-argument tool calls

When the model streams a tool_use block with no arguments (e.g.
get_categories), the accumulated input arrives as an empty string.
The Anthropic chat parser passed that empty string straight through as
function_args, and Assistant::FunctionToolCaller then called
JSON.parse("") — which raises "unexpected end of input", surfaces as a
Provider::Anthropic::Error, and kills the whole assistant turn.

Fix at the source by normalizing empty/nil tool input to an empty JSON
object in the parser, plus a defensive guard in FunctionToolCaller so
any provider that emits blank arguments cannot crash a turn.

Fixes #2722

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

* test: assert nil-args result as Hash to match jsonb function_result

Addresses Codex P1 / CodeRabbit review: EchoFunction returns the parsed
params Hash and ToolCall::Function stores it in a jsonb column, so the
nil-arguments test must assert the Hash directly instead of JSON.parse.

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-26 04:47:15 +02:00
RealDiligent
d61ee3abea fix: omit SimpleFin pending param when pending transactions are disabled (#2796)
The SimpleFIN protocol only defines pending=1 (include pending); pending
transactions are excluded by default when the param is absent. Bridges
presence-check the param, so the pending=0 we sent when the 'Include
pending transactions' setting (or SIMPLEFIN_INCLUDE_PENDING=0) was
disabled behaved exactly like pending=1, making the setting a no-op —
pending transactions kept being downloaded, causing pending/posted
duplicates and churn.

Omit the pending query param entirely unless pending is enabled, per
the spec.

Fixes #2440

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 01:56:42 +02:00
RealDiligent
98bf563458 fix: prevent AddAmountToTransfers migration aborting on legacy transfer data (#2795)
The backfill computed amount = outflow_entry.amount - source_fee_amount
and then added a check constraint requiring amount >= 0. Historical rows
that predate the modern sign convention (negative outflow amounts) or
carry fees larger than the entry amount produce a negative principal, so
the constraint aborts the migration with PG::CheckViolation — blocking
db:prepare and the entire upgrade for affected self-hosters.

Normalize with ABS and clamp at zero in the backfill. Rows that already
satisfied the old expression are unchanged (ABS is a no-op for positive
amounts); only previously-failing rows now migrate instead of killing
the upgrade.

Fixes #2653

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 01:54:49 +02:00
Erkan Doğan
1fb5202495 fix(indexa_capital): correct cash activity amount-sign convention (#2793) (#2801)
Fixes #2793.

## Problem

IndexaCapitalAccount::ActivitiesProcessor#normalize_cash_amount used the inverted amount-sign convention relative to Sure's core conventions.

- Outflows (WITHDRAWAL, TRANSFER_OUT, FEE, TAX) were stored as negative amounts.
- Inflows (CONTRIBUTION, TRANSFER_IN, DIVIDEND, DIV, INTEREST) were stored as positive amounts.

Sure requires asset account inflows to be stored as negative amounts (-amount.abs) and outflows as positive amounts (amount.abs).

## Fix

Flip signs in IndexaCapitalAccount::ActivitiesProcessor#normalize_cash_amount to align with Sure's sign convention and sibling processors (SnaptradeAccount::ActivitiesProcessor).

## Test

Added unit tests in test/models/indexa_capital_account/activities_processor_test.rb covering cash inflows, outflows, transfers, fee, label mappings, and empty payloads. All tests pass (39/39 for indexa_capital_account).

Co-authored-by: erkdgn <erkdgn@users.noreply.github.com>
2026-07-26 01:47:53 +02:00
William Wei Ming
34dd5fbc62 update transactions_controller (#1953)
* update transactions_controller

* fix FEEDBACK - Remove DISTINCT that breaks tag-filtered index queries

* fix FEEBACK from jjmata

* Ignore Brakeman EOLRails warning for Rails 7.2
Restore fingerprint-scoped ignore lost during merge from main.

* update transactions_controller

* fix FEEDBACK - Remove DISTINCT that breaks tag-filtered index queries

* fix FEEBACK from jjmata

* Ignore Brakeman EOLRails warning for Rails 7.2
Restore fingerprint-scoped ignore lost during merge from main.

* resolve review - Add .distinct to the tag filtering subquery

* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Drop obsolete Rails EOL note and Brakeman EOLRails ignore

The EOLRails ignore and the accompanying migration-rule note were only
needed while the app ran Rails 7.2.3.1, whose support window closed
2026-08-09. Main has since moved to Rails 8.1.3, so the check no longer
warns and both changes are dead weight that only widen this PR's diff.

Keeps the PR focused on the transactions controller query optimization.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 01:42:18 +02:00
Erkan Doğan
71106f63b0 fix(merchants): preserve user-selected merchant colors on save (#2760) (#2802)
* fix(merchants): preserve user-selected merchant colors on save (#2760)

Fixes #2760.

## Problem

FamilyMerchant#set_default_color callback ran on before_validation and unconditionally executed self.color = COLORS.sample.

As a result:
- Any user-selected color or color passed via API/import was discarded and replaced with a random palette color.
- Renaming or updating any merchant field re-assigned a new random color on every save.

## Fix

Guard set_default_color so it only assigns a random palette color when color is blank (self.color = COLORS.sample if color.blank?).

## Test

- Added test/models/family_merchant_test.rb testing color preservation on creation and update, as well as default color fallback when blank.
- Updated test/controllers/family_merchants_controller_test.rb to assert persisted color on create and update.
- Verified in dev container: 9 runs, 22 assertions, 0 failures, 0 errors.
- RuboCop: 0 offenses.

* fix(merchants): validate hex format and fallback to default color if invalid

Add hex format validation (/\A#[0-9A-Fa-f]{6}\z/) to FamilyMerchant#color (matching Category and Tag models) and ensure set_default_color replaces invalid hex values (e.g. from API/CSV imports) with a default palette color before saving.

Responds to Codex review feedback on PR #2802.

---------

Co-authored-by: erkdgn <erkdgn@users.noreply.github.com>
2026-07-26 01:38:31 +02:00
Antoine 🚀🏃‍♂️
6b6ae0ca31 feat(accounts): add Gains / ROI chart view for investment accounts (#2660)
* feat(accounts): add Gains / ROI chart view for investment accounts

Adds a fourth chart view to the account details page showing the
historical unrealized gains series (market value - cost basis per
holding, summed daily with LOCF and FX conversion), following the
same ChartSeriesBuilder/Series pipeline as the existing views.

Holdings without a usable cost basis (nil, or unlocked zero from
providers) contribute zero gain, matching Holding#avg_cost semantics.

* fix(accounts): carry cost basis forward over gap-filled holdings in gains series

Gap-filled holding rows (weekends, price-history gaps) are persisted
without cost_basis even though the position and basis are unchanged,
which zeroed the gains series on those dates. Look up the basis from
the latest snapshot that has a usable one instead of reading it from
the current row, so already-persisted gap-filled rows are handled too.

* docs(accounts): add method docstrings for gains chart view code

Satisfies the pre-merge docstring coverage check on methods added or
touched by the Gains / ROI feature.

* fix(accounts): sign converted amount like main indicator in gains view

Extracts the gains sign-prefix logic into a shared signed_format helper
so the family-currency converted amount shown on foreign-currency
accounts matches the main indicator (+€79.53 / +$85.00), and adds
component tests covering the positive, negative, non-gains and
foreign-currency formatting paths.

* test(accounts): cover FX conversion path in gains series

The gains_series tests only used USD holdings against a USD target,
leaving the exchange_rates LATERAL join untested. Adds a case with EUR
holdings converted to USD, including LOCF rate carry-forward.

---------

Co-authored-by: Antoine GUYON <agy@ibanfirst.com>
2026-07-25 20:14:56 +02:00
pro3958
12b040e47e fix(akahu): pull full history on initial Akahu sync (#2779)
* fix(akahu): pull full history on initial Akahu sync

Akahu-linked accounts only pulled ~90 days of history on their first
sync. Provider::Akahu#fetch_all already walks the full range via
Akahu's cursor pagination, so the paging logic was not the limit.
AkahuItem::Importer#determine_sync_start_date clamped the initial
window to 90.days.ago when an account had no stored transactions and
no configured sync_start_date, and Akahu's transactions endpoint only
returns data from the requested start onward, so that fallback capped
the first import at 90 days.

Request a 5.years lookback on the first sync instead. Incremental
syncs still continue from last_synced_at - 7.days, and an explicitly
configured sync_start_date still takes precedence.

Fixes #2609

* fix(akahu): omit start date on initial sync to pull full history

determine_sync_start_date still clamped the first import to INITIAL_SYNC_LOOKBACK.ago
(5 years), truncating Akahu apps/accounts that can access more history. Akahu's
account-transactions endpoint defaults to the entire accessible range when start/end
are omitted, so the no-config/no-stored-transactions case now returns nil (no start
date). Subsequent syncs still use the incremental last_synced_at - 7.days path.

Removes the now-unused INITIAL_SYNC_LOOKBACK constant and updates the test to assert
the initial sync omits the start date.

---------

Co-authored-by: agentloop <agentloop@localhost>
Co-authored-by: pro3958 <pro3958@users.noreply.github.com>
2026-07-25 20:11:25 +02:00
Guillem Arias Fauste
9fd959da0b fix(dashboard): add hover state to section header controls (#2798)
* fix(dashboard): add hover state to section header controls

The collapse chevron, widget-size settings, drag handle and cashflow
expand button signalled hover with a colour shift alone
(`text-secondary` → `text-primary`). On 14–16px glyphs that reads as
almost nothing, so there was no feedback for which of two adjacent
controls was about to be clicked.

Adopt the icon-control recipe already used for the top bar in
`layouts/application.html.erb`: add `hover:bg-container-inset-hover`
plus a radius alongside the existing colour shift. The token resolves to
gray-100 / gray-700, so light and dark are both covered and no design
system change is needed.

Replaces the ad-hoc `p-0.5` / `w-5 h-5` sizing with a uniform 24px
centred box, so the four controls present the same hover target and the
same tint area. 24px matches the header's `text-base` line-height, so
the row height is unchanged.

The drag handle moves from `hidden lg:block` to `hidden lg:flex` rather
than gaining a bare `flex`, which would have overridden `hidden` at the
mobile breakpoint depending on utility order.

* fix(dashboard): drop hover fill from the drag handle

The grip is a grab surface, not a button — clicking it does nothing. A
filled hover state reads as "clickable" and promises a click that never
lands, so it keeps cursor-grab plus the colour shift and skips the
background the genuinely clickable controls use.

Stays on the same 24px box so the icons remain aligned with the
widget-size control beside it.
2026-07-25 18:59:43 +02:00
Erkan Doğan
32ab402d38 fix(snaptrade): sign bare TRANSFER activities by provider direction (#2792)
SnapTrade delivers payroll-deducted 401k contributions as type "TRANSFER"
rather than "CONTRIBUTION", which normalize_cash_amount did not handle.
The value fell through to the pass-through branch and was stored positive,
violating the convention that inflows to an asset account are negative.

That stored sign drove both reported symptoms. Entry#classification reads a
positive amount as an expense, so the row rendered -$1,320.75 while the
brokerage showed +$1,320.75. Balance::ReverseCalculator reads it as a value
decrease, so walking backward from the provider-anchored current balance
produced a steadily declining history despite a correct present-day total.

TRANSFER_IN and TRANSFER_OUT encode direction in the type and can force the
sign with .abs. A bare TRANSFER does not, so the provider's sign is the only
directional signal available and is inverted into Sure's convention instead
of being passed through. Scoped to TRANSFER specifically: CASH_TYPES is
unreferenced, so every non-trade type reaches this method and broadening the
else branch would silently flip SPLIT, MERGER, JOURNAL and others.

Fixes #2756

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:54:37 +02:00
github-actions[bot]
f3afcf955a Bump version to next iteration after v0.7.3-alpha.5 release (#2791)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 07:18:12 +02:00
kianrafiee
40b18e8484 feat(reports): add net worth chart with breakdown tooltip (#2716)
* Add monthly net worth chart with group breakdown to Reports

Adds a net worth trend chart to the Reports > Net Worth section,
rendered in the same design scheme as the dashboard chart. The chart
shows one data point per month across the period selected at the top
of the Reports page, and its hover tooltip breaks the hovered month
down into per-account-group balances (Cash, Investments, Credit
Cards, Loans, etc.) under Assets and Liabilities headings with
section totals.

- BalanceSheet::NetWorthBreakdownSeriesBuilder builds the monthly
  series by running Balance::ChartSeriesBuilder per account group
  (grouped by accountable type), with liabilities reported as
  positive magnitudes and all-zero groups omitted; cached with the
  same invalidation pattern as the existing net worth series
- net_worth_chart Stimulus controller extends the existing
  time_series_chart controller, overriding only data normalization
  and the tooltip template
- Reports controller passes the series through the existing
  net_worth_metrics hash; tooltip headings reuse existing locale keys

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

* Address review: month-over-month tooltip deltas, Stimulus value labels

- Recompute each chart point's trend from the previous monthly point
  instead of inheriting the raw series trend, which at a monthly
  interval compared the underlying balance row's own start/end and so
  reflected only the last balance update before the sample date
  (chatgpt-codex-connector). The first point has no prior month and
  renders the standard flat state.
- Pass tooltip section labels to the Stimulus controller as declared
  values (data-*-value attributes) per coding guidelines (coderabbit)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 05:59:32 +02:00
sure-admin
3d40d93bba fix(schema): restore enable banking account columns 2026-07-25 03:36:33 +00:00
Guillem Arias Fauste
9d4b4bc27c feat(jobs): reap imports and exports stuck by lost background jobs (#2681)
* feat(jobs): reap imports and exports stuck by lost background jobs

A hard-killed worker (OOM, SIGKILL during deploy) loses its in-flight
Sidekiq job permanently. Sync is the only model with a stale sweep;
everything else wedges in a non-terminal status forever:

- Import stuck "importing"/"reverting" (e.g. #2274)
- ImportSession stuck "importing" — unrecoverable, publish_later
  refuses to re-publish while importing
- FamilyExport stuck "pending"/"processing" — exports index polls
  every 3s indefinitely
- PdfImport's AI-processing claim held forever (ProcessPdfJob's
  reclaim only runs if the job is redelivered)
- Provider activities_fetch_pending flags stranded when a
  self-rescheduling fetch chain loses a link

SyncCleanerJob (existing hourly cron) now sweeps all of them, each
isolated so one failure doesn't block the rest, and records every
reaped record as a DebugLogEntry with the family attached.

Idempotency guards so a stray or redelivered job cannot corrupt a
record the reaper (or a user retry) has since moved on:

- Import#publish skips complete/reverting/revert_failed imports —
  a redelivered ImportJob after completion double-applies data for
  import types without row dedup. Failed imports deliberately stay
  publishable: their transaction rolled back, so a re-run is a retry.
- FamilyDataExportJob refuses terminal exports but still allows
  processing ones through, since graceful-shutdown redelivery is what
  completes them.

Reaper thresholds (6h imports, 2h exports) key off updated_at and
dwarf legitimate runtimes, so live jobs are not swept in practice.

* fix(jobs): make the reapers race-safe and commit-aware

Review feedback on #2681 (jjmata, Codex, CodeRabbit):

- Every sweep now mutates under record.with_lock with a staleness
  re-check, mirroring the guard Sync#perform gained in #2680 — a job
  finishing between the sweep query and the write can no longer be
  clobbered mid-flight
- Import.clean distinguishes which side of import!'s single transaction
  the worker died on: rows attached means the data committed and only
  the status write was lost, so the record is finalized as complete
  (marking it failed invited a re-publish that double-imports types
  without row dedup, e.g. TradeImport); no rows means a clean rollback
  and the failed/try-again path stays
- PdfImport.clean applies the same split: no rows → the AI-extraction
  claim died, reclaim to pending; rows → the publish died post-commit,
  finalize complete instead of letting the same extracted rows be
  published twice. Stuck PdfImport reverts (previously unswept by
  either clean) now go to revert_failed like every other import
- ImportSession.clean reconciles chunks whose import! committed but
  never got the complete-status write before failing the session, so
  re-publish skips them instead of duplicating their rows via
  SureImport's split path
- Import#publish redelivery skip is captured via DebugLogEntry instead
  of Rails.logger so support can see it in /settings/debug
- The interrupted-error copy is i18n-backed (imports.errors.interrupted)

* fix(jobs): round-2 review feedback on the reapers

- Import.clean excludes session-owned chunks (import_session_id: nil) —
  SyncCleanerJob runs it before ImportSession.clean, so it could
  finalize or fail a SureImport chunk outside the session flow that
  owns its lifecycle. Regression test added (CodeRabbit major)
- family.sync_later moved outside the row-lock transaction in both
  reap paths — Rails doesn't defer enqueues to after-commit by default,
  so Sidekiq could pick the job up before the status write was visible
- Per-record rescue in Import.clean / PdfImport.clean so one bad record
  doesn't abort the rest of the hourly sweep
- ImportSession.clean uses the importing? enum predicate;
  reconcile_committed_chunks! iterates with each (default-ordered
  association made find_each warn, and chunk counts are tiny)

* fix(jobs): address round-3 reaper review (isolation + commit signal)

Per-record rescue isolation for the three sweeps that lacked it —
FamilyExport.clean, ImportSession.clean, and SyncCleanerJob's
activity-flag loop — mirroring Import.clean/PdfImport.clean. One bad
record (validation error, DB blip) no longer aborts the rest of that
sweep for the hour; the activity-flag guard is per-record so a failure
also stops skipping the models that follow.

data_committed? now covers Category/Rule/Merchant imports, whose
records hang off the family rather than the import (no entries or
accounts), via the new Import#committed_by_named_records? helper. A
committed one is reaped to complete instead of a retryable failed;
nameless RuleImport rows carry no stable key, so a nameless-only file
has no commit signal and stays retryable.

* fix(schema): drop duplicate enable_banking_accounts columns from merge

The merge of main into this branch re-appended product, credit_limit,
and identification_hashes after updated_at, so schema.rb declared each
twice and db:schema:load raised "you can't define an already defined
column 'product'". Removed the duplicate declarations (kept the new
treat_balance_as_available_credit column); the test database loads
again.
2026-07-25 05:04:30 +02:00