fire-planner/frontend/src/pages/ScenarioDetail.tsx

425 lines
16 KiB
TypeScript
Raw Normal View History

/**
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
* Plan-tab body Wave 2 chart-first redesign.
*
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
* Layout (chart is the SoT for editing life events):
*
* NW fan + floating stats badges (top-R)
* year-scrubber along the bottom
*
* Spending profile (stacked area)
*
* Event Gantt (drag/click to edit)
*
* Flex rules editor
*
* Drawer: legacy form sections (collapsed)
*
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { api, lifeEventsApi, type Scenario, type SimulateRequest } from '@/api/client';
import { ApiError } from '@/api/client';
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
import { EventGantt } from '@/components/EventGantt';
import { FanChart } from '@/components/FanChart';
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
import { FlexRulesEditor } from '@/components/FlexRulesEditor';
import { GoalsSection } from '@/components/GoalsSection';
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
import { IncomeStreamsSection } from '@/components/IncomeStreamsSection';
import { LifeEventsSection } from '@/components/LifeEventsSection';
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
import { SpendingProfileChart } from '@/components/SpendingProfileChart';
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
import { YearScrubber } from '@/components/YearScrubber';
import { gbp, pct } from '@/lib/format';
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
import { emojiFor } from '@/lib/milestone';
export function ScenarioDetail() {
const params = useParams<{ id: string }>();
const id = Number(params.id);
const navigate = useNavigate();
const qc = useQueryClient();
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
const [searchParams, setSearchParams] = useSearchParams();
const scen = useQuery({
queryKey: ['scenarios', id],
queryFn: () => api.scenarios.get(id),
enabled: Number.isFinite(id),
});
const proj = useQuery({
queryKey: ['scenarios', id, 'projection'],
queryFn: () => api.scenarios.projection(id),
enabled: Number.isFinite(id),
retry: (count, err) => {
if (err instanceof ApiError && err.status === 404) return false;
return count < 2;
},
});
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
const events = useQuery({
queryKey: ['scenarios', id, 'life-events'],
queryFn: () => lifeEventsApi.list(id),
enabled: Number.isFinite(id),
});
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
const profile = useQuery({
queryKey: ['spending-profile', id],
queryFn: () => api.spendingProfile(id),
enabled: Number.isFinite(id),
staleTime: 0,
refetchOnWindowFocus: true,
});
const yearStats = useQuery({
queryKey: ['year-stats', id, parseInt(searchParams.get('year') ?? '0', 10)],
queryFn: () => api.yearStats(id, parseInt(searchParams.get('year') ?? '0', 10)),
enabled: Number.isFinite(id) && proj.isSuccess,
staleTime: 0,
});
const del = useMutation({
mutationFn: () => api.scenarios.delete(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['scenarios'] });
navigate('/scenarios');
},
});
const sim = useMutation({
mutationFn: (req: SimulateRequest) => api.simulate(req),
});
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
const horizonYears = scen.data?.horizon_years ?? proj.data?.yearly.length ?? 60;
const maxYear = (proj.data?.yearly.length ?? horizonYears) - 1;
const yearFromUrl = Number(searchParams.get('year'));
const initialYear = Number.isFinite(yearFromUrl) && yearFromUrl >= 0 ? yearFromUrl : 0;
const [year, setYear] = useState<number>(initialYear);
useEffect(() => {
if (year > maxYear && maxYear >= 0) setYear(maxYear);
}, [maxYear, year]);
const setYearAndUrl = (y: number) => {
setYear(y);
const next = new URLSearchParams(searchParams);
next.set('year', String(y));
setSearchParams(next, { replace: true });
};
const milestones = useMemo(
() =>
(events.data ?? [])
.filter((e) => e.enabled)
.map((e) => ({
year_idx: e.year_start,
emoji: emojiFor(e.kind),
label: e.name,
delta_gbp: Number(e.delta_gbp_per_year) !== 0 ? gbp(e.delta_gbp_per_year) : null,
})),
[events.data],
);
const onDelete = () => {
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
if (!scen.data) return;
if (!confirm(`Delete scenario "${scen.data.name ?? id}"? This can't be undone.`)) return;
del.mutate();
};
const onRunNow = async (s: Scenario) => {
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
const fresh = await lifeEventsApi.list(s.id);
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
const flexRules = readFlexRules(s);
sim.mutate({
jurisdiction: s.jurisdiction,
strategy: s.strategy,
leave_uk_year: s.leave_uk_year,
spending_gbp: s.spending_gbp,
nw_seed_gbp: s.nw_seed_gbp,
savings_per_year_gbp: s.savings_per_year_gbp,
horizon_years: s.horizon_years,
n_paths: 5000,
seed: 42,
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
life_events: fresh.map((e) => ({
year_start: e.year_start,
year_end: e.year_end,
delta_gbp_per_year: e.delta_gbp_per_year,
one_time_amount_gbp: e.one_time_amount_gbp,
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
category: e.category,
enabled: e.enabled,
})),
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
flex_rules: flexRules,
});
};
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
if (!Number.isFinite(id)) return <p className="text-red-700">Invalid scenario id.</p>;
if (scen.isLoading) return <p className="text-slate-500">Loading</p>;
if (scen.isError || !scen.data) {
return (
<div className="rounded-md border border-red-200 bg-red-50 p-4 text-red-800 text-sm">
Couldn&apos;t load scenario {id}.
</div>
);
}
const s = scen.data;
const projection = proj.data;
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
const projection404 =
proj.isError && proj.error instanceof ApiError && proj.error.status === 404;
return (
<section className="space-y-6">
<div className="text-sm">
<Link to="/scenarios" className="text-slate-500 hover:text-slate-900">
Scenarios
</Link>
</div>
<header className="flex items-start justify-between gap-4">
<div>
<h1 className="text-3xl font-semibold tracking-tight">{s.name ?? s.external_id}</h1>
<p className="text-sm text-slate-500 mt-1">
{s.kind} · {s.jurisdiction} · {s.strategy} · leave UK y{s.leave_uk_year} ·{' '}
{s.glide_path} glide · {s.horizon_years}y horizon
</p>
{s.description && <p className="text-sm text-slate-600 mt-2">{s.description}</p>}
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => void onRunNow(s)}
disabled={sim.isPending}
className="rounded-md border border-slate-300 bg-white text-sm px-3 py-1.5 hover:bg-slate-50 disabled:opacity-60"
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
title="Run a fresh MC including this scenario's life events + flex rules"
>
{sim.isPending ? 'Running…' : 'Run now'}
</button>
{s.kind === 'user' && (
<>
<Link
to={`/scenarios/${s.id}/edit`}
className="rounded-md border border-slate-300 bg-white text-sm px-3 py-1.5 hover:bg-slate-50"
>
Edit
</Link>
<button
type="button"
onClick={onDelete}
disabled={del.isPending}
className="rounded-md border border-red-300 text-red-700 text-sm px-3 py-1.5 hover:bg-red-50 disabled:opacity-60"
>
{del.isPending ? 'Deleting…' : 'Delete'}
</button>
</>
)}
</div>
</header>
{del.isError && (
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-red-800 text-sm">
{String((del.error as Error)?.message ?? del.error)}
</div>
)}
{projection ? (
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
<>
{/* NW fan with floating stat badges */}
<div className="relative rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-baseline justify-between mb-2">
<h2 className="text-lg font-semibold">Portfolio fan</h2>
<span className="text-xs text-slate-500">
p10/p50/p90 over {projection.yearly.length}y · {projection.n_paths.toLocaleString()} paths
</span>
</div>
<FanChart
yearly={projection.yearly}
height={460}
showWithdrawal
milestones={milestones}
selectedYear={year}
onSelectYear={setYearAndUrl}
/>
<div className="mt-3">
<YearScrubber value={year} min={0} max={maxYear} onChange={setYearAndUrl} />
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
</div>
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
<FloatingStats
year={year}
maxYear={maxYear}
successRate={projection.success_rate}
p50End={projection.p50_ending_gbp}
netWorth={yearStats.data?.net_worth_p50}
changeNw={yearStats.data?.change_in_nw}
spending={yearStats.data?.spending}
taxes={yearStats.data?.taxes}
effectiveRate={yearStats.data?.effective_tax_rate}
age={yearStats.data?.age ?? null}
calendarYear={yearStats.data?.calendar_year}
/>
</div>
{/* Spending profile */}
<div className="rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-baseline justify-between mb-2">
<h2 className="text-lg font-semibold">Spending profile</h2>
<Legend />
</div>
{profile.data ? (
<SpendingProfileChart
points={profile.data.points}
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
selectedYear={year}
onSelectYear={setYearAndUrl}
/>
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
) : (
<p className="text-sm text-slate-500">Loading</p>
)}
</div>
{/* Interactive Gantt */}
<div className="rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-baseline justify-between mb-2">
<h2 className="text-lg font-semibold">Life events</h2>
<span className="text-xs text-slate-500">
Click empty space to add · drag bars to move · drag edges to resize
</span>
fire-planner: ProjectionLab parity Wave 1 — tabbed shell, year stats, goals, income streams, Sankey cashflow, progress overlay, settings sub-pages Wave 1 (9 features across 4 streams): Stream A — dashboard skeleton 1.A.1 ScenarioShell with top tabs (Plan/Cash Flow/Tax Analytics/Compare/ Reports/Estate/Settings) + left Sidebar with Plans switcher. 1.A.2 GET /scenarios/{id}/year-stats?year=N returning per-year metrics (NW, Δ NW, taxable income, taxes, eff. rate, spending, contribs, investment growth). YearScrubber + YearStatsPanel render the right-hand sidebar; URL ?year= preserves selection. 1.A.3 FanChart gains optional `milestones` prop (lib/milestone.ts maps life_event.kind → emoji) + selectedYear marker line. Stream B — goals + progress 1.B.1 New goals_eval module: target_nw_by_year / never_run_out / target_real_income probability evaluation. Wired into POST /simulate (exact, per-path) and GET /scenarios/{id}/projection (approximated from persisted fan via percentile interpolation). GoalsSection renders pass/fail badges. 1.B.2 GET /scenarios/{id}/progress overlays AccountSnapshot totals on the projection fan; ProgressPage shows variance side-panel. Stream C — income + cashflow 1.C.1 New IncomeStream model + alembic 0003 + CRUD endpoints. Engine aggregates streams into per-year inflows + taxable arrays; income tax routes through the jurisdiction tax engine. IncomeStreamsSection on Plan tab. 1.C.2 GET /scenarios/{id}/cashflow?year=N returns sources/sinks for an ECharts Sankey (sums conserve). CashflowTab body. Stream D — settings 1.D.1 SettingsTab + sub-nav (Milestones/Rates/Dividends/Bonds/Tax/ Metrics/Other/Notes); placeholder cards for unbuilt sub-pages. 1.D.2 LifeEventsSection relocated to /scenarios/:id/settings. 1.D.3 RatesSettings (Fixed/Historical/Advanced segmented + per-asset cards). SimulateRequest gains rates_mode, inflation_pct, stocks/bonds growth + dividend, stocks_allocation. New build_fixed_paths() in simulator. Real-return arithmetic verified against (1+g+d)/(1+i)−1 ≈ 5.4%. 1.D.4 NotesSettings — markdown textarea, save-on-blur, stored in scenario.config_json.notes. Backend: 238 pytest pass (+19 new), mypy + ruff clean. Frontend: typecheck + 7 unit tests + production build clean. Roadmap for Wave 2-N is documented in the implementation plan.
2026-05-10 12:49:44 +00:00
</div>
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
<EventGantt
scenarioId={id}
events={events.data ?? []}
horizonYears={horizonYears}
/>
</div>
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
<FlexRulesEditor scenario={s} />
</>
) : projection404 ? (
<div className="rounded-lg border border-slate-200 bg-white p-8 text-center text-slate-500">
<p className="font-medium text-slate-700">No projection yet.</p>
<p className="text-sm mt-2">
Run <code className="px-1">python -m fire_planner recompute-all</code> or{' '}
<code>POST /recompute</code> to fill in MC projections for all scenarios.
</p>
</div>
) : proj.isLoading ? (
<p className="text-slate-500">Loading projection</p>
) : (
<div className="rounded-md border border-red-200 bg-red-50 p-4 text-red-800 text-sm">
{String((proj.error as Error)?.message ?? proj.error)}
</div>
)}
{sim.data && (
<div className="rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-baseline justify-between mb-3">
<h2 className="text-lg font-semibold">Live preview run</h2>
<span className="text-xs text-slate-500">
{sim.data.elapsed_seconds}s · 5,000 paths · success {pct(sim.data.success_rate)}
</span>
</div>
<FanChart yearly={sim.data.yearly} height={360} showWithdrawal />
</div>
)}
{sim.isError && (
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-red-800 text-sm">
{String((sim.error as Error)?.message ?? sim.error)}
</div>
)}
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
{/* Legacy form sections collapsed by default. The chart UI above
is the primary editor; these stay for bulk edit + accessibility. */}
<details className="rounded-lg border border-slate-200 bg-white">
<summary className="cursor-pointer select-none px-5 py-3 text-sm font-medium text-slate-700">
Form-based editors (income streams · goals · life events table)
</summary>
<div className="p-5 space-y-5 border-t border-slate-100">
<IncomeStreamsSection scenarioId={id} />
<GoalsSection scenarioId={id} probabilities={projection?.goals_probability} />
<LifeEventsSection scenarioId={id} />
</div>
</details>
</section>
);
}
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
function FloatingStats(props: {
year: number;
maxYear: number;
successRate: string;
p50End: string;
netWorth?: string;
changeNw?: string;
spending?: string;
taxes?: string;
effectiveRate?: string;
age: number | null;
calendarYear?: number;
}) {
return (
<div className="absolute top-3 right-3 grid grid-cols-2 gap-2 max-w-xs pointer-events-none">
<Badge label="Year" value={String(props.calendarYear ?? `y${props.year}`)} />
<Badge label="Age" value={props.age != null ? String(props.age) : '—'} />
<Badge label="Net Worth" value={props.netWorth ? gbp(props.netWorth) : '—'} accent />
<Badge
label="Δ NW"
value={props.changeNw ? gbp(props.changeNw) : '—'}
signed={props.changeNw}
/>
<Badge label="Spending" value={props.spending ? gbp(props.spending) : '—'} />
<Badge label="Eff. tax" value={props.effectiveRate ? pct(props.effectiveRate) : '—'} />
</div>
);
}
function Badge({
label,
value,
accent,
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
signed,
}: {
label: string;
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
value: string;
accent?: boolean;
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
signed?: string;
}) {
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
let cls = 'text-slate-700';
if (accent) cls = 'text-emerald-700';
if (signed != null) {
const n = Number(signed);
if (n > 0) cls = 'text-emerald-700';
else if (n < 0) cls = 'text-red-600';
}
return (
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
<div className="rounded-md border border-slate-200 bg-white/90 backdrop-blur px-2 py-1 shadow-sm pointer-events-auto">
<div className="text-[9px] uppercase tracking-wide text-slate-500">{label}</div>
<div className={`text-xs font-semibold tabular-nums ${cls}`}>{value}</div>
</div>
);
}
function Legend() {
return (
<div className="flex items-center gap-3 text-xs">
<Swatch color="rgb(100, 116, 139)" label="Base" />
<Swatch color="rgb(16, 185, 129)" label="Essential" />
<Swatch color="rgb(245, 158, 11)" label="Discretionary" />
<Swatch color="rgb(239, 68, 68)" label="Flex cut" />
</div>
);
}
fire-planner: Wave 2 chart-first — flex spending, categorised life events, interactive Visx Gantt + spending-profile chart Charts are now the primary editor for life events. The Plan-tab body re-orders to make charts ~80% of viewport real-estate; legacy form sections are collapsed into a drawer. Backend: - alembic 0004: life_event.category enum (essential / discretionary / not_spending). Defaults to essential so existing rows keep their full spending impact. - Simulator gains discretionary_outflows + flex_rules params. Tracks per-path running ATH, applies the deepest applicable cut to discretionary outflows when portfolio drops vs ATH (PLab-style flex spending). Cut amount stays in the portfolio (refund pattern). - New flex_spending module with FlexRule + applicable_cut + cuts_per_year (vectorised). Sortable rules; "deepest cut wins" so users specify cumulative cuts at each tier. - New /scenarios/{id}/spending-profile endpoint returning per-year base / essential / discretionary / flex_cut / total breakdown. - SimulateRequest gains flex_rules + life_event.category roundtrip. - 8 new tests; 246 total pytest pass; mypy + ruff clean. Frontend (Visx + ECharts): - Installed @visx/{scale,shape,group,axis,event,responsive,tooltip} for native SVG drag interactions. - New <SpendingProfileChart> — Visx stacked-area of base/essential/ discretionary with red flex-cut overlay, hover tooltip, click-to- scrub-year. - New <EventGantt> — interactive Visx Gantt: * Click empty space → popover create at that year (default essential spending event) * Click a bar → inline edit popover (name, kind, range, £/y, category) with delete button * Drag bar middle → moves the whole event (year-resolution snap) * Drag bar edges → resizes year_start / year_end * All gestures persist via PATCH /life-events/{id} - New <FlexRulesEditor> — list of {from_ath_pct, cut} tiers, save-on- change to scenario.config_json.flex_rules. - Plan-tab redesign: NW fan dominant top with floating stat badges (Year/Age/NW/Δ NW/Spending/Eff. tax) over the chart; spending- profile chart middle; Gantt bottom; flex-rules editor; legacy form sections in a collapsed <details> drawer. - Frontend typecheck + 7 vitest tests + production build all clean.
2026-05-10 16:49:04 +00:00
function Swatch({ color, label }: { color: string; label: string }) {
return (
<span className="flex items-center gap-1.5">
<span className="inline-block w-2.5 h-2.5 rounded-sm" style={{ background: color }} />
<span className="text-slate-600">{label}</span>
</span>
);
}
function readFlexRules(s: Scenario): { from_ath_pct: string; cut_discretionary_pct: string }[] {
const blob = s.config_json as Record<string, unknown>;
const raw = blob?.flex_rules;
if (!Array.isArray(raw)) return [];
return raw
.filter((r): r is Record<string, unknown> => typeof r === 'object' && r !== null)
.map((r) => ({
from_ath_pct: String(r.from_ath_pct ?? 0),
cut_discretionary_pct: String(r.cut_discretionary_pct ?? 0),
}));
}