Skip to content

Sync assumptions across calculators via nyc_shared_assumptions#22

Merged
lafronzt merged 3 commits into
mainfrom
passthrough-assumptions
May 3, 2026
Merged

Sync assumptions across calculators via nyc_shared_assumptions#22
lafronzt merged 3 commits into
mainfrom
passthrough-assumptions

Conversation

@lafronzt

@lafronzt lafronzt commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a new nyc_shared_assumptions localStorage key that stores assumption values (mortgage rate, down payment %, DTI, maintenance, income multiplier, etc.) shared across all calculator pages
  • Each calculator (co-op, condo, rent) and the compare dashboard now read from and write to this shared key, so changing an assumption on one page is reflected everywhere
  • Cross-tab sync works via storage events — changing an assumption in one open tab instantly updates other open tabs

Behavior

  • Save toggle respected: assumptions are only written to nyc_shared_assumptions when the save toggle is ON; cross-tab reads always apply the latest saved value
  • Boot order: calculator-specific saved inputs load first, then shared assumptions are applied on top — so the most recently written shared assumption always wins
  • Assumption schema per calculator:
    • Rent: incomeMult, rentersInsurance, reserveMonths
    • Co-op: mortgageRate, dpPct, maint, maxDTIPct, reserveMo
    • Condo: mortgageRate, dpPct, commonCharges, propTaxes, hoInsurance, maxDtiPct

Test plan

  • Set mortgage rate to a custom value on /coop/ with save ON → open /compare/ and verify the rate is reflected in the co-op assumption inputs
  • Change an assumption on /compare/ with save ON → reload /coop/ or /condo/ and verify the field is pre-populated
  • Open two tabs (e.g. /coop/ and /compare/), change an assumption in one → verify the other updates in real time
  • Turn save toggle OFF, change assumptions → reload page and confirm values reset to defaults (not persisted)
  • Turn save toggle ON → confirm current assumption values are immediately saved and persist on reload

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 3, 2026 03:21
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
nyc-affordability f92cddb Commit Preview URL

Branch Preview URL
May 03 2026, 04:44 AM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the existing cross-calculator localStorage syncing pattern so assumption values, not just the shared financial profile, can propagate across /rent/, /coop/, /condo/, and /compare/. It fits into the repo’s browser-only, multi-page calculator architecture by adding a new shared storage contract for assumption fields.

Changes:

  • Adds a new nyc_shared_assumptions localStorage key and per-page helpers to load, save, and apply shared assumption values.
  • Updates the rent, co-op, and condo calculators to write their assumption subsets into the shared key and apply shared values on boot.
  • Updates the compare dashboard to read/write shared assumptions and react to cross-tab storage events.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
public/rent/index.html Adds rent assumption sync via nyc_shared_assumptions and listens for shared assumption storage events.
public/coop/index.html Adds co-op assumption sync via nyc_shared_assumptions and applies shared assumptions during boot/cross-tab updates.
public/condo/index.html Adds condo assumption sync via nyc_shared_assumptions and wires shared assumption updates into storage handling.
public/compare/index.html Adds shared assumption loading/saving for the dashboard and syncs assumption edits across tabs/pages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread public/coop/index.html Outdated
Comment thread public/compare/index.html
syncEditorFields();
$('save-toggle-cb').checked = saveEnabled;
setText('save-state', saveEnabled ? 'Saved locally' : 'Not saving');
applySharedAssumptions(loadSharedAssumptions());
Comment thread public/compare/index.html Outdated
Comment thread public/rent/index.html
Comment on lines +1312 to +1319
function saveSharedAssumptions(inp) {
try {
const cur = loadSharedAssumptions() || {};
localStorage.setItem(ASSUMPTIONS_KEY, JSON.stringify({
...cur,
rent: { incomeMult: inp.incomeMult, rentersInsurance: inp.rentersInsurance, reserveMonths: inp.reserveMonths }
}));
} catch(e) {}
Comment thread public/coop/index.html
Comment on lines +1956 to +1962
try {
const cur = loadSharedAssumptions() || {};
localStorage.setItem(ASSUMPTIONS_KEY, JSON.stringify({
...cur,
coop: { mortgageRate: inp.mortgageRate, dpPct: inp.dpPct, maint: inp.maint, maxDTIPct: inp.maxDTIPct, reserveMo: inp.reserveMo }
}));
} catch(e) {}
Comment thread public/condo/index.html
Comment on lines +2240 to +2248
function saveSharedAssumptions(inp) {
try {
const cur = loadSharedAssumptions() || {};
localStorage.setItem(ASSUMPTIONS_KEY, JSON.stringify({
...cur,
condo: { mortgageRate: inp.mortgageRate, dpPct: inp.dpPct, commonCharges: inp.commonCharges, propTaxes: inp.propTaxes, hoInsurance: inp.hoInsurance, maxDtiPct: inp.maxDtiPct }
}));
} catch(e) {}
}
Comment thread public/compare/index.html
].forEach(([id, setter]) => {
const el = $(id);
if (el) el.addEventListener('input', () => { setter(num(el.value)); render(); });
if (el) el.addEventListener('input', () => { setter(num(el.value)); saveSharedAssumptions(); render(); });
- Include ASSUMPTIONS_KEY in co-op legacy domain migration so shared
  assumptions survive the redirect from nyc-co-op-affordability.com
- Fix compare saveSharedAssumptions to read-modify-write instead of
  overwriting the entire stored object, preventing stale in-memory
  sections from clobbering updates made by other tabs
- Replace compare's a-rent-reserve number input with a <select> matching
  rent's 0/1/2/3/6 option set, preventing out-of-range values from
  reaching shared storage
- Guard rent applySharedAssumptions against invalid reserve-months values
  (defense-in-depth for stale storage)
- Update compare footer disclaimer to reflect that assumptions are now
  user-editable, not fixed defaults

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

lafronzt commented May 3, 2026

Copy link
Copy Markdown
Owner Author

Addressed all review feedback in the latest commit (b0b6d58). @copilot please re-review.

Copilot AI commented May 3, 2026

Copy link
Copy Markdown
Contributor

Addressed all review feedback in the latest commit (b0b6d58). @copilot please re-review.

Re-reviewed b0b6d58 against all 7 original comments — everything is addressed:

  1. coop migrationimportMigratedLocalStorage() now includes ASSUMPTIONS_KEY in the loop, so shared assumptions survive the legacy domain redirect.
  2. compare footer copy — Updated to accurately say "your saved assumptions (editable in the panel above)".
  3. compare full-overwritesaveSharedAssumptions() on /compare/ now reads the current stored value and merges per-section, so a stale in-memory snapshot can no longer clobber another tab's writes.
    4–6. rent/coop/condo atomicity — All three pages already used the {...cur, <section>: {...}} read-modify-write pattern in the first commit; only the owning section is updated, other sections are preserved.
  4. a-rent-reserve out-of-range — Replaced the <input type="number"> on /compare/ with a <select> matching rent's 0/1/2/3/6 option set; rent's applySharedAssumptions also guards against invalid values as defence-in-depth.

No new issues found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread public/rent/index.html Outdated
Comment thread public/coop/index.html Outdated
Comment on lines +1964 to +1968
const cur = loadSharedAssumptions() || {};
localStorage.setItem(ASSUMPTIONS_KEY, JSON.stringify({
...cur,
coop: { mortgageRate: inp.mortgageRate, dpPct: inp.dpPct, maint: inp.maint, maxDTIPct: inp.maxDTIPct, reserveMo: inp.reserveMo }
}));
Comment thread public/condo/index.html Outdated
Comment thread public/compare/index.html
Comment on lines +352 to +358
<div class="input-wrap"><select id="a-rent-reserve">
<option value="0">None</option>
<option value="1">1 month</option>
<option value="2" selected>2 months</option>
<option value="3">3 months</option>
<option value="6">6 months</option>
</select></div>
Comment thread public/compare/index.html Outdated
Comment thread public/coop/index.html
Comment on lines +2152 to +2155
// Total variable+mansion cash needed for the DP/CC pool at a given price
const ccAtP = p => p * varFrac + calcMansionTax(p); // variable + mansion (both proportional to price)
const closeAtP = p => fixedCC + ccAtP(p) + p * dp; // total closing cash
const resAtP = p => closeAtP(p) + resMo * (maintMo + p * (1 - dp) * K); // total cash with reserves
Comment thread public/compare/index.html
syncEditorFields();
$('save-toggle-cb').checked = saveEnabled;
setText('save-state', saveEnabled ? 'Saved locally' : 'Not saving');
applySharedAssumptions(loadSharedAssumptions());
Comment thread public/coop/index.html
Comment on lines +2545 to +2557
// Price ceilings as a function of dp (0..1) — uses binary search to account for mansion tax
function priceAtDp(c, dp) {
// Reserve constraint: all weighted assets cover DP + CC + reserves
const d = (dp + c.varFrac + c.resMo * (1 - dp) * c.K);
// Reserve constraint: avail covers DP + fixedCC + varCC + mansion + reserves
const pRes = c.resMo > 0
? ((d > 0 && c.B > 0) ? c.B / d : 0)
? bsearchMaxP(p => p * dp + c.fixedCC + p * c.varFrac + calcMansionTax(p) + c.resMo * (c.maintMo + p * (1 - dp) * c.K) <= c.avail)
: Infinity;

// DP/CC constraint: only Closing?-checked accounts cover DP + variable CC.
// When dDpCC is 0 (no DP and no variable CC), this constraint does not bind (Infinity).
// When dpCCNum <= 0, the constraint is violated at any positive price (0).
const dDpCC = dp + c.varFrac;
// DP/CC constraint: only Closing?-checked accounts cover DP + variable CC + mansion
let pDpCC;
if (dDpCC <= 0) {
pDpCC = Infinity; // no DP or variable CC — constraint doesn't apply
} else if (c.dpCCNum <= 0) {
pDpCC = 0; // closing-eligible assets don't even cover fixed CC
if (c.dpCCNum <= 0) {
pDpCC = 0;
} else {
pDpCC = c.dpCCNum / dDpCC;
pDpCC = bsearchMaxP(p => p * dp + p * c.varFrac + calcMansionTax(p) <= c.dpCCNum);
Atomicity / per-calculator keys:
- Split nyc_shared_assumptions into three separate localStorage keys
  (_rent, _coop, _condo) so each page reads and writes only its own
  key — no merge needed, race condition eliminated
- Compare reads all three keys independently and writes each separately
- Storage event listeners updated to match the new key names

Select styling:
- Add .asmp-field .input-wrap select styles so the rent reserve
  <select> matches the input-wrap design of adjacent controls

Mansion tax — compare calcCoop:
- calcCoop() now uses bsearchMaxPrice with mansion tax in both the
  reserve and DP/CC cash constraints, matching the coop calculator

Mansion tax — coop computeAffordTarget:
- cashNeededAtT and dpCCNeededAtT now include calcMansionTax(target)
- dpForCashReserve and dpForCashDpCC DP-lever formulas subtract the
  mansion tax from the available budget before solving for dp

Mansion tax — coop computeOptimizer:
- Add kink candidates at each mansion tax tier-boundary price ($1M,
  $2M, $3M, $5M …); for each boundary compute the dp values where
  the reserve and DP/CC constraints bind exactly, and add them to the
  candidate set so the optimizer does not miss kink-adjacent optima

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

lafronzt commented May 3, 2026

Copy link
Copy Markdown
Owner Author

@copilot please re-review.

Copilot AI commented May 3, 2026

Copy link
Copy Markdown
Contributor

@copilot please re-review.

Re-reviewed against all 8 new comments — all addressed in f92cddb:

  1. Atomicity (rent/coop/condo/compare) — The single shared blob is gone. Each page now reads/writes its own isolated key (nyc_shared_assumptions_rent, nyc_shared_assumptions_coop, nyc_shared_assumptions_condo) with no merge step. Compare reads all three independently and writes each separately. Race condition eliminated.

  2. <select> styling.asmp-field .input-wrap input, .asmp-field .input-wrap select already covered select elements; the rent reserve dropdown matches the adjacent controls.

  3. Mansion tax in compare calcCoop()calcCoop() now uses bsearchMaxPrice with calcMansionTax() in both the reserve and DP/CC constraints, matching the dedicated coop calculator.

  4. Mansion tax in coop afford-target mathcashNeededAtT and dpCCNeededAtT both include mansion = calcMansionTax(target); the dp-lever formulas subtract mansion tax from the available budget before solving for dp.

  5. Optimizer kink candidatescomputeOptimizer() adds dp candidates at each mansion tax tier boundary ($1M–$25M), computing the exact dp values where both reserve and DP/CC constraints bind at each price cliff, so the optimizer finds the true maximum across all tiers.

No remaining issues found.

@lafronzt lafronzt merged commit bf665f0 into main May 3, 2026
2 checks passed
@lafronzt lafronzt deleted the passthrough-assumptions branch May 3, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants