Commit Graph

157 Commits

Author SHA1 Message Date
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
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
Anthony
e3a7107271 Feature/dashboard Add "Money In / Out" dashboard widget with monthly bar chart (#2594)
* Add "Money In / Out" dashboard widget

Adds a new dashboard section showing a monthly bar chart of cash
activity alongside a summary card (net balance, income, expenses),
with per-widget month navigation and account filtering.

- IncomeStatement#totals_for computes income/expense totals for an
  arbitrary period, optionally scoped to a set of account ids
- New bar_chart_controller.js (D3) renders the monthly bars
- Income/expense rows link through to filtered transactions

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

* Fix tooltip/dropdown positioning and split money flow chart by income/expense

The widget's @container wrapper established a new containing block for
position:fixed descendants, so the month-picker and account-filter
menus (floating-ui, strategy: fixed) and the D3 tooltip (container-
relative coordinates) rendered away from their trigger/bar. Drop
@container in favor of regular viewport breakpoints for this
full-width widget, and position the tooltip with page-relative
coordinates like the other chart controllers.

Also replace the single combined bar per month with a grouped
expense/income pair (red/green, with a legend) so each month's
inflow and outflow are visible independently.

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

* Cap and scroll the money flow widget's month picker

Add an optional max_height to DS::Menu (opt-in, backward compatible)
so a long item list scrolls inside a fixed-height panel instead of
overflowing the viewport. Use it for the money flow widget's 12-month
picker.

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

* Use design system tokens for income/expense indicators in money flow widget

Swap the four bg-green-500/bg-red-500 dot indicators (legend + income/expense row links) in the money flow widget for bg-success/bg-destructive, matching the "functional tokens only" rule. The same partial already uses
text-success/text-destructive for the balance figure, and bg-success/ bg-destructive are already established elsewhere (DS::Alert, budget_categories/_budget_category.html.erb).

* Fix future-month 500 and pending-transaction link mismatch in money flow widget

Two CI review findings on the money flow widget:

- A future month passed via ?money_flow_month= (e.g. a bookmarked/hand-edited URL) made end_date earlier than month_start once capped at Date.current, which Period.custom rejects, causing a 500. money_flow_month_param now clamps future months to the current month, same as it already does for malformed input.

- The income/expense row links passed type/date/account filters but no status, so Transaction::Search included pending transactions even though the displayed totals (IncomeStatement#totals_for) exclude them via excluding_pending. Add status: ["confirmed"] to both links so the linked list matches the card total.

Also strengthens the widget's controller tests: asserts the highlighted bar's actual income/expense values instead of just its presence, and adds a regression test proving an account id outside the current user's accessible accounts is dropped (falls back to the unfiltered state) rather than leaking or erroring.

* Fix account filter eligibility and SVG dark mode fill in money flow widget

Two more CI review findings on the money flow widget:

The account filter iterated over all visible/accessible accounts, a broader set than IncomeStatement actually counts (accounts excluded from reports, tax-advantaged accounts like 401k/IRA, or shared accounts not included in the user's finances). Selecting one of these silently computed to zero while its drill-down link could still list its transactions. Add IncomeStatement#eligible_accounts, mirroring the same criteria already applied in the totals SQL, and use it for both the checkbox list and the account_ids intersection.

The D3 axis tick text elements used text-primary/text-secondary, which set CSS color, not SVG fill, so labels rendered with the default black fill and were unreadable in dark mode. Add fill-current, matching the pattern already used in sankey_chart_controller.js.

Adds controller/model tests for eligible_accounts (excluded from the account filter, ignored when passed as a filter id, excluded from totals) and verifies the dark-mode fill fix visually.

* Extract duplicated bar-chart JSON parsing into a test helper

The css_select("[data-controller='bar-chart']").first + JSON.parse(chart["data-bar-chart-data-value"]) pattern was repeated across four money flow widget tests in pages_controller_test.rb. Extract it into a private money_flow_bars helper and use it everywhere instead.

* Preserve eligible account scope in money flow drill-down links when unfiltered

The income/expense drill-down links used money_flow_data[:account_ids] directly, which is nil in the widget's default unfiltered state, so .compact dropped the account filter entirely from the link. TransactionsController treats an absent account_ids as all accessible accounts, a broader set than IncomeStatement#eligible_accounts (which excludes tax-advantaged, excluded-from-reports, and non-finance shared accounts). Users with any such account could click a displayed total and see transactions that were never counted in it.

Use selected_account_ids (already computed as money_flow_data[:account_ids] || accounts.map(&:id)) for both links instead, so they always pin to the same eligible accounts backing the total, filtered or not. Left the month-picker link on money_flow_data[:account_ids] so navigating months while unfiltered doesn't bloat the URL with every eligible account id.

Adds a regression test confirming the default (unfiltered) links include account_ids and exclude an ineligible account's id.

* Refine Money In/Out dashboard widget

- 6-month window (was 3), half-width default with responsive stack, income-first
  bars, 2px floor + faded in-progress (partial) month
- Expense series/figures neutral (gray/text-primary) — app reserves red for
  negative/overspend; neutral zero balance
- Account filter: outline DS::Button + list-filter icon trigger with in-panel
  search (DS::SearchInput + list-filter), matching the app's filter convention
- i18n search_accounts (en + fr); bump money_flow bar-count test 3 -> 6

* Clarify Money In/Out scope: month label, 6-month caption, filter "All"

Addresses confusion between the widget's own month picker and the dashboard's
global period, and between the picked month and the 6-month chart span.

- Card now headed with the selected month (e.g. "July 2026") so it's clear the
  totals below are that month's, driven by the picker
- Chart legend row captioned "Last N months" so the trailing window is explicit
- Info tooltip by the month picker: the widget scopes to the month you pick
  here, independent of the dashboard's top-level period
- Account filter trigger reads "All accounts" when nothing is filtered out,
  instead of "Filter accounts (N)"
- i18n (en + fr) for the new strings

* Match tooltip expense dot to the gray bar palette

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Guillem Arias <accounts@gariasf.com>
2026-07-25 04:59:29 +02:00
Juan José Mata
66cf9e7f0b feat(insights): proactive financial intelligence feed (#2550)
* feat(insights): proactive financial intelligence feed

Adds a nightly job that analyzes each family's finances in pure Ruby and
surfaces typed, stateful insights on the dashboard and a new /insights page.

- Insight model (active/read/dismissed) with a per-family dedup_key unique
  index so nightly re-runs refresh rows instead of duplicating them
- Seven generators (spending anomaly, cash-flow warning, net worth milestone,
  subscription audit, savings rate change, idle cash, budget health) built on
  IncomeStatement, BalanceSheet, RecurringTransaction, and BudgetCategory
- LLM used as a writer, not a reasoner: Insight::BodyWriter narrates
  pre-computed facts via the configured provider, with an i18n template
  fallback so self-hosted installs without API keys work identically; bodies
  are only (re)written when an insight is new or its numbers changed
- GenerateInsightsJob: cron fan-out per family, per-family advisory lock,
  metadata-diff upsert that preserves read/dismissed state for unchanged
  signals and reactivates on material change
- Dashboard insights_feed section (top 3, collapsible/reorderable) and an
  /insights feed page with turbo-stream dismissal; viewing the feed marks
  insights read
- Tests for the model, job upsert semantics, and controller flows

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

* fix(insights): address Codex review — expiry, budget metadata, LLM usage

- Expire visible insights whose condition cleared: generators declare the
  insight types they produce, the registry reports which generators ran to
  completion, and the job expires visible insights of those types whose
  dedup_key was not regenerated. A crashing generator can't wipe out its
  healthy insights, and an expired insight reactivates when its condition
  returns — unlike a user-dismissed one, which stays dismissed.
- Include a bucketed budget-spent percent in budget_at_risk metadata so the
  body refreshes when overall usage moves >=10 points even if the same
  categories remain flagged.
- Pass family to chat_response so LLM narration is recorded in llm_usages.

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

* fix(insights): address CodeRabbit review

- Skip the mark-as-read write for Turbo hover-prefetch requests
  (X-Sec-Purpose) so unread badges don't clear before a real visit
- Show the New pill on the dashboard feed (active = unread there; the
  feed never marks insights read)
- Filter idle accounts in SQL instead of a per-account exists? loop
- Eager-load merchants in the subscription audit query
- Widen the advisory-lock key to the signed-bigint range and log when
  acquisition fails so a skipped nightly run is observable
- Mirror the dedup_key unique index as a model validation
- Assert the refresh action enqueues for the signed-in family

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

* fix(insights): clear stale lifecycle timestamps on reactivation

When an insight resurfaces, the row now leaves no contradictory state
behind: the material-change path clears both read_at and dismissed_at,
and the expired-recovery path clears read_at. Tests assert the contract,
and the dashboard feed test now also locks in the unread "New" badge.

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

* fix(insights): drop redundant standalone family_id index

Every composite index on insights already leads with family_id, so the
auto-created single-column index was pure write overhead.

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

* fix(insights): stop nightly drift from resurrecting dismissed insights

Three generators stored continuously drifting values (projected amounts,
starting balance, current net worth) in metadata, and any metadata diff
counts as a material change: the body is rewritten (an LLM call when a
provider is configured) and read/dismissed state is cleared. Dismissing a
cash-flow warning was undone at the next 6:00 run.

Metadata now carries only the signal identity plus a coarse bucket, the
same damping the budget generators already use; display values live in
facts. Also orders the idle-cash pick by balance so the selected accounts
don't flip between runs, and documents the dollar-scale threshold
assumption on the relevant constants.

* fix(ds): map DS::Button aria_label into the aria hash

A bare aria_label: option reaches the tag helpers as a literal aria_label
attribute (Rails only dasherizes the nested aria: hash), so the icon-only
fallback overrode it and screen readers announced the insight dismiss
button and the popover trigger as "X".

* fix(insights): gate LLM narration behind AI consent

The nightly job runs unprompted for every family, so narration now
requires someone in the family to have AI enabled: consent to share
financial facts with the provider, and a cost cap in managed mode. The
template fallback keeps behavior identical otherwise. Narration failures
are captured via DebugLogEntry so support can see them.

Adds BodyWriter coverage, including a template-interpolation test for
every generator template key.

* feat(insights): rework the dashboard feed

The feed rendered full insight cards inside the section shell — the only
widget nesting card-on-card — and appended below the fold for every
family with a saved section order. It now mirrors the outflows and
balance-sheet list idiom (inset well with an uppercase mini-header, white
row block, 28px sentiment-tinted icon circles via color-mix on the DS
CSS variables, right-aligned key figures) and leads the dashboard for
saved orders that predate it.

Icon color comes from sentiment, not priority — a savings-rate
improvement is high priority AND good news, and must not render red; red
is reserved for a projected-negative balance. Rows have no hover wash
(cursor plus a gentle icon scale, like the sibling widgets), links to
/insights disable Turbo prefetch so the mark-as-read actually fires for
mouse users, and the standard widget-size popover offers Half/Full. Full
stays the default: the feed is far shorter than any other single-width
widget, so a half default leaves a grid hole the masonry cannot backfill.

* feat(insights): actionable cards, dismiss undo, live refresh

Each row now persists its display facts (new jsonb column) alongside the
change-detection metadata. Facts refresh every run without touching the
body or user state, which is exactly why they are not part of the
material-change comparison.

The card gains what the stored data always supported: a type-and-period
meta line replacing "x minutes ago", a right-aligned key figure (green
only for good news), and a contextual link resolved from the subject ids
in metadata — category, account, recurring transaction, budget month —
omitted when the subject no longer exists.

Dismiss is forgiving: a toast in the notification tray offers undo, and
undismissing restores the row as read rather than re-badging it.
Milestone insights can never regenerate once dismissed, so this closes a
real loss path. Upsert failures are captured via DebugLogEntry.

Manual refresh gets feedback: the button swaps to a disabled checking
state, the page subscribes to a family-scoped stream, and the job
broadcasts the refreshed list and the idle button when it finishes (also
after a lock-skipped run, so the button cannot stay stuck).

Savings copy is sign-aware: a negative rate reads "you spent more than
you earned" with true minus signs. The empty state swaps Lucide sparkles
for the brand assistant glyph (DS::EmptyState learns icon_custom:).

* feat(insights): top-bar entry with unread count

The dashboard feed hides at zero insights and nothing else linked to
/insights, so the manual refresh (and the empty state) were unreachable
for exactly the families who need them: fresh setups before the first
6:00 run. The sticky top bar travels to every screen and its right
cluster had room, so insights get an icon entry there with a monochrome
unread badge. Prefetch is disabled on the link for the same
mark-as-read reason as the feed.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillem Arias <accounts@gariasf.com>
2026-07-14 02:19:39 +02:00
Orange🍊
59b2e59a4a fix(ds): 给仪表盘尺寸控件加焦点环 (#2364) 2026-06-17 11:54:44 +02:00
Guillem Arias Fauste
c29380ce57 feat(dashboard): masonry packing + per-widget size controls (#2328)
* feat(dashboard): masonry packing + per-widget size controls

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-15 20:34:34 +02:00
Guillem Arias Fauste
84547f766c fix(dashboard): align sankey zoom-out button with section header (#2313)
The cashflow sankey zoom-out button sat in a bare flex justify-start
row. Because the dashboard section body has no horizontal padding (just
py-4), the button rendered flush against the card's left edge — 16px
left of the section header title and out of line with the rest of the
widget. Add px-4 to the button row so it aligns with the header,
matching the _net_worth_chart widget's px-4 header row.
2026-06-14 22:02:35 +02:00
Guillem Arias Fauste
215864fdd9 fix(dashboard): apply two-column layout at xl, not 2xl (#2310)
The dashboard two-column layout preference only added 2xl:grid-cols-2
(>=1536px), but the setting copy promises two columns on large screens.
On any display narrower than 1536px (most laptops, ~1280-1440px) the
toggle did nothing. Lower the breakpoint to xl (>=1280px) so it engages
on the screens users actually have while keeping widgets wide enough to
stay usable.
2026-06-14 22:01:05 +02:00
Will Wilson
d908560ed9 feat(cashflow): deep-link category labels to filtered transactions (#2083)
* feat(cashflow): deep-link category labels to filtered transactions

Clicking a category's text label in the dashboard cashflow Sankey chart
now navigates to the transactions page filtered by that category and the
cashflow's active period date range. The colored node bar keeps its
existing zoom-into-subcategories behavior; structural nodes (Cash Flow,
Surplus) do not navigate.

URL-building lives in a pure, unit-tested utils/transactions_filter_url
module (mirroring utils/sankey_zoom) pinned in the importmap. Period dates
are threaded from the dashboard view into the Stimulus controller via data
values. This matches the existing donut chart's click-to-filter behavior.

* Update app/javascript/controllers/sankey_chart_controller.js

Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
Signed-off-by: Will Wilson <will@willwilson.uk>

---------

Signed-off-by: Will Wilson <will@willwilson.uk>
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
2026-06-11 21:21:56 +02:00
Guillem Arias Fauste
7ebe5154cb fix(ds): balance-sheet weight column alignment + category pill padding (#2275)
- Dashboard balance-sheet weight cell wrapped its percentage ("64 %" -> two
  lines) for 2-digit values: the account-row cell is w-14 (56px), too narrow
  for the 5-bar gauge plus a spaced locale percentage (ca/es format "NN %").
  Widen the account-row weight cell to w-20 to match the header/group columns
  (also fixing the column misalignment) and add whitespace-nowrap to the value.
- Bump DS::Pill :md vertical padding py-0.5 -> py-1 so category pills (and the
  other :md pills) are not vertically cramped.
2026-06-11 15:32:07 +02:00
Guillem Arias Fauste
211e407456 fix(ds): migrate remaining amber notice boxes to DS::Alert(:warning) (#2247)
Continues the #2198 warning consolidation (after the SSO surfaces).
Replaces the last hand-rolled bg-amber-50 notice boxes with DS::Alert:

- pages/redis_configuration_error: "why Redis is required" callout →
  DS::Alert(title:, message:, variant: :warning).
- family_exports/new: export "Note" callout → DS::Alert(title:, message:).
- sessions/new: "no auth methods enabled" notice → DS::Alert(message:).

The DS::Alert warning recipe (warning tint + neutral text + alert-triangle)
replaces the literal amber palette while preserving each notice's intent.

Part of #2198. After this, the only remaining raw amber-* is the dynamic
validation feedback in admin_sso_form_controller.js (deferred — JS form
state styling, a separate concern).
2026-06-11 15:25:33 +02:00
Guillem Arias Fauste
96079188a2 feat(dashboard): unify per-widget period selectors into one picker (#2162)
* feat(ds): elevate dropdown overlays and stabilize selection check gutter

Menus and popovers floated at the same elevation as inline cards
(shadow-border-xs), so dropdowns blended into the content beneath them.
Bump DS::Menu and DS::Popover panels to shadow-border-lg.

DS::MenuItem rendered its leading icon only when present, so a selection
check shifted the row's text out of alignment with the unselected rows.
Add a `selected:` param that reserves a fixed-width check gutter (check
when selected, empty otherwise) so row text stays aligned. Apply the same
reserved gutter to the bespoke category dropdown row, and add a
`selectable` menu preview.

* feat(dashboard): unify per-widget period selectors into one picker

The dashboard rendered three identical period <select>s (cashflow,
outflows, net worth), each writing the same global User#default_period
and full-reloading the page via turbo_frame "_top" — so changing one
changed all. Replace them with a single shared UI::PeriodPicker
(DS::Menu of period links) in a toolbar, and wrap the sections grid in a
"dashboard_sections" Turbo frame so a period change swaps only the
dashboard (no full-page reload). Reuse the same picker on the account
chart, removing its duplicate select.

* refactor(dashboard): use DS::MenuItem selected: gutter in period picker

Now that DS::MenuItem reserves a check gutter, the period picker passes
selected: instead of a conditional leading check icon, so the current
period's row stays aligned with the rest.

* fix(ds): expose menu selection via menuitemradio + aria-checked

Selectable DS::MenuItem rows conveyed selection only visually. Render them
as role="menuitemradio" with aria-checked so assistive tech gets the
selection state of single-select lists, merging the menu ARIA contract with
any caller-supplied aria. Addresses CodeRabbit review feedback.

* refactor(dashboard): drop redundant aria-current from period picker

DS::MenuItem now exposes selection via menuitemradio + aria-checked, so the
period picker no longer needs its own aria-current. Update the component
test to assert the new ARIA.

* fix(ds): include selectable roles in menu roving-focus query

DS::MenuItem selectable rows render as role=menuitemradio, but the menu
controller built its roving-focus list from [role=menuitem] only, leaving
single-select menus with no keyboard focus/arrow handling. Query the
menuitemradio/menuitemcheckbox roles too. Addresses Codex review feedback.

* fix(dashboard): keep non-picker links out of frame + fix custom-month picker

- turbo_frame_tag "dashboard_sections" now targets _top so ordinary links
  inside the sections (e.g. Balance Sheet account links) navigate the page
  instead of failing to resolve inside the frame.
- Period.current_month_for / last_month_for carry their semantic key for
  custom-month families, so the picker shows the right label and checks the
  right option instead of falling back to 30D.

Addresses Codex review feedback.

* fix(dashboard): announce selected period in picker trigger's accessible name

The static "Select time period" aria-label overrode the visible selected
label as the trigger's accessible name, so assistive tech kept announcing
the same name regardless of selection. Interpolate the selected short
label into the aria-label and pin it with a component test.

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-06-04 23:11:43 +02:00
Guillem Arias Fauste
52b9b47f8c feat(ds): semantic color — neutralize decorative green/red on reports (#2134) (#2144)
Color should signal good/bad, not decorate. The always-on decorative coloring
diluted that: report assets were always green, liabilities always red (and that
red == destructive, so a mortgage read like "danger"), expense totals always
red, and investment contributions/withdrawals always green/orange.

Neutralize those to text-primary:
- _net_worth: assets-vs-liabilities headline + per-group asset/liability rows.
- _breakdown_table: expense totals (income stays green).
- _trends_insights: avg monthly expenses (income + net-savings stay semantic).
- _investment_summary: contributions + withdrawals.

KEEP genuine signal: transaction income->green, net-worth total by sign,
Trend#color gains/losses/changes, budget over/near/on-track status, period
net-savings/income/expense *deltas*. ("Balanced" per design discussion.)

Separate follow-up: category icons using a user-chosen hex equal to destructive
red (color-picker concern, not amount coloring).
2026-06-04 22:13:18 +02:00
Guillem Arias Fauste
ca8cbaa201 fix(dashboard): make Outflows widget responsive to its container, not the viewport (#2059)
* fix(dashboard): make Outflows widget responsive to its container, not the viewport

The Outflows widget switched to its two-column (donut + category list)
layout on the `lg:` viewport breakpoint, but the card's real width is set
by the account sidebar and AI panel — and the two-column dashboard setting.
With those open the viewport stays >= lg while the card is only ~500px, so
the desktop layout was forced into a narrow card: the donut clipped and the
category list overflowed with names truncated to icons.

Switch the widget to container queries (`@container` + `@2xl:`) so it
responds to its own width: side-by-side when the card is wide, stacked (the
existing mobile layout) when narrow — regardless of sidebars, viewport, or
the dashboard column setting. Mobile/PWA is unchanged (already stacked) and
it degrades to always-stacked on browsers without container-query support.

Closes #2058

* fix(dashboard): keep w-16 base on Outflows categories header label

Restore the w-16 base width on the categories-header label column so all
eight responsive classes are uniform lg:->@2xl: swaps. The label now matches
its value/weight siblings and the narrow-width layout is identical to main's
current mobile rendering (zero regression). Addresses review feedback on the
asymmetric width drop.
2026-05-31 09:14:46 +02:00
Rene Arredondo
a002920bfb fix(charts): auto-fit donut center text to inner ring (#2002) (#2017)
* fix(charts): auto-fit donut center text to inner ring (#2002)

* fix(charts): use Number.parseFloat for biome lint

* fix(charts): use rendered donut diameter and destructive token
2026-05-30 16:20:21 +02:00
Michal Tajchert
e21ab9819f feat(dashboard): zoom into cashflow sankey categories (#1807)
* feat(dashboard): zoom into cashflow sankey categories

Click a category node on the dashboard cashflow Sankey to focus on it and
its descendants only; a back button restores the full view. Clicking the
Cash Flow node zooms to the expense (outbound) side.

- Pure utility (app/javascript/utils/sankey_zoom.js) computes the
  descendant subgraph from a clicked node, with direction inferred by
  reachability from the cash flow node (outbound for expense, inbound
  for income).
- Stable node ids emitted from the controller so the JS can identify
  nodes across re-renders.
- Stimulus controller adds chart + zoomOutButton targets, fade
  transition, and only sets a pointer cursor when a node has children.
- Node:test coverage for expense, income, cash-flow, and malformed-data
  cases; \"type\": \"module\" added to package.json so the .js util is
  ESM-compatible under Node.

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

* refactor(dashboard): extract cashflow sankey chart partial

Deduplicate sankey chart markup between inline and expanded dialog views,
and reset zoom state when chart data changes.

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

* refactor(js): rename sankey_zoom util to .mjs to drop project-wide ESM flag

Removes "type": "module" from package.json to avoid implicitly switching
every .js file in the project to ESM (a future footgun for any .js config
file added by Biome, Vite, etc.). Renames the utility to .mjs so node --test
can import the ES module directly, and adds an explicit importmap pin since
pin_all_from only globs .js/.jsm.

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

* fix(assets): register .mjs MIME type for Propshaft

Propshaft derives Content-Type from Mime::Type.lookup_by_extension, which
returns nil for :mjs by default. Browsers refuse to execute ES modules
served with an empty Content-Type, breaking the sankey_zoom util loaded
via importmap.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:17:35 +02:00
Guillem Arias Fauste
cdce00c71e refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B) (#1860)
* refactor(design-system): migrate 9 hand-rolled buttons with orphan btn-- classes to DS::Button / DS::Link

Part of #1715 §5. The `btn`, `btn--primary`, `btn--outline`, `btn--ghost`,
`btn--sm` CSS classes have no backing styles anywhere in the codebase
(no .btn definition in app/assets/, no Bootstrap dependency). These
callsites have been rendering unstyled buttons / links since the
underlying CSS was last removed.

Migrate the 9 broken callsites:

- `app/views/transactions/show.html.erb` — duplicate-merge action
  buttons (×2): `button_to ... class: "btn btn--primary btn--sm"` /
  `class: "btn btn--outline btn--sm"` → DS::Button with href +
  variant + size + `data: { turbo_method: :post }`.
- `app/views/snaptrade_items/select_existing_account.html.erb` —
  "Go to Provider Settings" link → DS::Link primary sm.
- `app/views/indexa_capital_items/select_existing_account.html.erb` —
  same pattern → DS::Link primary sm.
- `app/views/import/confirms/show.html.erb` — Publish button +
  Cancel link → DS::Button primary full-width + DS::Link ghost
  full-width.
- `app/views/simplefin_items/new.html.erb` — Cancel link
  (`class: "btn"` only) + Connect submit → DS::Link secondary +
  bare `f.submit` (already routes to DS::Button via
  StyledFormBuilder).
- `app/views/settings/providers/_ibkr_panel.html.erb`,
  `_snaptrade_panel.html.erb`,
  `_indexa_capital_panel.html.erb` — strip the orphan
  `class: "btn btn--primary"` from `f.submit` callers; the submit
  is already a styled DS::Button via the form builder.

The next PR in this chain (Phase B) will tackle the larger inline-
button cluster (~29 files, 38 instances) — provider panels and
provider-item flows hand-rolling the same
`inline-flex items-center justify-center rounded-lg px-4 py-2
text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover
focus:outline-none focus:ring-2 focus:ring-primary transition-colors`
string.

* refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B)

Bulk sweep of the second cluster from §5. 29 files, 38 button
instances — each one hand-rolled the same long Tailwind string for
the primary action button:

  inline-flex items-center justify-center rounded-lg px-4 py-2
  text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover
  focus:outline-none focus:ring-2 focus:ring-primary transition-colors

(some variations used `button-bg-primary hover:button-bg-primary-hover`
instead of `bg-inverse hover:bg-inverse-hover` — same intent).

Every instance is now a DS::Button / DS::Link with `variant: :primary`,
which:

- Picks up the new focus-ring + touch-target work from #1840 once
  that merges.
- Stops duplicating the long Tailwind string across 29 files —
  single source of truth in `DS::Buttonish::VARIANTS[:primary]`.
- Picks up consistent `aria-label` derivation for icon-only forms.
- Removes the misnamed `focus:ring-primary` (no token) — the new
  ring comes from `base.css` automatically.

Migration patterns applied:

- `f.submit text, class: "inline-flex …"` inside `styled_form_with`
  → bare `<%= f.submit text %>`. StyledFormBuilder routes through
  DS::Button.
- `link_to text, path, class: "inline-flex …"` → DS::Link primary.
- `button_to text, path, method: :X, class: "inline-flex …"` →
  DS::Button with `href: path` and `data: { turbo_method: :X }`.
- `submit_tag text, class: "inline-flex …"` inside raw `form_with`
  → DS::Button with `type: :submit`.

Notable adjustments:

- `holdings/show.html.erb` — the form was `form_with` (not styled).
  Switched to `styled_form_with` so `f.submit` routes through
  DS::Button. `f.combobox` (hotwire_combobox) still works through
  the styled builder.
- Two `link_to settings_providers_path` callsites in
  `coinstats_items/new.html.erb` + `enable_banking_items/new.html.erb`
  had `w-full inline-flex … hidden md:inline-flex` — the responsive
  pair conflicted (both `inline-flex` and `hidden md:inline-flex`
  on the same element). Migrated to `full_width: true` without the
  responsive split; the buttons now render at all breakpoints
  consistently. (Pre-existing copy-paste bug, fixed in passing.)
- `enable_banking_panel` add-connection button gained
  `icon: "plus"` via the DS::Button API; the explicit `gap-2 …
  icon "plus"` markup is now redundant.

Sibling buttons that don't match the primary spec (destructive
trash, secondary outline-bordered, button-bg-secondary-strong on
holdings/show.html.erb, etc.) are intentionally left alone — they
need their own audit pass once #1840 lands and the focus-ring
behavior on those variants is stable.

* fix(review): restore SimpleFIN submit styling + i18n provider_form label

- SimpleFIN new modal: switch form_with -> styled_form_with so f.submit
  picks up the DS::Button render via styled builder (Codex #1860).
- _provider_form: replace hardcoded "Save and connect" with t(".save_and_connect")
  and add scoped key under settings.providers.provider_form (CodeRabbit).
2026-05-20 18:15:15 +02:00
Brendon Scheiber
7411db5689 feat(i18n): add Hungarian translations for strings extracted in #1806 (#1817)
* add missing Hungarian translations for newly extracted strings

Replace hard-coded UI strings with I18n lookups across controllers, models and views (breadcrumbs, dashboard, reports, settings, transactions, balance sheet, MFA status). Update models to use translations for category defaults, account/display names, classification group and period labels; remove a few hardcoded display_name methods. Add and update numerous locale files (English and extensive Hungarian translations, plus model/view/doorkeeper entries) to provide the required keys. These changes centralize copy for localization and prepare the app for Hungarian/English UI text.

* Pluralize account type labels; tidy Crypto model

Update English locale account type labels to use plural forms for consistency (Investment(s), Properties, Vehicles, Other Assets, Credit Cards, Loans, Other Liabilities). Also remove an extra blank line in app/models/crypto.rb to tidy up formatting.

* Back to singular

* fix(i18n): separate singular and group account labels

* Update _accountable_group.html.erb

* Use I18n plural names for account types

Change Accountable#display_name to look up pluralized account type names via I18n (accounts.types_plural.<underscored_class>) with a fallback to the legacy display logic. Add legacy_display_name helper to preserve previous behavior (singular for Depository and Crypto, pluralized otherwise). Add corresponding types_plural entries in English and Hungarian locale files for various account types.

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-05-18 20:49:28 +02:00
Brendon Scheiber
0c126b1674 feat(i18n): extract hardcoded English strings to locale files (#1806)
* Extract hardcoded strings to i18n

Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.

* Update en.yml

* Update preview-cleanup.yml

* Revert "Update preview-cleanup.yml"

This reverts commit 1ba6d3c34c.

* test: align i18n assertions with translated messages

* Standardize balance error key and tweak locales

Replace SophtronAccount's :requires_balance error key with :no_balance and update related locale strings for sophtron, plaid, and simplefin accounts to use the new key and clearer copy. Also switch the QIF upload redirect notice to use a relative translation key (t('.qif_uploaded')), remove an unused SSO providers help line, and fix a trailing-newline/whitespace issue in the subscriptions locale. These changes standardize validation keys and improve translation consistency and messaging.

---------

Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
2026-05-17 09:52:49 +02:00
Guillem Arias Fauste
2bcdf6c554 fix(design-system): replace undefined utility classes and broken /N modifiers (#1660)
* fix(design-system): replace undefined utility classes and broken /N modifiers

Audit of class-name resolution in views surfaced two related silent
failures across ~17 files:

1. Class names that don't exist anywhere in the design system. Tailwind
   silently drops them and the element renders with no CSS for that
   property.
   - bg-primary (and bg-primary/5, /10, /90): never defined as a
     custom utility, no --color-primary in @theme. Used as a CTA bg
     in 8 places, all rendered transparent.
   - text-inverted: typo of text-inverse.
   - text-primary-foreground: shadcn/Radix vocabulary, not in our
     token system.
   - bg-accent / border-accent / text-accent: same shadcn vocabulary;
     not defined.

2. Slash modifier (/N) used on custom @utility blocks. Modifiers only
   resolve on Tailwind theme colors (anything in tokens.json color.*).
   Custom @utility blocks compile to static @apply statements and
   silently drop the /N variant. Affected uses:
   - border-surface-inset/50 across provider account selectors.
   - border-secondary/30, /40 in admin SSO form and simplefin setup.
   - bg-surface-inset/30, /40 in settings preferences and simplefin.

Fixes:

| From                                              | To                                                  |
|---------------------------------------------------|------------------------------------------------------|
| bg-primary text-white (and similar primary CTAs)  | button-bg-primary text-inverse                      |
| bg-primary text-primary-foreground (badges)       | button-bg-primary text-inverse                      |
| bg-primary text-inverted (typo)                   | button-bg-primary text-inverse                      |
| bg-primary text-primary (broken active pill)      | bg-inverse text-inverse                             |
| bg-primary (status dot)                           | bg-inverse                                          |
| bg-primary/5, bg-primary/10 (subtle accent bg)    | bg-gray-tint-5, bg-gray-tint-10                     |
| hover:bg-primary/90                               | hover:button-bg-primary-hover                       |
| border-accent bg-accent/10 text-accent (badges)   | border-secondary bg-surface-inset text-secondary    |
| border-surface-inset/50                           | border-secondary                                     |
| border-secondary/30, /40                          | border-tertiary                                      |
| bg-surface-inset/30                               | bg-surface-inset (full strength)                     |
| bg-surface-inset/40                               | bg-container-inset                                   |

Also documents the alpha-modifier limitation in design/tokens/README.md
under a new "Alpha modifiers in views (/N syntax)" section, with the
opacity-N convention for custom utilities and a note that the
gray-tint-5 / gray-tint-10 family (and similar pre-resolved tints) are
theme colors and accept /N modifiers natively.

The accent-badge mapping uses neutral semantics for now. A dedicated
brand-accent token (text-link-tint-10 etc.) is worth considering as a
follow-up if the "highlighted metadata badge" pattern recurs.

* fix(design-system): replace undefined divide-primary / divide-secondary with alpha tokens

Same class of bug as the rest of this PR: divide-{name} requires the
name to be a theme color (i.e. expose --color-{name}), and our custom
@utility utilities (primary, secondary, etc.) do not. Tailwind silently
drops the unrecognized class and rows render with no separator.

Spotted six instances during the visual audit:

- admin/users/index.html.erb (×2): users table + pending invitations
- admin/sso_providers/index.html.erb (×2): configured + legacy lists
- transactions/categorizes/_transaction_list.html.erb: categorize sidebar
- settings/preferences/show.html.erb: divide-secondary/60 (also broken)

Swapped to the alpha-black/white pattern already used elsewhere in the
codebase (imports/cleans/show, transactions/_summary, etc.):

  divide-y divide-primary
  -> divide-y divide-alpha-black-200 theme-dark:divide-alpha-white-200

  divide-y divide-secondary/60
  -> divide-y divide-alpha-black-100 theme-dark:divide-alpha-white-100

The lighter (-100) variant on the preferences list matches the original
intent of /60 (more subtle).
2026-05-04 21:40:17 +02:00
Guillem Arias Fauste
0fe1e06645 refactor(design-system): migrate fg-* utilities to text-* and remove namespace (#1626)
* refactor(design-system): migrate fg-* utilities to text-* and remove namespace

The design system carried two parallel namespaces for foreground colors:
text-* (canonical, ~2,000 uses) and fg-* (32 uses). Most fg-* tokens
were 1:1 duplicates of a text-* counterpart. fg-gray was nearly
identical to text-secondary, with a one-step shade difference in dark
mode.

This PR migrates all 32 usages to their text-* equivalents and removes
the fg-* block from the design tokens. Closes #1606.

Mapping:
- fg-inverse  -> text-inverse  (20 usages, identical light/dark values)
- fg-gray     -> text-secondary (7 usages; light values match, dark is
                                 one step lighter: gray-300 vs gray-400)
- fg-primary  -> text-primary  (3 usages, identical values)
- fg-subdued  -> text-subdued  (2 usages, identical values)

The four other fg-* tokens (fg-contrast, fg-primary-variant,
fg-secondary, fg-secondary-variant) had zero usages despite being
defined; they are removed without replacement.

JSON / build:
- design/tokens/sure.tokens.json: $version 1.0.0 -> 2.0.0 (breaking
  schema change per the policy added in #1620). 8 fg-* token
  definitions removed.
- button-bg-ghost-hover's dark value still references "fg-inverse"
  internally; rewritten to "bg-gray-800 text-inverse" so the cleanup
  doesn't break that utility.
- _generated.css regenerated. 42 utility blocks now (was 50).

Lookbook tokens preview:
- The Text & foregrounds section dropped its split between text-*
  (canonical) and fg-* (legacy). Now a single section listing the
  five text-* utilities. The "(legacy)" framing is gone since there's
  no legacy left.

README:
- design/tokens/README.md's button-bg-ghost-hover edge-case example
  updated to reflect the new "bg-gray-800 text-inverse" dark value.

Visual review needed in dark mode:
- Anywhere icons use the application_helper#icon helper with
  color: "default" (most icons in the app). The default class moved
  from fg-gray (gray-400 dark) to text-secondary (gray-300 dark), so
  default-color icons render slightly lighter in dark mode.
- DS::Buttonish icons in secondary buttons (same shade shift).
- DS::Link icons (same).
- Time series chart axes (same).
- All tooltips, account add flow, settings hostings buttons,
  invitations, AI consent, family export, danger-zone buttons --
  these used fg-inverse, which is identical to text-inverse, so no
  visual change expected.

* fix(design-system): use inverse pair on tooltips for readable dark mode

* fix(lookbook): use semantic tokens in menu preview header text

* fix(lookbook): set text-primary on layout body so previews inherit theme

* fix(design-system): keep shadows dark-toned in dark mode

Inverting shadows to white|8% on dark surfaces produces a halo
effect rather than an elevation cue, and stacks redundantly with
the alpha-white 1px ring already in shadow-border-*.

Switch dark-mode shadows to black at progressively higher alpha
(25%/30%/35%/40%/50% for xs..xl) so they read as actual cast
shadows on near-black surfaces. Surface-tint differences and the
existing alpha-white border ring continue to handle elevation
hierarchy and edge definition.

Approach matches Material 3, Apple HIG, IBM Carbon, Refactoring UI,
and the dark-mode shadows used in Linear/Vercel/Stripe.

* fix(design-system): set text-primary on DS::Dialog element

Browser UA stylesheets apply color: black directly to <dialog>,
which overrides ancestor inheritance even when a body or html
ancestor sets a theme-aware color. Unstyled child content then
renders black regardless of theme.

Setting text-primary on the dialog element itself defeats the UA
override and lets descendants inherit the semantic token.

* fix(lookbook): use shadow css vars in effects preview so dark theme renders

* Revert "fix(design-system): keep shadows dark-toned in dark mode"

This reverts commit 3e9d76ed0beb5ac5f2acbad61e4d1c39eadc9ac2.

* fix(design-system): use opacity-70 instead of text-inverse/70 in value tooltip

The custom @utility text-inverse expands to @apply text-white and
isn't modifier-aware, so text-inverse/70 produced no CSS at all and
the muted labels fell through to inherited color (invisible on the
white pill in dark mode).

Replace with text-inverse + opacity-70. Same visual effect, works
with the existing utility definition.
2026-05-04 00:50:52 +02:00
Guillem Arias Fauste
c429f20a77 chore(design-system): replace dead Bootstrap classes with Sure tokens (#1621)
Sure uses Tailwind v4 with the design system tokens but several views
still carried Bootstrap-style class names that don't render anything
because no Bootstrap stylesheet is loaded. They're effectively dead
markup.

Replacements:
- text-muted, text-muted-foreground -> text-subdued
- bg-light -> bg-surface
- font-italic -> italic
- text-uppercase -> uppercase
- font-weight-bold -> font-bold

Touched files:
- app/views/doorkeeper/applications/_form.html.erb
- app/views/doorkeeper/applications/show.html.erb
- app/views/pages/privacy.html.erb
- app/views/pages/terms.html.erb
- app/views/pages/redis_configuration_error.html.erb
- app/views/settings/providers/_mercury_panel.html.erb

Also tightening application.css:
- The .hw-combobox__label rule used raw text-gray-500 / text-gray-400
  via @apply. Now uses the text-secondary / text-subdued tokens so the
  combobox label responds to the theme.
- Custom scrollbar thumbs in .windows and .scrollbar used hardcoded
  #d6d6d6 / #a6a6a6 hex values. Now reference var(--color-gray-300) /
  var(--color-gray-400). Slight color shift (the hex values were close
  to but not identical to those tokens), so this needs a quick visual
  check.

And reports/print.html.erb had four <span style="color: #666"> elements
on the metric cards. Replaced with class="text-secondary" merged into
the existing tufte-metric-card-change class, so print uses the same
secondary-text color the rest of the app uses.
2026-05-01 22:10:46 +02:00
Alessio Cappa
943a4d007d feat: Add table-divider class and use it in investments summary section (#1463)
* feat: Add table-divider class and use it in investments summary section

* fix: manage dark-theme variant

* feat: apply table-divider to other tables

* refactor: use rem instead of px for table-divider class
2026-04-13 23:29:41 +02:00
Derek Brown
133582ef4d Fix balance sheet dashboard section on iPhone (#1318)
* Update _balance_sheet.html.erb

Update Balance Sheet to work on iPhone Pro Max without scrolling

Signed-off-by: Derek Brown <browndw4@gmail.com>

* Update _group_weight.html.erb

Make the % icon smaller (5 bars not 10) to fit better on smaller format devices

Signed-off-by: Derek Brown <browndw4@gmail.com>

* Update _group_weight.html.erb

Resolve Codex comment

Signed-off-by: Derek Brown <browndw4@gmail.com>

* Update _balance_sheet.html.erb

Increasing width of the weight column

Signed-off-by: Derek Brown <browndw4@gmail.com>

---------

Signed-off-by: Derek Brown <browndw4@gmail.com>
2026-04-07 20:53:04 +02:00
Louis
455c74dcfa Add Binance support, heavily inspired by the Coinbase one (#1317)
* feat: add Binance support (Items, Accounts, Importers, Processor, and Sync)

* refactor: deduplicate 'stablecoins' constant and push stale_rate filter to SQL

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-04-07 14:43:17 +02:00
soky srm
560c9fbff3 Family sharing (#1272)
* Initial account sharing changes

* Update schema.rb

* Update schema.rb

* Change sharing UI to modal

* UX fixes and sharing controls

* Scope include in finances better

* Update totals.rb

* Update totals.rb

* Scope reports to finance account scope

* Update impersonation_sessions_controller_test.rb

* Review fixes

* Update schema.rb

* Update show.html.erb

* FIX db validation

* Refine edit permissions

* Review items

* Review

* Review

* Add application level helper

* Critical review

* Address remaining review items

* Fix modals

* more scoping

* linter

* small UI fix

* Fix: Sync broadcasts push unscoped balance sheet to all users

* Update sync_complete_event.rb

 The fix removes the sidebar broadcasts (which rendered unscoped account groups using family.balance_sheet without user context)
  along with the now-unused sidebar_targets, account_group, and family_balance_sheet private methods.

  The sidebar will still update correctly — when the sync completes, Family::SyncCompleteEvent#broadcast fires family.broadcast_refresh, which triggers a
  morph-based page refresh for each user with their own authenticated session, rendering properly scoped sidebar content.
2026-03-25 10:50:23 +01:00
soky srm
65f439e4da Add appearance setting for 2 column layout (#1285) 2026-03-24 19:19:05 +01:00
soky srm
8dc5c92282 Merge pull request #1210 from lolimmlost/fix/dashboard-touch-hold
Fix dashboard mobile: require press-and-hold to reorder sections
2026-03-24 16:40:00 +01:00
juan
a9e1c221a5 Fix: use interaction state instead of device capability for drag guard
isTouchDevice() blocked mouse/trackpad drag on touch-capable laptops
(e.g. Windows with touchscreen). Now checks if a touch interaction is
actually in progress instead.

Also adds touchcancel binding to clean up hold timer state when the
OS interrupts a touch (notifications, palm rejection, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:07:12 -07:00
Dream
61ee9d34cf Refactor report and dashboard table layouts to semantic HTML (#1222)
* Refactor report and dashboard tables from div grids to semantic HTML

Convert div-based grid layouts to proper <table>/<thead>/<tbody>/<tr>/<th>/<td>
elements in report views and the dashboard investment summary:

- reports/_breakdown_table + _category_row (income/expense breakdown)
- reports/_trends_insights (monthly trends)
- reports/_net_worth (asset/liability summaries)
- reports/_investment_performance (top holdings)
- pages/dashboard/_investment_summary (top holdings)

Replaces shared/ruler dividers with border-b border-divider on <tr> elements.
Updates test selectors from div[data-category] to tr[data-category] and from
[role="columnheader"] to thead/th.

Closes #1121

* Address PR review feedback

- Restore w-max sm:w-full wrapper on report tables to preserve horizontal
  scroll behavior on narrow screens
- Add sr-only accessible header for net worth amount columns
- Use border-divider instead of border-primary in dashboard investment summary

* Fix rounded corners on semantic table body containers

Move rounded-lg, shadow-border-xs, and bg-container from tbody
(where border-radius and box-shadow don't apply) to a wrapper div
with overflow-hidden. Add bg-container-inset on thead to preserve
the two-tone card design.
2026-03-22 11:50:33 +01:00
sokiee
b6b093c578 Extend privacy headers
Extend privacy mode coverage to remaining financial views

  Transfers, trades, valuations, and holdings detail views were missing
  the privacy-sensitive class, leaving amounts visible when privacy mode
  was enabled. Also adds blur to the summary card partial (used by credit
  cards, loans, etc.), account chart balances, and time series chart
  containers (dashboard net worth and per-account charts).
2026-03-22 10:57:56 +01:00
sokiee
9693477761 Merge main: resolve grid layout conflicts, add missing privacy-sensitive coverage
Resolve merge conflicts in investment summary/performance views where main's
grid layout refactoring conflicted with privacy-sensitive class additions.
Also add privacy-sensitive to transaction list amounts, transaction detail
header, and sankey cashflow chart containers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 10:46:52 +01:00
juan
528f5f32a6 Revert "Bind touch events to drag handle only, not entire section"
This reverts commit 55ab9aed6e.
2026-03-18 20:22:45 -07:00
juan
55ab9aed6e Bind touch events to drag handle only, not entire section
Touch events on the full section meant any touch on the header could
trigger hold-to-drag. Now touch events are scoped to the grip handle
button, which is made visible on mobile (was hidden lg:block before).
Desktop drag-and-drop via draggable attribute is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:56:59 -07:00
soky srm
e1ff6d46ee Make categories global (#1160)
* Make categories global

This solves us A LOT of cash flow and budgeting problems.

* Update schema.rb

* Update auto_categorizer.rb

* Update income_statement.rb

* FIX budget sub-categories

* FIX sub-categories and tests

* Add 2 step migration
2026-03-11 15:54:01 +01:00
Alessio Cappa
3b2d630d99 Fix holdings table on mobile (#1114)
* fix: extend width for holdings table in reports

* fix: use right cols for header

* fix: reduce padding on sections

* fix: update holdings table display on dashboard

* feat: set max width for holding name

* fix: remove fixed width on last column

* Update app/views/pages/dashboard/_investment_summary.html.erb

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>

* fix: add check on holding.ticker to ensure it's present

---------

Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-06 10:11:21 +01:00
Chakib
d5d450fc1a Optimize privacy mode: move toggle to breadcrumbs bar, fix flash, improve UX
- Move privacy toggle button from dashboard-only to global breadcrumbs bar
  (desktop) and mobile top nav so it's accessible on every page
- Add _privacy_mode_check.html.erb inline script in <head> to prevent
  flash of unblurred content on first page load (same pattern as dark mode)
- Improve Stimulus controller: add iconOn/iconOff targets to toggle between
  eye/eye-off icons; update aria-pressed on all toggle targets simultaneously
- Remove hover-unblur (blur(4px) on hover) from CSS for security; add
  pointer-events: none to prevent interaction with blurred values
- Move i18n key from pages.dashboard scope to layouts.application scope;
  add privacy_mode translation to all 12 locales (EN, DE, FR, ES, CA,
  NL, NB, PT-BR, RO, TR, ZH-CN, ZH-TW)
2026-03-01 17:46:50 +01:00
Chakib
583d91291f Add privacy mode with blur toggle for financial data
- Stimulus controller on global layout (persists across all pages)
- CSS blur effect on .privacy-sensitive elements (8px active, 4px on hover)
- localStorage persistence survives page navigations
- Toggle button on dashboard with aria-pressed for accessibility
- Applied to 28+ views: accounts, budgets, transactions, reports, etc.

Fixes: UTF-8 encoding, global controller mount, aria-pressed attribute
2026-02-22 23:20:49 +01:00
Alessio Cappa
23087c1e98 feat: Display dashboard as a 2-columns grid on big screens (#1000)
* feat: display 2 columns grid in dashboard for wide screens

* fix: update sortable controller to consider X/Y coordinates

* fix: lint issues + missing variable init
2026-02-16 13:45:49 +01:00
Claude
d9acf19038 Disable Turbo prefetch on dashboard outflow category links
Hovering over category links in the outflow donut chart triggered
Turbo 8's default link prefetching, which made a real request to the
transactions controller. The controller's store_params! before_action
saved those filter params (category + date range) to the session.
When the user later navigated to /transactions via the nav menu,
the stored params were restored, showing an unexpected filtered view.

Adding data-turbo-prefetch="false" prevents the prefetch on hover
while preserving the intended click-to-navigate behavior.

https://claude.ai/code/session_01Na7AF1wyidPwFdPq5w8oaw
2026-02-13 15:46:19 +01:00
StalkerSea
4cf25ada63 fix: restore drawer positioning for transaction modals on desktop (#857) (#896)
* feat: Add responsive dialog behavior for transaction modals

Add responsive option to DS::Dialog component that switches between:
- Mobile (< 1024px): Modal style (centered) with inline close button
- Desktop (≥ 1024px): Drawer style (right side panel) with header close button
Update transaction, transfer, holding, trade, and valuation views to use
responsive behavior, maintaining mobile experience while reverting desktop
to drawer style like budget categories.

Changes:
- app/components/DS/dialog.rb: Add responsive parameter and helper methods
- app/components/DS/dialog.html.erb: Apply responsive styling
- app/views/*/show.html.erb: Add responsive: true and hide close icons on mobile

* fix: Enhance close button accessibility in dialog components

* fix: Refactor dialog component to improve close button handling and accessibility
2026-02-11 00:02:15 +01:00
Juan José Mata
705b5a8b26 First cut of a simplified "intro" UI layout (#265)
* First cut of a simplified "intro" UI layout

* Linter

* Add guest role and intro-only access

* Fix guest role UI defaults (#940)

Use enum predicate to avoid missing role helper.

* Remove legacy user role mapping (#941)

Drop the unused user role references in role normalization
and SSO role mapping forms to avoid implying a role that
never existed.

Refs: #0

* Remove role normalization (#942)

Remove role normalization

Roles are now stored directly without legacy mappings.

* Revert role mapping logic

* Remove `normalize_role_settings`

* Remove unnecessary migration

* Make `member` the default

* Broken `.erb`

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-02-09 11:09:25 +01:00
StalkerSea
a86329d632 PWA: Update manifest, meta tags, and fix UI layout issues (#801)
* pwa(cleanup): enforce LF, head meta + icons, manifest orientation, remove static webmanifest

* pwa(cleanup): add gitattributes, head meta/icons, manifest orientation; remove static manifest; small nav & dashboard fixes

* pwa(cleanup): improve transaction drawer header layout with inline close button

* fix: address PR review feedback

- Add dom_id to transaction header for Turbo Stream updates (Codex)
- Add pending badge next to date when transaction is pending (CodeRabbit)
- Make close button keyboard-focusable by removing tabindex=-1 (CodeRabbit)
- Fix settings nav horizontal scroll with flex-nowrap space-x-1 (CodeRabbit)

* fix: localize 'Linked with Plaid' tooltip string (CodeRabbit)

* Update .gitattributes

Better comment smh

* fix: align transaction/transfer dialog icons and update transfer drawer pattern

- Fix icon alignment in transaction header (items-center instead of items-start)
- Make transfer/linked icons consistent size and color
- Update transfers/show.html.erb to use frame: drawer with hide_close_icon pattern
- Match transfer dialog header layout with transaction details

* fix: enhance header layout

This in the transaction and transfer views, with consistent icon placement

* fix: remove fixed height from HTML document class

basically a regression issue pretty sure

* fix: update dialog rendering to use 'frame' and hide close icon in headers

* fix: update transaction type tabs layout for improved responsiveness

* fix: conditionally render transaction type tabs based on account type
2026-01-29 15:16:49 +01:00
Juan José Mata
5ba051c8cf fix: Broken /terms and /privacy routes (#749)
* fix: replace invalid redirect("about:blank") with proper controller actions

The privacy and terms routes were using redirect("about:blank") which is
invalid because about:blank is a browser-specific pseudo URL, not a valid
HTTP redirect target. This fix replaces them with proper controller actions
that render placeholder pages.

Changes:
- Add privacy and terms actions to PagesController with skip_authentication
- Create privacy.html.erb and terms.html.erb view templates
- Add i18n translations for the new pages
- Update routes to use pages#privacy and pages#terms

https://claude.ai/code/session_01RL36dMda1o6LXGsnGnTJZu

* Make legal routes configurable

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-23 11:39:48 +01:00
Number Eight
0c6d208ef2 feat: implement expandable view for cashflow sankey chart (#739)
* feat: implement expandable view for cashflow sankey chart

* refactor: migrate cashflow dialog sizing to tailwind utilities

* refactor: declarative draggable restore on cashflow dialog close

* refactor: localized title and use Tailwind utilities

* refactor: update dialog interaction especially on mobile

* refactor: add global expand text to localization

* fix: restore draggable immediately after dialog close

* Whitespace noise

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-23 09:52:15 +01:00
soky srm
179552657c Mercury integration (#723)
* Initial mercury impl

* FIX both mercury and generator class

* Finish mercury integration and provider generator

* Fix schema

* Fix linter and tags

* Update routes.rb

* Avoid schema drift

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-22 20:37:07 +01:00
Mark Hendriksen
79d14c142f Remove relative positioning from net worth chart div (#687)
The 'relative' class was removed from the net worth chart container, possibly to resolve layout or stacking issues. The chart should now rely on default positioning.
2026-01-18 11:21:35 +01:00
LPW
0c2026680c Improve investment activity labels UX and add convert-to-trade feature (#649)
* Add `investment_activity_label` to trades and enhance activity label handling

- Introduced `investment_activity_label` column to the `trades` table with a migration.
- Backfilled existing `trades` with activity labels based on quantity (`Buy`, `Sell`, or `Other`).
- Replaced `category_id` in trades with `investment_activity_label` for better alignment with transaction labels.
- Updated views and controllers to display and manage activity labels for trades.
- Added localized badge components for displaying and editing labels dynamically.
- Enhanced `PlaidAccount::Investments::TransactionsProcessor` to assign and process activity labels automatically.
- Added investment flows section to reports for tracking contributions and withdrawals.
- Refactored related tests and models for consistency and to ensure proper validation and filtering.

* Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions

- Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature.
- Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers.
- Streamlined UI for trade and transaction forms by updating dropdown options and label text.
- Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility.
- Adjusted tests and views to align with updated workflows and ensure consistent behavior.

* Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions

- Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature.
- Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers.
- Streamlined UI for trade and transaction forms by updating dropdown options and label text.
- Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility.
- Adjusted tests and views to align with updated workflows and ensure consistent behavior.

* Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions

- Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature.
- Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers.
- Streamlined UI for trade and transaction forms by updating dropdown options and label text.
- Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility.
- Adjusted tests and views to align with updated workflows and ensure consistent behavior.

* Add safeguard for `dropdownTarget` existence in quick edit controller

- Prevent errors by ensuring `dropdownTarget` is present before toggling its visibility.

* Fix undefined method 'category' for Trade on mobile view

Trade model uses investment_activity_label, not category. The upstream
merge introduced a call to trade.category which doesn't exist. Use the
activity label badge on mobile instead.

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

* Fix activity label logic for zero/blank quantity and sell inference

- Return `nil` for blank or zero quantity in `investment_activity_label_for`.
- Correct `is_sell` logic to use the amount’s sign properly in `transactions_controller`.

* Fix i18n key paths in transactions controller for convert_to_trade

- Update flash message translations to use full i18n paths.
- Use `BigDecimal` for quantity and price calculations to improve precision.

---------

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 21:04:10 +01:00
Alessio Cappa
fabd60c098 fix: PWA display issues (#653)
* feat: re-apply PWA changes from previous PR

* feat: adjust padding on pages for mobile

* fix: Add position relative to netWorthChart to avoid overflow issues on mobile

* fix: Add safe-area to progress bar

* feat: add missing class on html

* fix: Replace touch-none with overscroll-none
2026-01-15 12:53:35 +01:00
Alessio Cappa
5f8a0dc78e FIX: Small layout adjustments (#507)
* feat: adjust padding and icon sizing in outflows chart

* fix: Remove extra div in reports page to fix layout issues on mobile

* fix: Padding adjustments to keep pages consistent

* fix: Align report page with main
2026-01-13 13:23:10 +01:00