chore(goals): drop dead V1 hooks + surface chart errors

Loose ends from the V1 → V2 refactor that the architecture commit
didn't sweep.

- Demo generator (B14): the `goal_spec[:contributions]` arrays
  + the `wedding_contribs` / `house_contribs` builders still
  shipped in the file, but the seeding loop that consumed them
  was deleted alongside `GoalContribution`. Dead data. Strip both
  the per-goal arrays and the two locals. Goal balance/pace in
  the demo family now derives from the linked depository
  accounts' own seeded entries elsewhere in the generator.

- Goal stepper controller (B16): the `static targets` declaration
  still listed `initialContributionAmount` and
  `initialContributionAccountSelect`, and `refreshAccountSelect`
  + its two callsites still ran every time a linked-account
  checkbox flipped. The HTML targets disappeared with the V2
  stepper rebuild, so `has*Target` guards short-circuited and the
  method was a no-op — but it was still dispatched on every
  change. Drop the targets, the method, and the two callsites.

- Chart series rescue (B25): `Goal#balance_series_values` and
  `FundingAccountsBreakdownComponent#sparkline_map` both swallowed
  `StandardError` with a `Rails.logger.warn(…)`. The chart then
  degraded to "target line only" silently. Promote the log to
  `error` level and forward to Sentry when present (matching the
  pattern in `Account::Syncer`, `Sync`, `PlaidItem`). Fallback to
  empty result still preserved so the surface degrades instead of
  500-ing.
This commit is contained in:
Guillem Arias
2026-05-14 19:48:32 +02:00
parent 4a46a90a88
commit 5530ff5f06
4 changed files with 20 additions and 85 deletions

View File

@@ -105,7 +105,8 @@ class Goals::FundingAccountsBreakdownComponent < ApplicationComponent
result
end
rescue StandardError => e
Rails.logger.warn("Sparkline map for goal #{goal.id} failed: #{e.message}")
Rails.logger.error("Sparkline map for goal #{goal.id} failed: #{e.class}: #{e.message}")
Sentry.capture_exception(e) if defined?(Sentry)
{}
end
end

View File

@@ -3,8 +3,8 @@ import { Controller } from "@hotwired/stimulus";
// 2-step modal stepper for creating a goal.
//
// Single <form> with two panels. Step 1 collects identity (name, amount,
// date, color, notes, linked accounts). Step 2 reviews + optional initial
// contribution. All state lives in the DOM — no half-records, single POST.
// date, color, notes, linked accounts). Step 2 reviews and submits. All
// state lives in the DOM — no half-records, single POST.
export default class extends Controller {
static targets = [
"step1Panel",
@@ -22,8 +22,6 @@ export default class extends Controller {
"amountError",
"accountsError",
"linkedAccountCheckbox",
"initialContributionAmount",
"initialContributionAccountSelect",
"reviewName",
"reviewSummary",
"reviewSuggested",
@@ -85,7 +83,6 @@ export default class extends Controller {
this.step1PanelTarget.classList.add("hidden");
this.step2PanelTarget.classList.remove("hidden");
this.updateStepperState();
this.refreshAccountSelect();
this.updateReview();
this.updateFooter();
}
@@ -99,7 +96,6 @@ export default class extends Controller {
}
linkedAccountChanged() {
this.refreshAccountSelect();
this.refreshSubmitState();
this.updateReview();
if (this.linkedAccountCheckboxTargets.some((cb) => cb.checked) && this.hasAccountsErrorTarget) {
@@ -169,26 +165,6 @@ export default class extends Controller {
this.footerRightButtonTarget.classList.toggle("opacity-50", !anyChecked && this.currentStep === 1);
}
refreshAccountSelect() {
if (!this.hasInitialContributionAccountSelectTarget) return;
const select = this.initialContributionAccountSelectTarget;
const previous = select.value;
while (select.options.length > 1) select.remove(1);
this.linkedAccountCheckboxTargets
.filter((cb) => cb.checked)
.forEach((cb) => {
const opt = document.createElement("option");
opt.value = cb.value;
opt.textContent = cb.dataset.accountName || cb.value;
select.appendChild(opt);
});
if ([...select.options].some((o) => o.value === previous)) {
select.value = previous;
}
}
updateStepperState() {
if (this.hasStep1CircleTarget) {
this.step1CircleTarget.classList.toggle("bg-inverse", this.currentStep === 1);

View File

@@ -1285,102 +1285,57 @@ class Demo::Generator
currency = depository_accounts.first.currency
eligible = depository_accounts.select { |a| a.currency == currency }
primary = eligible.first
secondary = eligible[1] || primary
# Build "Wedding fund" on_track contributions: target 12 months out,
# $200/mo required, demo contributes $220/mo for last 6 months → on
# pace.
wedding_contribs = (0..5).map do |i|
{ amount: 220, source: i.zero? ? "initial" : "manual", days_ago: 30 * (6 - i), account: primary }
end
# "House downpayment" gets a fuller contribution history so the
# scrollable list has real density.
house_contribs = [
{ amount: 5_000, source: "initial", days_ago: 365, account: primary },
{ amount: 750, source: "manual", days_ago: 330, account: primary },
{ amount: 750, source: "manual", days_ago: 300, account: secondary },
{ amount: 750, source: "manual", days_ago: 270, account: primary },
{ amount: 750, source: "manual", days_ago: 240, account: primary },
{ amount: 750, source: "manual", days_ago: 210, account: secondary },
{ amount: 750, source: "manual", days_ago: 180, account: primary },
{ amount: 750, source: "manual", days_ago: 150, account: primary },
{ amount: 750, source: "manual", days_ago: 120, account: secondary },
{ amount: 750, source: "manual", days_ago: 90, account: primary },
{ amount: 750, source: "manual", days_ago: 60, account: primary },
{ amount: 750, source: "manual", days_ago: 30, account: secondary }
]
# V2 goals derive balance + pace from the linked depository accounts
# directly; the demo's contribution arrays were V1 ledger seed data
# and have nothing to consume them now. Account-level transaction
# seeding (paychecks, etc.) elsewhere in this generator already
# populates the goal pace/balance.
goals = [
{
name: "Vacation in Italy",
target: 5_000,
target_date: 4.months.from_now.to_date,
accounts: eligible.first(2),
contributions: [
{ amount: 500, source: "initial", days_ago: 90, account: primary },
{ amount: 250, source: "manual", days_ago: 60, account: primary },
{ amount: 250, source: "manual", days_ago: 30, account: secondary }
]
accounts: eligible.first(2)
},
{
name: "Wedding fund",
target: 2_400,
target_date: 6.months.from_now.to_date,
accounts: eligible.first(2),
contributions: wedding_contribs
accounts: eligible.first(2)
},
{
name: "Emergency fund",
target: 10_000,
target_date: nil,
accounts: [ primary ],
contributions: [
{ amount: 1_000, source: "initial", days_ago: 180, account: primary },
{ amount: 250, source: "manual", days_ago: 60, account: primary },
{ amount: 250, source: "manual", days_ago: 30, account: primary }
]
accounts: [ primary ]
},
{
name: "House downpayment",
target: 50_000,
target_date: 24.months.from_now.to_date,
accounts: eligible.first(2),
contributions: house_contribs
accounts: eligible.first(2)
},
{
name: "Sabbatical",
target: 15_000,
target_date: 18.months.from_now.to_date,
state: "paused",
accounts: [ primary ],
contributions: [
{ amount: 1_500, source: "initial", days_ago: 200, account: primary },
{ amount: 500, source: "manual", days_ago: 150, account: primary }
]
accounts: [ primary ]
},
{
name: "Old laptop fund",
target: 1_500,
target_date: 12.months.ago.to_date,
state: "archived",
accounts: [ primary ],
contributions: [
{ amount: 400, source: "initial", days_ago: 540, account: primary }
]
accounts: [ primary ]
},
{
name: "Paid-off car",
target: 8_000,
target_date: 6.months.ago.to_date,
state: "completed",
accounts: [ primary ],
contributions: [
{ amount: 2_000, source: "initial", days_ago: 730, account: primary },
{ amount: 2_000, source: "manual", days_ago: 600, account: primary },
{ amount: 2_000, source: "manual", days_ago: 450, account: primary },
{ amount: 2_000, source: "manual", days_ago: 300, account: primary }
]
accounts: [ primary ]
}
]

View File

@@ -297,7 +297,10 @@ class Goal < ApplicationRecord
period: Period.last_90_days
).balance_series.values
rescue StandardError => e
Rails.logger.warn("Goal##{id} balance series failed: #{e.message}")
# Degrade gracefully (chart drops to target-line-only) but surface
# the failure — silent fallbacks here masked real Builder bugs.
Rails.logger.error("Goal##{id} balance series failed: #{e.class}: #{e.message}")
Sentry.capture_exception(e) if defined?(Sentry)
[]
end