Path to Freedom — capital pools, retirement model, depletion chart#180
Merged
Conversation
Builds on the v1 /freedom page with five things the original lacked: Capital pools - Two optional pools next to the dividend portfolio: interest-bearing (bonds/real estate/savings) and growth (index funds/growth stocks). Reinvest-interest checkbox controls whether interest compounds during accumulation or counts toward the goal as paid-out income. - Five new persisted columns on users + validations + serializer + ProfilesController params. Distribution model (after goal/retirement) - Capped adaptive withdrawal: each year sell up to the inflation- adjusted shortfall, never more than 4% of capital-at-distribution (inflated each year — SWR ceiling). Healthy retirements get exactly the goal income; lean retirements get passive + 4% with the gap visible as acquisitive-power loss. - Sales come from the growth pool first, then dividend/interest pools proportionally — dividend income stays nominally constant for as long as possible. Retirement-age trigger - Optional motivation_retirement_age input. Combined with the new motivation_birth_year, forces distribution to start at that age even if the dividend goal hasn't been met. Empty distribution-year interpolation when retirement triggers (clean birthday boundary, not sub-year). Total-capital chart with monthly income - New TotalCapitalChart component with dual Y axis: total capital on the left (declines through distribution), monthly income on the right. Vertical rose-dashed marker at the year acquisitive power is first lost. Distribution loop continues past that point until capital hits zero, so the chart shows the full depletion curve. Two new insight stat cards: "Real income lasts" and "Capital lasts". Misc UX - Bumped DEFAULT_MAX_YEARS 60 → 80 so realistic FIRE scenarios have room to deplete on the chart. - X-axis tick selector now uses pixel-based collision detection (LABEL_MIN_PX), prioritising anchors (today/goal/start/end) over step ticks. Fixes overlapping "+37y +38y" / "-2y 0" labels. - Optional motivation_birth_year input renders an "age at this year" secondary row on the X axis of all three charts. - Y-axis labels use trimDecimalZeros so $1.25M and $1.88M don't both render as $1M. Bugfix - PathToFreedomPage had two useMemo calls below an early-return guard, violating rules of hooks. On hard refresh (when React Query cache wasn't pre-warmed) the page rendered blank because the hook count changed between the loading and loaded renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on the v1
/freedompage (PR #179) with five follow-ups:motivation_retirement_age+motivation_birth_year. Forces distribution to start at that age even if the dividend goal hasn't been met — distribution proceeds with whatever passive income exists./freedomrendered blank on hard refresh because twouseMemos sat below an early-return guard. Moved them above the guard so the hook count is stable between loading and loaded renders.Other UX touches
DEFAULT_MAX_YEARS60 → 80 so realistic FIRE projections have room to deplete on the chart.LABEL_MIN_PX) — anchors (today/goal/start/end) win over step ticks. Fixes overlapping+37y +38yand-2y 0labels regardless of span.motivation_birth_yearrenders an "age at this year" secondary row on the X axis of all three charts.trimDecimalZeroskeeps$1.25Mand$1.88Mdistinct (they both used to collapse to$1M).Backend
motivation_interest_capital,motivation_interest_rate_pct,motivation_growth_capital,motivation_growth_rate_pct,motivation_reinvest_interest,motivation_birth_year,motivation_retirement_agetousers. (One short-livedmotivation_birth_datewas replaced bymotivation_birth_yearin the same series — followup migration, not a rollback.)MotivationProjectionService.simulateextended with all the new inputs;distribution_outcomeimplements the capped-adaptive rule. Cache invalidation triggers include every new field.ProfilesControllerpermits + serializes the new fields; the demo bundle showcases all of them.app/frontend/lib/motivation.tsstays in sync.Test plan
bundle exec rspec— full suite still green (716 examples).npx tsc --noEmitandnpx vite buildclean.bin/rubocopandbin/brakemanclean./freedom, fill capital pools + retirement age, observe the three charts update + the new stat cards./freedom(the bug from this PR) — page must render, not blank./demo— same flows work from the prefilled demo profile.🤖 Generated with Claude Code