Unlock BellPathâ„¢
Enter your 4-digit PIN
 
\n
BellPathâ„¢ by DSB
BellPathâ„¢ INVESTING BY DSB
LIVE
📊 DEMO DATA — not real prices
Not yet refreshed
ðŸâ€Â³
Debt Resolution Status
SYNCED FROM BELLPATH DEBT APP
Balance
—
Loading…
12M
+18.4%
vs S&P · +2.1%
Today
+$1,847
+1.01%
Beta
1.12
Moderate
Sharpe
1.34
Above bench
Market Mood
Fear & Greed
—
—
—
Drawdown Coach
—
Goals
—
Value · 12M
—
Sectors
Daily P&L
Drift
Updated now
Holdings
TickerSectorSh. PriceValueCost P&L%24h
P
Find an asset
Search 200+ stocks, ETFs, crypto, bonds · Type ticker, name, or sector
🔍
What's this?
How to use: Search for any ticker ↆenter shares and your cost basis ↆclick Buy to add to portfolio, or Track to watch without owning. Toggle "Previously held" to log a closed position for tax tracking.
$0
0 positions · $0 unrealized
—
total
Q
Market Headlines
Loading…
Markets
S&P 500 notches third consecutive week of gains on tech rally
+1.2%
Technology
NVIDIA reports record data center revenue in latest quarter
+4.5%
Fixed Income
Treasury yields rise on stronger-than-expected jobs report
-0.8%
Commodities
Gold ETFs see record inflows as investors seek safe havens
+2.1%
ETFs
Retail investors pour into dividend ETFs amid volatility
+0.6%
Crypto
Bitcoin holds above key support level after brief selloff
+1.8%
Healthcare
Healthcare sector outperforms on FDA approval news
+1.4%
Energy
Oil prices stabilize after OPEC+ production meeting
+0.9%
Markets
Small-cap stocks rally on improving economic outlook
+2.3%
Market news provided by Finnhub.io · For informational purposes only · Not investment advice
$
Buying Power
$100,000
Available cash
Equity
$0
Market value of holdings
Account Value
$100,000
Cash + equity
All-Time P&L
$0
0%
Trades
0
Total executed
How to use: Search for any asset ↆchoose Market or Limit order ↆset quantity ↆpreview cost ↆSubmit. Practice strategies risk-free. Click Reset to restart at $100K. Your paper account is saved separately from your real portfolio.
Order Ticket
ORDER PREVIEW
Enter ticker and quantity to see preview
Open Positions
Trade History
0 trades
TimeSideTickerQtyPriceTotalType
G
FINANCIAL HEALTH
—
—

Your savings targets

Loading…
Total saved
$0
—
Total target
$0
Across all goals
Progress
0%
Aggregate
Status
—
—
Monthly in
$0
Combined contributions
Allocation by Goal
Projected Timeline
At current contribution rate
ðŸâ€Â¡ 3 Financial Plans to Reach Your Goals
AI-generated frameworks · Conservative · Balanced · Aggressive
Click Generate Plans to see three customized financial roadmaps based on your active goals — conservative, balanced, and aggressive — each with specific action steps.
Growth Simulator
Monthly contribution $500
Starting balance$10,000
Years to grow20 yrs
Annual return (%)7%
Inflation (%)2.5%
Final Value
—
Contributed
—
Interest Earned
—
Projection Chart
Goal Planner
Back-solve monthly contribution from target · Save as tracked goal
Enter goal details to see your required monthly contribution
What-If Scenario Planner
Model how buying or selling changes your long-term outlook
Contribution Tracker
Log contributions · Running totals · Distribution suggestions
Log every investment contribution. BellPathâ„¢ tracks your running total against your goals and suggests how to distribute new money for maximum impact.
Total Contributed
$0
This Month
$0
This Year
$0
Avg / Month
$0
Distribution Suggestion
How to allocate your next contribution for maximum impact
DateAmountGoal/AccountNotes
Payroll & Tax Calculator
Full paycheck breakdown · Federal + State + FICA · Pre/post-tax deductions
How to use: Enter your pay information. BellPath™ calculates exact take-home pay, all tax withholdings, and projects your annual financial picture. All data is saved locally — nothing is sent anywhere.
PRE-TAX DEDUCTIONS
2026 limits: 401k/403b/TSP $23,500 · SIMPLE IRA $16,500 · SEP 25% of comp
▶ SPECIAL PAY — Overtime · Night Diff · Holiday · Violations click to expand
POST-TAX DEDUCTIONS
EMPLOYER CONTRIBUTIONS
e.g. "4" % means employer matches 4% of your salary
Budget & Cash Flow Optimizer
50/30/20 framework · Optimize money flow · Maximize growth
Based on your payroll data, BellPathâ„¢ builds a recommended budget and shows you exactly where to redirect dollars for maximum wealth-building impact.
MONTHLY EXPENSES
DEBT PAYMENTS
SAVINGS & INVESTMENTS
From your payroll data:
Enter payroll info on the Payroll tab first.
Financial Health Score
Comprehensive gauge of your financial adherence and progress
C
Diversification Score
Quality of portfolio spread · Updated live
—
—

Calculating…

Analyzing concentration, sector spread, and risk balance.
High-Growth Picks for Your Portfolio
Aggressive gainers ranked · Picks fill gaps in your sector exposure
How to use: These picks are ranked by recent price momentum and selected to diversify into sectors you're underweight in. Click any ticker to add it directly to your portfolio. Aggressive growth assets carry higher volatility — use position-sizing accordingly.
Rebalance Assistant
Exact trades to hit your target weights
Extra $ /mo
How to use: Click Compute to see specific trades needed. Each row shows a recommended order type based on trade size. Click Auto-Rebalance to instantly apply all trades to your portfolio (simulated).
Total drift
—
Cumulative absolute
Trades needed
—
Buy + sell actions
Buy total
$0
Across all buys
Sell total
$0
Across all sells
Partial Rebalance Entry
Enter how much you bought or sold for each position this period
Can't rebalance the whole portfolio at once? Enter exactly what you bought (+) or sold (âˆâ€) for each position. BellPathâ„¢ updates your holdings and recalculates drift.
Fee Impact Analyzer
How much an expense ratio costs you over decades
Low fee · 0.03%
$—
Vanguard-style index
Mid fee · 0.50%
$—
Typical ETF
High fee · 1.50%
$—
Active mutual fund
Lesson: A 1% fee may sound trivial, but over 30 years on a meaningful balance it can erase 30–40% of your final wealth. Always check expense ratios before buying any fund.
Compare Stocks
Side-by-side comparison · up to 12 tickers
How to use: Type any ticker and press Enter or click Add. Compare up to 12 assets across P/E, market cap, dividend yield, beta, and 1Y/5Y returns. Click âÅ"• on any card to remove it.
Glossary
Plain-English definitions · Hover any underlined term throughout the app
M
Status
ACTIVE
Watching
Last Scan
Just now
Next in 30s
Alerts
0
Active triggers
Asset Trip
10%
Per-position
Sector Trip
15%
Per-sector
Rules
Active Alerts
—
Audit log
Daily feed · JSON
How to use this feed: The reporting engine produces a daily JSON snapshot of your portfolio including 12-month value trend, sector allocations, drift vs targets, position-level data, and active alerts. Use the Regenerate button to refresh on demand, or Download to save the JSON file. Connect it to your S3/Lambda/CRON pipeline for automated daily emails, dashboards, or compliance archives.
—

  
T
Upload CSV
📂
Drop CSV or click to browse
ticker, shares, cost_basis, current_price, held_days
Settings
Lots
Upload a CSV or load demo data to begin
Impact
Select lots above to model impact
Wash-Sale Rule: Repurchasing substantially identical securities within 30 days voids the loss deduction. Consult a licensed tax advisor before executing these trades.
G
Inputs
Add$500
Start$10,000
Years20 yrs
Return7.0%
Inflation2.5%
Fees0.10%
What-if
4% vs 10%
Projection
By year
YearInGrowthGrossReal
Compounding Edge: The gap between 4% and 10% over 30 years on $500/month is often 3–5Ãâ€" your final balance. Start early, minimize fees, stay invested.
Goal Planner
Back-solve required monthly contribution from your target
How to use: Enter what you want to save toward and by when. The tool back-solves how much you need to contribute monthly. Click Save as Tracked Goal to add it to the Goals tab where you can monitor progress.
You need to invest
$—
Per month
Total contributions
$—
Out of pocket
Growth from investing
$—
Compounding magic
Reality check: If the required monthly figure looks impossible, push the time horizon out, lower the target, or raise expected return (but never assume more than 8–10% for stocks long-term).
DCA vs Lump Sum
When does DCA beat investing all at once?
Lump Sum final
$—
—
DCA final
$—
—
The behavioral edge: Studies show lump-sum wins ~67% of the time mathematically, but DCA wins almost 100% of the time emotionally — fewer panic sells, easier to start, smoother sleep. Re-roll the market a few times to see the dispersion of outcomes.
Retirement Readiness
Based on the 25Ãâ€" rule
Need to retire
$—
25Ãâ€" expenses
You'll have
$—
At retirement age
Gap
—
Surplus or shortfall
Readiness
—
—
BEHINDON TRACKAHEAD
Dividend Income
Projected income from your holdings · Forward 10 years
Annual income
$—
At current rate
Monthly average
$—
Estimated
Yield on cost
—%
Income ÷ basis
10-yr cumulative
$—
If reinvested
TickerSh.YieldAnnualMonthly5-yr Total
What-If Scenario Planner
Model how buying or selling changes your long-term outlook
How to use: Enter ticker, choose action, set shares and horizon. BellPath™ compares your current projected value vs the what-if — great for position sizing decisions.
ðŸâ€Â¡ Financial Literacy Hub
Plain English investing education · Click any topic
📚 Recommended Resources
Books: The Little Book of Common Sense Investing · I Will Teach You to Be Rich · The Psychology of Money
Free: Khan Academy Personal Finance · Coursera Financial Markets (Yale) · SEC.gov Investor Education · FINRA.org
BellPathâ„¢ is educational only. For personalized advice consult a licensed CFP, RIA, or CPA.
`); w.document.close(); w.focus(); setTimeout(function(){ try{ w.print(); }catch(e){} }, 500); } // ── WHAT-IF SCENARIOS ───────────────────────────────── function runWhatIf() { const ticker = document.getElementById('wi-ticker')?.value?.trim()?.toUpperCase(); const action = document.getElementById('wi-action')?.value; const shares = parseFloat(document.getElementById('wi-shares')?.value)||0; const horizon = parseInt(document.getElementById('wi-horizon')?.value)||12; const returnRate = parseFloat(document.getElementById('wi-return')?.value)||7; const el = document.getElementById('wi-result'); if(!el) return; if(!ticker||shares<=0) { el.innerHTML='
Enter ticker, shares, and horizon to see projection.
'; return; } const info = typeof getTickerInfo==='function' ? getTickerInfo(ticker) : null; if(!info && action!=='remove') { el.innerHTML=`
Ticker "${ticker}" not found.
`; return; } const price = info?.p || 0; const tradeValue = shares * price; const currentTV = totalValue || 0; let projectedTV, newTV; if(action==='buy') { newTV = currentTV + tradeValue; } else if(action==='sell' || action==='remove') { const pos = portfolio.find(p=>p.t===ticker); const posVal = pos ? pos.shares*(info?.p||pos.basis||0) : 0; newTV = Math.max(0, currentTV - Math.min(tradeValue, posVal)); } else { newTV = currentTV; } // Project forward const r = returnRate/100/12; const n = horizon; projectedTV = newTV * Math.pow(1+r, n); const currentProjected = currentTV * Math.pow(1+r, n); const diff = projectedTV - currentProjected; const diffPct = currentProjected>0 ? ((diff/currentProjected)*100).toFixed(1) : '0'; el.innerHTML = `
CURRENT SCENARIO
$${Math.round(currentProjected).toLocaleString()}
in ${horizon} months @ ${returnRate}%
WHAT-IF SCENARIO
$${Math.round(projectedTV).toLocaleString()}
in ${horizon} months @ ${returnRate}%
${diff>=0?'+':''}\$${Math.abs(Math.round(diff)).toLocaleString()} (${diff>=0?'+':''}${diffPct}%)
${action==='buy'?'Buying '+shares+' shares of '+ticker+' adds':'Selling '+shares+' shares of '+ticker+' removes'} this ${diff>=0?'upside':'value'} over ${horizon} months
Educational only · Assumes constant ${returnRate}% annual return · Not investment advice
`; } // ── TICKER QUOTE LOOKUP ─────────────────────────────── /* ── QUOTE LOOKUP AUTOCOMPLETE ── */ let quoteSearchResults = []; function onQuoteSearch(val) { const q = val.trim().toUpperCase(); const dd = document.getElementById('quoteDropdown'); if(!dd) return; if(!q || q.length < 1) { dd.classList.remove('open'); return; } quoteSearchResults = TICKER_DB.filter(t => t.t.startsWith(q) || t.t.includes(q) || t.n.toUpperCase().includes(q) || t.s.toUpperCase().includes(q) ).sort((a,b) => { // Prioritize exact ticker match, then starts-with, then contains const aExact = a.t === q ? 0 : a.t.startsWith(q) ? 1 : 2; const bExact = b.t === q ? 0 : b.t.startsWith(q) ? 1 : 2; return aExact - bExact; }).slice(0, 10); if(!quoteSearchResults.length) { dd.classList.remove('open'); return; } dd.innerHTML = quoteSearchResults.map((t,i) => `
${t.t}
${t.n}
${t.x ? `${t.x}` : ''}${t.s}
$${t.p.toFixed(2)}
${t.c>=0?'+':''}${t.c.toFixed(2)}%
`).join(''); dd.classList.add('open'); } function selectQuoteTicker(i) { const t = quoteSearchResults[i]; if(!t) return; const input = document.getElementById('quoteInput'); if(input) input.value = t.t; const dd = document.getElementById('quoteDropdown'); if(dd) dd.classList.remove('open'); lookupTicker(); } function onQuoteKey(e) { const dd = document.getElementById('quoteDropdown'); if(e.key === 'Escape') { if(dd) dd.classList.remove('open'); return; } if(e.key === 'Enter') { if(dd) dd.classList.remove('open'); lookupTicker(); return; } if(e.key === 'ArrowDown') { const items = dd?.querySelectorAll('.search-result-item'); if(items?.length) { e.preventDefault(); items[0].focus(); } } } // Close quote dropdown when clicking outside document.addEventListener('click', function(e) { const dd = document.getElementById('quoteDropdown'); const input = document.getElementById('quoteInput'); if(dd && !dd.contains(e.target) && e.target !== input) { dd.classList.remove('open'); } }); function lookupTicker() { // Show/hide the demo warning based on provider const pill = document.getElementById('quoteInfoPill'); const src = document.getElementById('quoteDataSource'); const hasLive = liveDataSettings?.provider && liveDataSettings.provider !== 'demo'; if(pill) pill.style.display = hasLive ? 'none' : 'block'; if(src && hasLive) src.textContent = `Live · ${liveDataSettings.provider} · ${new Date().toLocaleTimeString()}`; const t = document.getElementById('quoteInput')?.value?.trim()?.toUpperCase(); const el = document.getElementById('quoteResult'); if(!el) return; if(!t) { el.innerHTML=''; return; } const info = typeof getTickerInfo==='function' ? getTickerInfo(t) : null; if(!info) { el.innerHTML=`
Ticker "${t}" not found in database.
For live prices, connect a data provider in Settings.
`; return; } // Simulate bid/ask const spread = info.p * 0.0002; const bid = (info.p - spread).toFixed(2); const ask = (info.p + spread).toFixed(2); const vol = (Math.random()*50+5).toFixed(2)+'M'; const mktCap = info.p > 1000 ? (info.p * (Math.random()*2+0.5)).toFixed(0)+'B' : info.p > 100 ? (info.p * (Math.random()*20+5)).toFixed(0)+'B' : (info.p * (Math.random()*500+100)).toFixed(0)+'M'; const pe = info.s==='ETF'||info.s==='Fixed Income'||info.s==='Crypto'?'—':(info.p/(info.p*0.04+1)).toFixed(1); const div = info.s==='Fixed Income'?'4.2%':info.s==='ETF'?'1.6%':info.s==='Crypto'?'—':'1.8%'; el.innerHTML=`
${info.t}
${info.n}
${info.s} · ${info.x||''}
$${info.p.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}
${info.c>=0?'+':''}${info.c.toFixed(2)}% today
${[['Bid',(+bid).toLocaleString(undefined,{minimumFractionDigits:2})],['Ask',(+ask).toLocaleString(undefined,{minimumFractionDigits:2})],['Volume',vol],['Mkt Cap',mktCap],['P/E',pe],['Div Yield',div]].map(([l,v])=>`
${l}
${v}
`).join('')}
⚠ Demo prices · Connect a data provider for live quotes
`; } function addQuoteToPortfolio(ticker) { const info = getTickerInfo(ticker); if(!info) return; selectedTickerObj = info; document.getElementById('tickerSearch').value = ticker + ' — ' + info.n; document.getElementById('costBasisInput').value = info.p.toFixed(2); showPage('portfolio'); showToast('Selected ' + ticker + ' · Enter shares on Positions tab'); } function addSelectedToWatchlistByTicker(ticker) { const info = getTickerInfo(ticker); if(!info) return; if(!watchlist.find(w=>w.t===ticker)) { watchlist.push({t:info.t, n:info.n, s:info.s, p:info.p, c:info.c, addedAt:Date.now()}); renderWatchlist(); showToast('👁 '+ticker+' added to watchlist'); } else { showToast('Already in watchlist'); } } // ── GOALS: 3 FINANCIAL PLANS ────────────────────────── function generateGoalPlans() { const el = document.getElementById('goalPlansOutput'); if(!el) return; if(!goals.length) { el.innerHTML='
Add goals first, then generate plans.
'; return; } const totalNeeded = goals.reduce((s,g)=>s+Math.max(0,g.target-g.current),0); const totalMonthly = goals.reduce((s,g)=>s+(g.monthly||0),0); const avgYears = goals.length ? goals.reduce((s,g)=>{ const now=new Date(), target=new Date(g.targetDate); return s+Math.max(0.1,(target-now)/(1000*60*60*24*365)); },0)/goals.length : 10; const plans = [ { name:'Conservative',color:'#4ade80',icon:'🛡', desc:'Low risk, steady progress. Prioritize guaranteed savings accounts and short-term bonds.', steps:[ `Build a ${(totalMonthly*0.3).toFixed(0)}/mo emergency buffer in HYSA (5%+ yield)`, `Allocate 60% bonds, 30% dividend stocks, 10% cash equivalents`, `Expected portfolio growth: 4-5% annually`, `Total needed: $${totalNeeded.toLocaleString()} · Timeline: ${(avgYears*1.2).toFixed(1)} yrs at this pace`, 'Max contribution tax-advantaged: IRA ($7,000/yr) + HSA ($4,150/yr)', ] }, { name:'Balanced',color:'#c9a227',icon:'âšâ€"️', desc:'Moderate risk, consistent growth. A diversified 60/40 approach with dollar-cost averaging.', steps:[ `Contribute $${Math.round(totalMonthly*1.1).toLocaleString()}/mo total — split 60% stocks, 40% bonds`, `Core holdings: VTI + BND ETFs for instant diversification`, `Expected portfolio growth: 6-8% annually`, `Total needed: $${totalNeeded.toLocaleString()} · Timeline: ${avgYears.toFixed(1)} yrs`, 'Max 401(k) contribution ($23,000/yr) + IRA ($7,000/yr) = $30,000 annual tax shelter', ] }, { name:'Aggressive',color:'#f97316',icon:'🚀', desc:'Higher risk, maximum growth potential. Focus on growth equities and compounding.', steps:[ `Invest $${Math.round(totalMonthly*1.3).toLocaleString()}/mo in growth stocks (NASDAQ-heavy, small-cap tilt)`, `Dollar-cost average into QQQ, ARKK, and individual growth positions`, `Expected portfolio growth: 9-12% annually (with higher volatility)`, `Total needed: $${totalNeeded.toLocaleString()} · Timeline: ${(avgYears*0.8).toFixed(1)} yrs`, 'Warning: Aggressive strategies can lose 30-50% in downturns. Only appropriate for long horizons (10+ years).', ] } ]; el.innerHTML = plans.map(p=>`
${p.icon}
${p.name} Plan
${p.desc}
⚠ Educational framework only · Not personalized investment advice · Consult a licensed financial advisor
`).join(''); } // ── FINANCIAL LITERACY: toggle tip ──────────────────── function showLiteracyTip(id) { const el = document.getElementById('lit-'+id); if(el) el.style.display = el.style.display==='none'?'block':'none'; } /* ════════════════════════════════════════════════════════ SYNC TO DEBT RESOLUTION APP ════════════════════════════════════════════════════════ */ function syncToDebtApp() { try { // Force a fresh payroll calculation from form fields if user has entered any if(document.getElementById('py-gross') && parseFloat(document.getElementById('py-gross').value)>0) { calculatePayroll(); // ensures payrollData is current } // Capture all raw form values so debt app can populate its identical fields const rawForm = { gross: parseFloat(document.getElementById('py-gross')?.value)||0, periods: parseFloat(document.getElementById('py-freq')?.value)||26, status: document.getElementById('py-status')?.value||'single', stateRate: parseFloat(document.getElementById('py-state')?.value)||0, payType: document.getElementById('py-pay-type')?.value||'period', hours: parseFloat(document.getElementById('py-hours')?.value)||40, k401: parseFloat(document.getElementById('py-401k')?.value)||0, k401Unit: document.getElementById('py-401k-unit')?.value||'dollar', health: parseFloat(document.getElementById('py-health')?.value)||0, hsa: parseFloat(document.getElementById('py-hsa')?.value)||0, fsa: parseFloat(document.getElementById('py-fsa')?.value)||0, dental: parseFloat(document.getElementById('py-dental')?.value)||0, vision: parseFloat(document.getElementById('py-vision')?.value)||0, allotments: parseFloat(document.getElementById('py-allotments')?.value)||0, commuter: parseFloat(document.getElementById('py-commuter')?.value)||0, depcare: parseFloat(document.getElementById('py-depcare')?.value)||0, roth: parseFloat(document.getElementById('py-roth')?.value)||0, match: parseFloat(document.getElementById('py-match')?.value)||0, matchUnit: document.getElementById('py-match-unit')?.value||'dollar', }; // Merge with calculated payrollData for complete picture const exportPayroll = Object.assign({}, rawForm, (payrollData && payrollData.gross ? payrollData : {})); // Build the comprehensive export package const exportData = { payroll: exportPayroll || {}, portfolio: { totalValue: totalValue, holdingCount: holdings.length, positions: holdings.map(h=>({t:h.t, n:h.n, s:h.s, mv:h.mv, alloc:h.alloc})), }, contributions: contribLog || [], goals: goals || [], budget: { totalInvestments: totalValue, monthlyContributions: (contribLog||[]).filter(c=>{ const d=new Date(c.date), now=new Date(); return d.getMonth()===now.getMonth()&&d.getFullYear()===now.getFullYear(); }).reduce((s,c)=>s+c.amount,0), }, timestamp: Date.now(), }; localStorage.setItem('bp_invest_export', JSON.stringify(exportData)); localStorage.setItem('bpinvestexport', JSON.stringify(exportData)); var _periods = exportPayroll.periods || rawForm.periods || 26; var _gross = exportPayroll.gross || rawForm.gross || 0; var _net = (exportPayroll.takeHome!=null ? exportPayroll.takeHome : (exportPayroll.takeHomeAnnual ? exportPayroll.takeHomeAnnual/_periods : 0)) || 0; var _payrollOut = Object.assign({}, exportPayroll, { gross: _gross, net: _net, takeHome: _net, periods: _periods, monthly: _net * (_periods/12), source: 'BellPath Investing' }); try { localStorage.setItem('bp_payroll', JSON.stringify(_payrollOut)); localStorage.setItem('bppayroll', JSON.stringify(_payrollOut)); localStorage.setItem('bpd_payroll', JSON.stringify(_payrollOut)); } catch(e){} const label = 'Portfolio: $' + Math.round(totalValue).toLocaleString() + (exportPayroll ? ' · Payroll: $' + Math.round(exportPayroll.takeHome||0).toLocaleString() + '/period' : ''); showToast('âÅ"“ Exported to Debt App · ' + label); updateSyncStatus('exported'); return true; } catch(e) { console.error('Sync error:', e); showToast('âš  Export failed: ' + e.message); return false; } } function updateSyncStatus(direction) { const el = document.getElementById('syncBanner'); if(!el) return; if(direction === 'exported') { el.innerHTML = 'âÅ"… Exported to Debt App · Portfolio value, payroll, and contributions shared · ' + new Date().toLocaleTimeString(); } else if(direction === 'imported') { el.innerHTML = 'âÅ"… Synced from Debt App · Debt balances updated in dashboard · ' + new Date().toLocaleTimeString(); } } // Import data FROM debt app (debts, payments) /* ── buildPartialRows: legacy alias for compatibility ── The debt-partial-planner moved to the Debt Resolution app. This stub keeps importFromDebtApp from throwing. ── */ function buildPartialRows() { // If the new partial rebalance rows function exists, call it if (typeof buildPartialRebalRows === 'function') { try { buildPartialRebalRows(); } catch(e) {} } // Also try to populate any partial-payoff rows if present in the rebalance section const container = document.getElementById('partialPayoffRows'); if (container && Array.isArray(window.importedDebts) && window.importedDebts.length) { container.innerHTML = window.importedDebts.map(d => `
${d.name} ${d.rate}% APR $${Math.round(d.balance).toLocaleString()}
`).join(''); } } function importFromDebtApp() { try { const debtsJson = localStorage.getItem('bpd_debts'); const paymentsJson = localStorage.getItem('bpd_payments'); const paychecksJson = localStorage.getItem('bpd_paychecks'); let imported = 0; if(debtsJson) { const debts = JSON.parse(debtsJson); window.importedDebts = debts; imported += debts.length; // Auto-populate Minimum Debt Payments in Budget tab const totalMin = debts.reduce((s,d)=>s+(d.minPayment||0), 0); const bDebtMin = document.getElementById('b-debt-min'); if(bDebtMin && totalMin > 0) bDebtMin.value = totalMin; // Auto-populate partial planner rows buildPartialRows(); } if(paymentsJson) { window.importedPayments = JSON.parse(paymentsJson); } if(paychecksJson) { window.importedPaychecks = JSON.parse(paychecksJson); imported += JSON.parse(paychecksJson).length; } if(imported > 0) { updateDebtSummaryWidget(); analyzeBudget(); showToast('âÅ"“ Imported ' + imported + ' records · Budget updated'); updateSyncStatus('imported'); return true; } else { showToast('No debt data found · Open BellPathâ„¢ Debt App and add debts first'); return false; } } catch(e) { console.error('Import error:', e); showToast('âš  Import failed: ' + e.message); return false; } } function updateDebtSummaryWidget() { const widget = document.getElementById('debtSummaryWidget'); if(!widget) return; const debts = window.importedDebts || []; if(!debts.length) { widget.innerHTML = '
No debts synced yet. Click "Import from Debt App" to load.
'; return; } const total = debts.reduce((s,d) => s + d.balance, 0); const minTotal = debts.reduce((s,d) => s + (d.minPayment||0), 0); const avgRate = total > 0 ? debts.reduce((s,d) => s + d.rate*d.balance, 0) / total : 0; widget.innerHTML = `
TOTAL DEBT
$${Math.round(total).toLocaleString()}
MIN PAYMENTS
$${minTotal.toLocaleString()}
AVG APR
${avgRate.toFixed(1)}%
${debts.length} debt account${debts.length>1?'s':''} synced from BellPathâ„¢ Debt Resolution
`; } // Auto-import on load function autoSyncDebt() { if(localStorage.getItem('bpd_debts')) { importFromDebtApp(); } } /* ──────────────────────────────────────── FINANCIAL LITERACY RENDERER ──────────────────────────────────────── */ const LITERACY_TOPICS = [ ["What is a stock?","A stock (share/equity) is a small ownership stake in a company. When you buy Apple stock, you literally own a tiny fraction of Apple Inc. If the company grows, your shares are worth more. Shareholders may also receive dividends — a share of profits paid out regularly."], ["What is a bond?","A bond is a loan you make to a government or corporation. They pay you interest (coupon) periodically and return your principal at maturity. Bonds are generally less volatile than stocks but provide lower long-term returns. Used to balance portfolios and reduce risk."], ["What is an ETF?","An Exchange-Traded Fund holds a basket of securities that trades like a single stock. SPY holds all 500 S&P 500 companies. Buying 1 share of SPY gives you exposure to all 500 at once. ETFs provide instant diversification, low fees (0.03–0.20%), and are the cornerstone of passive investing."], ["What is diversification?","Not putting all eggs in one basket. Owning assets across sectors, geographies, and asset classes means a loss in one area is offset by gains in others. It reduces risk without necessarily reducing expected returns — the closest thing to a free lunch in investing."], ["What is dollar-cost averaging (DCA)?","Investing a fixed dollar amount on a regular schedule regardless of price. When prices are high, you buy fewer shares; when low, you buy more. Over time this averages your cost per share. Research shows most investors do better with DCA than trying to time the market."], ["What is compound interest?","You earn returns on your returns. $10,000 at 7% grows to $76,000 over 30 years without adding another dollar. Starting early is the single most impactful investing decision — a 25-year-old who invests $5,000 will dramatically outpace a 35-year-old who invests $50,000."], ["What is a 401(k) and IRA?","Tax-advantaged retirement accounts. 401(k): employer-offered, reduces taxable income now (traditional) or grows tax-free (Roth), $23,000/yr limit. IRA: opened independently, $7,000/yr. Maxing these before taxable accounts is almost always correct — tax savings compound dramatically."], ["What is risk vs. return?","Higher potential returns almost always come with higher risk. Cash: ~5%, safe. Stocks: ~10% historically, but can drop 50% in a bear market. Your risk tolerance — ability and willingness to handle losses — should drive your asset allocation. Time horizon matters enormously."], ["What is a P/E ratio?","Price-to-Earnings: stock price ÷ earnings per share. P/E of 20 means you pay $20 for every $1 of annual profit. S&P 500 historical average: ~16-18. High P/E (growth stocks) = priced for rapid future growth. Low P/E (value stocks) = may be undervalued. Never use P/E alone."], ["What is inflation?","The general rise in prices over time. At 3% inflation, $100 today buys $97 worth next year. Cash and low-yield savings lose purchasing power. Stocks historically return ~7% real (inflation-adjusted) annually — well above inflation. This is why 'doing nothing' with money is actually a losing strategy."], ["What is dollar-cost averaging vs lump sum?","Lump sum investing (putting it all in at once) historically beats DCA about 2/3 of the time because markets trend upward over time. DCA beats lump sum mainly during market peaks. If you have a lump sum and a long horizon, evidence favors investing it immediately — but DCA reduces psychological stress."], ["What is a stop-loss order?","An instruction to automatically sell a security if it falls to a specified price. If you buy a stock at $100 and set a stop-loss at $90, it sells automatically if price hits $90, limiting your loss to ~10%. Useful for risk management but can trigger on temporary dips — use carefully."], ]; function renderLiteracyHub() { const el = document.getElementById('literacyList'); if(!el) return; el.innerHTML = LITERACY_TOPICS.map((t,i)=>`
`).join(''); } function toggleLiteracy(i) { const el=document.getElementById('lit-'+i); const icon=document.getElementById('lit-icon-'+i); if(!el) return; const open = el.style.display!=='none'; el.style.display = open ? 'none' : 'block'; if(icon) icon.textContent = open ? '+' : '-'; } /* ════════════════════════════════════════════════════════ PAYROLL CALCULATOR ════════════════════════════════════════════════════════ */ // 2026 Federal Tax Brackets (Single / MFJ / HOH) const FED_BRACKETS = { single: [{max:11925,rate:0.10},{max:48475,rate:0.12},{max:103350,rate:0.22},{max:197300,rate:0.24},{max:250525,rate:0.32},{max:626350,rate:0.35},{max:Infinity,rate:0.37}], married: [{max:23850,rate:0.10},{max:96950,rate:0.12},{max:206700,rate:0.22},{max:394600,rate:0.24},{max:501050,rate:0.32},{max:751600,rate:0.35},{max:Infinity,rate:0.37}], hoh: [{max:17000,rate:0.10},{max:64850,rate:0.12},{max:103350,rate:0.22},{max:197300,rate:0.24},{max:250500,rate:0.32},{max:626350,rate:0.35},{max:Infinity,rate:0.37}], }; const STD_DEDUCTION = {single:15000, married:30000, hoh:22500}; const SS_WAGE_BASE = 176100; let payrollData = {}; let contribLog = []; function loadPayrollData() { try { const p = localStorage.getItem('bp_payroll'); if(p) payrollData = JSON.parse(p); const c = localStorage.getItem('bp_contribs'); if(c) contribLog = JSON.parse(c); } catch(e){} } function savePayrollData() { try { localStorage.setItem('bp_payroll', JSON.stringify(payrollData)); localStorage.setItem('bp_contribs', JSON.stringify(contribLog)); } catch(e){} } function calcFedTax(taxableIncome, status) { const brackets = FED_BRACKETS[status] || FED_BRACKETS.single; let tax = 0, prev = 0; for(const b of brackets) { if(taxableIncome <= prev) break; const portion = Math.min(taxableIncome, b.max) - prev; tax += portion * b.rate; prev = b.max; } return Math.max(0, tax); } /* ── PARTIAL REBALANCE (Investing App) ── */ function buildPartialRebalRows() { const el = document.getElementById('partialRebalRows'); if(!el) return; if(!holdings.length) { el.innerHTML = '
Add positions to your portfolio first.
'; return; } el.innerHTML = holdings.map(h => `
${h.t}
${h.n.substring(0,20)} · ${h.shares.toFixed(4)} sh · $${(h.price||0).toFixed(2)}
Shares bought/sold: ≈ $0
`).join(''); // Add value preview on input holdings.forEach(h => { const input = document.getElementById('prb-' + h.t); if(input) input.oninput = function() { const shares = parseFloat(this.value) || 0; const valEl = document.getElementById('prb-val-' + h.t); if(valEl) valEl.textContent = Math.abs(shares * (h.price||0)).toFixed(0); }; }); } function applyPartialRebalance() { if(!holdings.length) { showToast('âš  No positions in portfolio'); return; } let changed = 0; const summary = []; portfolio.forEach(p => { const input = document.getElementById('prb-' + p.t); const delta = parseFloat(input?.value) || 0; if(delta === 0) return; const oldShares = p.shares; p.shares = Math.max(0, p.shares + delta); const info = getTickerInfo(p.t); const price = info?.p || p.basis || 0; summary.push({ t: p.t, delta: delta, dollars: Math.abs(delta * price), action: delta > 0 ? 'bought' : 'sold', newShares: p.shares }); changed++; }); if(!changed) { showToast('âš  Enter share amounts first'); return; } savePortfolio(); rebuildPortfolio(); setTimeout(generateFeed, 100); const el = document.getElementById('partialRebalResult'); if(el) { el.innerHTML = `
TRADES RECORDED
${summary.map(s => `
${s.t} ${s.action==='bought'?'+':'âˆâ€'}${Math.abs(s.delta).toFixed(4)} shares $${Math.round(s.dollars).toLocaleString()} ${s.newShares.toFixed(4)} sh total
`).join('')}
Portfolio rebuilt · Feed updated · Clear inputs to start fresh
`; } // Clear inputs holdings.forEach(h => { const input = document.getElementById('prb-' + h.t); if(input) { input.value = ''; } const valEl = document.getElementById('prb-val-' + h.t); if(valEl) valEl.textContent = '0'; }); showToast('âÅ"“ ' + changed + ' trade' + (changed>1?'s':'') + ' recorded · Portfolio updated'); buildPartialRebalRows(); } /* ── PAY TYPE TOGGLE ── */ function onPayTypeChange() { const t = document.getElementById('py-pay-type')?.value; const lbl = document.getElementById('py-gross-label'); const hrow = document.getElementById('py-hours-row'); if(lbl) { lbl.textContent = t === 'hourly' ? 'Hourly Rate ($)' : t === 'salary' ? 'Annual Salary ($)' : 'Gross Pay Per Period ($)'; } if(hrow) hrow.style.display = t === 'hourly' ? 'block' : 'none'; calculatePayroll(); } function calculatePayroll() { // Determine gross per period from pay type const payType = document.getElementById('py-pay-type')?.value || 'period'; const payAmt = parseFloat(document.getElementById('py-gross')?.value) || 0; const periods = parseFloat(document.getElementById('py-freq')?.value) || 26; const hoursWk = parseFloat(document.getElementById('py-hours')?.value) || 40; let gross = payAmt; if(payType === 'hourly') gross = payAmt * hoursWk * (52 / periods); if(payType === 'salary') gross = payAmt / periods; const status = document.getElementById('py-status')?.value || 'single'; const stateRate= parseFloat(document.getElementById('py-state')?.value) || 0; const addl = parseFloat(document.getElementById('py-addl')?.value) || 0; const k401Raw = parseFloat(document.getElementById('py-401k')?.value) || 0; const k401Unit = document.getElementById('py-401k-unit')?.value || 'dollar'; const k401 = k401Unit === 'pct' ? (gross * k401Raw / 100) : k401Raw; const health = parseFloat(document.getElementById('py-health')?.value) || 0; const hsa = parseFloat(document.getElementById('py-hsa')?.value) || 0; const fsa = parseFloat(document.getElementById('py-fsa')?.value) || 0; const preOther = parseFloat(document.getElementById('py-pretax-other')?.value) || 0; const roth = parseFloat(document.getElementById('py-roth')?.value) || 0; const life = parseFloat(document.getElementById('py-life')?.value) || 0; const postOther= parseFloat(document.getElementById('py-posttax-other')?.value) || 0; const matchRaw = parseFloat(document.getElementById('py-match')?.value) || 0; const matchUnit = document.getElementById('py-match-unit')?.value || 'dollar'; const match = matchUnit === 'pct' ? (gross * matchRaw / 100) : matchRaw; const empHsa = parseFloat(document.getElementById('py-emp-hsa')?.value) || 0; if(gross <= 0) { const el = document.getElementById('payrollResult'); if(el) el.innerHTML = '
Enter your gross pay per period to see your breakdown.
'; return; } const annualGross = gross * periods; // Pre-tax deductions (reduce federal/state taxable income) const dental2 = parseFloat(document.getElementById('py-dental')?.value) || 0; const vision = parseFloat(document.getElementById('py-vision')?.value) || 0; const allotments = parseFloat(document.getElementById('py-allotments')?.value) || 0; const totalPreTax = k401 + health + hsa + fsa + dental2 + vision + allotments + preOther; const fedTaxablePerPeriod = gross - totalPreTax; const annualFedTaxable = (fedTaxablePerPeriod * periods) - STD_DEDUCTION[status]; // Federal income tax per period const annualFedTax = calcFedTax(Math.max(0, annualFedTaxable), status); const fedTaxPeriod = annualFedTax / periods + addl; // FICA (Social Security + Medicare) const ssWagesThisPeriod = Math.min(gross, SS_WAGE_BASE / periods); const ssTax = ssWagesThisPeriod * 0.062; const medTax = gross * 0.0145; // Additional Medicare for high earners const addlMed = annualGross > 200000 ? (gross * 0.009) : 0; const ficaTax = ssTax + medTax + addlMed; // State tax (simplified: applied to gross - pre-tax deductions) const stateTax = fedTaxablePerPeriod * (stateRate / 100); // Post-tax deductions const totalPostTax = roth + life + postOther; const totalDeductions = totalPreTax + fedTaxPeriod + ficaTax + stateTax + totalPostTax; const takeHome = gross - totalDeductions; // Save for budget tab payrollData = { gross, periods, annualGross, payType: document.getElementById('py-pay-type')?.value || 'period', hours: parseFloat(document.getElementById('py-hours')?.value) || 40, retType: document.getElementById('py-ret-type')?.value || '401k', k401Raw, k401Unit, matchRaw, matchUnit, health, hsa, fsa, dental: dental2, vision, allotments, commuter: parseFloat(document.getElementById('py-commuter')?.value)||0, depcare: parseFloat(document.getElementById('py-depcare')?.value)||0, preOther: parseFloat(document.getElementById('py-pretax-other')?.value)||0, roth: parseFloat(document.getElementById('py-roth')?.value)||0, life: parseFloat(document.getElementById('py-life')?.value)||0, postOther: parseFloat(document.getElementById('py-posttax-other')?.value)||0, addl: parseFloat(document.getElementById('py-addl')?.value)||0, empHsa: parseFloat(document.getElementById('py-emp-hsa')?.value)||0, takeHome, takeHomeAnnual: takeHome * periods, k401, k401Annual: k401*periods, match, matchAnnual: match*periods, hsa: hsa+empHsa, hsaAnnual: (hsa+empHsa)*periods, fedTax: fedTaxPeriod, fedTaxAnnual: fedTaxPeriod*periods, stateTax, stateTaxAnnual: stateTax*periods, ficaTax, ficaTaxAnnual: ficaTax*periods, totalPreTax, totalPostTax, effectiveFedRate: annualGross>0?(annualFedTax/annualGross*100).toFixed(1):0, stateRate, status, }; savePayrollData(); updateBudgetPayrollPull(); const fmt = n => '$'+Math.abs(n).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2}); const row = (label, val, cls='', sub='') => ` ${label} ${val} ${sub} `; const el = document.getElementById('payrollResult'); if(!el) return; el.innerHTML = `
${row('Gross Pay', fmt(gross), 'gold', fmt(annualGross))} ${k401>0?row('401(k) contribution', 'âˆâ€'+fmt(k401), 'dn', 'âˆâ€'+fmt(k401*periods)):''} ${health>0?row('Health insurance', 'âˆâ€'+fmt(health), 'dn', 'âˆâ€'+fmt(health*periods)):''} ${hsa>0?row('HSA contribution', 'âˆâ€'+fmt(hsa), 'dn', 'âˆâ€'+fmt(hsa*periods)):''} ${fsa>0?row('FSA contribution', 'âˆâ€'+fmt(fsa), 'dn', 'âˆâ€'+fmt(fsa*periods)):''} ${preOther>0?row('Other pre-tax', 'âˆâ€'+fmt(preOther), 'dn', 'âˆâ€'+fmt(preOther*periods)):''} ${row('Federal income tax', 'âˆâ€'+fmt(fedTaxPeriod), 'dn', 'âˆâ€'+fmt(fedTaxPeriod*periods)+' ('+payrollData.effectiveFedRate+'% eff.)')} ${stateTax>0?row('State income tax ('+stateRate+'%)', 'âˆâ€'+fmt(stateTax), 'dn', 'âˆâ€'+fmt(stateTax*periods)):''} ${row('Social Security (6.2%)', 'âˆâ€'+fmt(ssTax), 'dn', 'âˆâ€'+fmt(ssTax*periods))} ${row('Medicare (1.45%)', 'âˆâ€'+fmt(medTax), 'dn', 'âˆâ€'+fmt(medTax*periods))} ${addlMed>0?row('Additional Medicare (0.9%)', 'âˆâ€'+fmt(addlMed), 'dn', 'âˆâ€'+fmt(addlMed*periods)):''} ${roth>0?row('Roth 401(k)', 'âˆâ€'+fmt(roth), 'dn', 'âˆâ€'+fmt(roth*periods)):''} ${life>0?row('Life/disability ins.', 'âˆâ€'+fmt(life), 'dn', 'âˆâ€'+fmt(life*periods)):''} ${postOther>0?row('Other post-tax', 'âˆâ€'+fmt(postOther), 'dn', 'âˆâ€'+fmt(postOther*periods)):''} ${match>0?row('Employer 401(k) match', '+'+fmt(match), 'up', '+'+fmt(match*periods)+' free money!'):''} ${empHsa>0?row('Employer HSA contribution', '+'+fmt(empHsa), 'up', '+'+fmt(empHsa*periods)):''} ${(match+empHsa)>0?row('Total employer benefits', '+'+fmt(match+empHsa), 'up', '+'+fmt((match+empHsa)*periods)):row('âš  Check employer benefits', 'None entered', '', 'Missing free money?')}
ItemPer PeriodAnnual
── PRE-TAX DEDUCTIONS ──
── TAXES ──
── POST-TAX DEDUCTIONS ──
TAKE-HOME PAY ${fmt(takeHome)} ${fmt(takeHome*periods)}/yr
── EMPLOYER CONTRIBUTIONS (not deducted from pay) ──
âš  Calculations are estimates based on standard withholding tables. Actual withholding may vary based on W-4 allowances, local taxes, and employer-specific plans. Consult a tax professional for exact figures. Federal brackets based on 2026 IRS tables.
`; } /* ════════════════════════════════════════════════════════ BUDGET ANALYZER ════════════════════════════════════════════════════════ */ function updateBudgetPayrollPull() { const el = document.getElementById('b-payroll-summary'); if(!el || !payrollData.takeHome) return; el.innerHTML = `Take-home: $${payrollData.takeHome.toLocaleString(undefined,{maximumFractionDigits:0})}/period · $${payrollData.takeHomeAnnual.toLocaleString(undefined,{maximumFractionDigits:0})}/yr`; } function analyzeBudget() { // Auto-fill savings from portfolio if not entered const emergGoalEl = document.getElementById('b-emergency-goal'); const emergCurrEl = document.getElementById('b-emergency-current'); // auto-populate current savings (investments) if portfolio has value if(emergCurrEl && !emergCurrEl.value && totalValue > 0) { emergCurrEl.value = Math.round(totalValue); } const monthly = payrollData.takeHomeAnnual ? payrollData.takeHomeAnnual/12 : 0; const b = { housing: parseFloat(document.getElementById('b-housing')?.value)||0, transport: parseFloat(document.getElementById('b-transport')?.value)||0, food: parseFloat(document.getElementById('b-food')?.value)||0, utilities: parseFloat(document.getElementById('b-utilities')?.value)||0, insurance: parseFloat(document.getElementById('b-insurance')?.value)||0, subs: parseFloat(document.getElementById('b-subs')?.value)||0, entertainment:parseFloat(document.getElementById('b-entertainment')?.value)||0, other: parseFloat(document.getElementById('b-other')?.value)||0, debtMin: parseFloat(document.getElementById('b-debt-min')?.value)||0, debtExtra: parseFloat(document.getElementById('b-debt-extra')?.value)||0, emergGoal: parseFloat(document.getElementById('b-emergency-goal')?.value)||0, emergCurrent:parseFloat(document.getElementById('b-emergency-current')?.value)||0, }; const totalExpenses = b.housing+b.transport+b.food+b.utilities+b.insurance+b.subs+b.entertainment+b.other+b.debtMin+b.debtExtra; const available = monthly > 0 ? monthly : totalExpenses * 1.2; const leftover = available - totalExpenses; const needs = b.housing+b.transport+b.food+b.utilities+b.insurance+b.debtMin; const wants = b.subs+b.entertainment+b.other; const savings = leftover; const needsPct = available>0?(needs/available*100).toFixed(0):0; const wantsPct = available>0?(wants/available*100).toFixed(0):0; const savePct = available>0?(Math.max(0,savings)/available*100).toFixed(0):0; const emergGap = Math.max(0, b.emergGoal - b.emergCurrent); const emergMonths = leftover > 0 ? Math.ceil(emergGap/leftover) : 999; // Optimal allocation suggestion const opt50 = available * 0.50; const opt30 = available * 0.30; const opt20 = available * 0.20; const el = document.getElementById('budgetAnalysis'); if(!el) return; el.innerHTML = `
Monthly Income
$${available.toLocaleString(undefined,{maximumFractionDigits:0})}
Total Expenses
$${totalExpenses.toLocaleString(undefined,{maximumFractionDigits:0})}
Monthly Surplus
${leftover>=0?'+':''}\$${leftover.toLocaleString(undefined,{maximumFractionDigits:0})}
Savings Rate
${savePct}%
Your Budget vs 50/30/20
${[ ['Needs (50% target)', needs, opt50, needsPct], ['Wants (30% target)', wants, opt30, wantsPct], ['Savings (20% target)', Math.max(0,savings), opt20, savePct], ].map(([label,actual,target,pct])=>`
${label} ${pct}% · $${Math.round(actual).toLocaleString()}
`).join('')}
ðŸâ€Â¡ Optimization Suggestions
${leftover>0?`
âÅ"… You have $${Math.round(leftover).toLocaleString()}/mo surplus — allocate it intentionally.
`:'
⚠ Expenses exceed income — identify items to reduce.
'} ${emergGap>0&&leftover>0?`
🛟 Emergency fund gap: $${emergGap.toLocaleString()} · At current surplus: ${emergMonths} months to fund it.
`:''} ${payrollData.matchAnnual>0?`
🎁 Ensure you capture full employer 401(k) match: $${payrollData.matchAnnual.toLocaleString()}/yr free money.
`:''} ${parseFloat(savePct)<20?`
📈 Increase savings rate to 20%+ — redirect $${Math.round(opt20-Math.max(0,savings)).toLocaleString()}/mo from wants to savings.
`:''}
ðŸâ€Â³ High-interest debt should be priority — every dollar paid saves guaranteed interest rate return.
${b.debtExtra>0?`
⚡ Extra $${b.debtExtra.toLocaleString()}/mo toward debt is excellent — see Debt Payoff app for optimal order.
`:''}
âš  Educational framework only. Not professional financial advice. Consult a licensed CFP for personalized guidance.
`; updateBudgetPayrollPull(); } /* ── PARTIAL PAYOFF HELPERS ── */ function fillMinimums() { if(!portfolio) return; // debt app only const debtsData = window.importedDebts||[]; debtsData.forEach(d => { const el = document.getElementById('pp-'+d.id); if(el) el.value = d.minPayment || 0; }); showToast('Filled with minimum payments - open Debt App to apply'); } function fillExtraMethod() { const extra = parseFloat(document.getElementById('extra-payment')?.value)||0; const debtsData = window.importedDebts||[]; if(!debtsData.length) { showToast('âš  No debt data — sync from Debt App first'); return; } // Fill minimums first debtsData.forEach(d => { const el = document.getElementById('pp-'+d.id); if(el) el.value = d.minPayment || 0; }); // Add extra to target according to current method const currentMethod = document.getElementById('rebal-method')?.value || 'avalanche'; let target = null; if(currentMethod === 'snowball') { target = debtsData.filter(d=>d.balance>0).sort((a,b)=>a.balance-b.balance)[0]; } else { target = debtsData.filter(d=>d.balance>0).sort((a,b)=>b.rate-a.rate)[0]; } if(target && extra>0) { const el = document.getElementById('pp-'+target.id); if(el) el.value = (parseFloat(el.value)||0) + extra; } showToast('Applied strategy extra + $' + extra.toLocaleString() + ' extra to target debt'); } function recordAllPartialPayments() { // Records payments to localStorage so debt app picks them up const debtsData = window.importedDebts||[]; let recorded = 0; const today = new Date().toISOString().split('T')[0]; debtsData.forEach(d => { const el = document.getElementById('pp-'+d.id); const amount = parseFloat(el?.value)||0; if(amount <= 0) return; // Apply payment to local copy const r = (d.rate||0)/100/12; const interest = d.balance * r; const principal = Math.max(0, amount - interest); d.balance = Math.max(0, d.balance - principal); recorded++; }); if(recorded > 0) { // Save updated debts back localStorage.setItem('bpd_debts', JSON.stringify(debtsData)); buildPartialRows(); updateDebtSummaryWidget(); showToast('âÅ"“ ' + recorded + ' payment(s) recorded · Debt balances updated'); } else { showToast('âš  Enter payment amounts first'); } } /* ════════════════════════════════════════════════════════ CONTRIBUTION TRACKER ════════════════════════════════════════════════════════ */ function logContribution() { const amount = parseFloat(document.getElementById('contrib-amount')?.value)||0; const goal = document.getElementById('contrib-goal')?.value?.trim()||'General'; const date = document.getElementById('contrib-date')?.value||new Date().toISOString().split('T')[0]; const notes = document.getElementById('contrib-notes')?.value?.trim()||''; if(amount<=0){showToast('âš  Enter a contribution amount');return;} contribLog.unshift({id:Date.now(), amount, goal, date, notes}); savePayrollData(); renderContributions(); showToast('âÅ"“ Contribution logged: $'+amount.toLocaleString()); document.getElementById('contrib-amount').value=''; document.getElementById('contrib-notes').value=''; } function deleteContrib(id) { contribLog = contribLog.filter(c=>c.id!==id); savePayrollData(); renderContributions(); } function renderContributions() { const now = new Date(); const thisMonth = now.getMonth(), thisYear = now.getFullYear(); const total = contribLog.reduce((s,c)=>s+c.amount,0); const month = contribLog.filter(c=>{const d=new Date(c.date);return d.getMonth()===thisMonth&&d.getFullYear()===thisYear;}).reduce((s,c)=>s+c.amount,0); const year = contribLog.filter(c=>new Date(c.date).getFullYear()===thisYear).reduce((s,c)=>s+c.amount,0); const months = contribLog.length>0?Math.max(1,(now-new Date(contribLog[contribLog.length-1].date))/2592000000):1; const avg = total/months; const fmt = n=>'$'+n.toLocaleString(undefined,{maximumFractionDigits:0}); const set = (id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v;}; set('cs-total',fmt(total));set('cs-month',fmt(month));set('cs-year',fmt(year));set('cs-avg',fmt(avg)); const tbody = document.getElementById('contribLog'); if(!tbody) return; tbody.innerHTML = contribLog.length===0 ? 'No contributions logged yet. Click + Log Contribution to start.' : contribLog.map(c=>` ${c.date} $${c.amount.toLocaleString()} ${c.goal} ${c.notes||'—'} `).join(''); } function suggestDistribution() { const amount = parseFloat(document.getElementById('dist-amount')?.value)||0; const el = document.getElementById('distSuggestion'); if(!el||amount<=0){if(el)el.innerHTML='
Enter an amount above to see your personalized distribution plan.
';return;} // Build multiple prioritized suggestions based on full financial picture const suggestions = []; const emergGoal = parseFloat(document.getElementById('b-emergency-goal')?.value)||0; const emergCurr = parseFloat(document.getElementById('b-emergency-current')?.value)||0; const hasEmergGap = emergCurr < emergGoal; const emergGap = Math.max(0, emergGoal - emergCurr); // 1. Emergency fund (highest priority if underfunded) if(hasEmergGap) { const fundedPct = emergGoal>0?(emergCurr/emergGoal*100).toFixed(0):0; suggestions.push({ label:'🛟 Emergency Fund (HYSA)', pct: emergCurr < emergGoal*0.5 ? 35 : 20, reason:`Only ${fundedPct}% funded · Target: $${emergGoal.toLocaleString()} (3-6 month buffer)`, type:'safety', action:`Deposit to high-yield savings — aim for $${Math.round(Math.min(amount*0.35, emergGap)).toLocaleString()} this period` }); } // 2. Capture 401k match (free money — always first) if(payrollData && payrollData.matchAnnual > 0 && (payrollData.k401Annual||0) < payrollData.matchAnnual) { suggestions.push({ label:'🏦 401(k) — Capture Employer Match', pct:25, reason:`Free $${Math.round(payrollData.matchAnnual).toLocaleString()}/yr employer match · 100% instant return`, type:'retirement', action:'Increase 401k contribution to at least the match threshold immediately' }); } // 3. High-interest debt paydown (if debt data imported) const importedDebts = window.importedDebts||[]; const highRateDebt = importedDebts.filter(d=>d.rate>=10).sort((a,b)=>b.rate-a.rate)[0]; if(highRateDebt) { suggestions.push({ label:`ðŸâ€Â³ ${highRateDebt.name} (${highRateDebt.rate}% APR)`, pct:20, reason:`Paying this off = guaranteed ${highRateDebt.rate}% return · Balance: $${Math.round(highRateDebt.balance).toLocaleString()}`, type:'debt', action:'Extra payment above minimum directly reduces principal fastest' }); } else { suggestions.push({ label:'ðŸâ€Â³ High-Interest Debt Payoff', pct:15, reason:'Guaranteed return equal to your interest rate — better than most investments', type:'debt', action:'Check BellPathâ„¢ Debt App — sync to see your specific accounts' }); } // 4. Underweight portfolio sectors const sd = getSectorRollup(holdings); const tv = sd.values.reduce((a,b)=>a+b,0)||1; const sectorWeights = {}; sd.labels.forEach((l,i)=>{sectorWeights[l]=(sd.values[i]/tv)*100;}); const targetSectors = ['Technology','Healthcare','Financials','Consumer Staples','Industrials']; const missing = targetSectors.filter(s=>!sectorWeights[s]||sectorWeights[s]<5); if(holdings.length>0 && missing.length>0) { // Find best ETF for each missing sector const sectorETFs = {'Technology':'XLK','Healthcare':'XLV','Financials':'XLF','Consumer Staples':'XLP','Industrials':'XLI','Energy':'XLE','Real Estate':'VNQ','Utilities':'XLU'}; const topMissing = missing.slice(0,2); suggestions.push({ label:`📈 Portfolio: ${topMissing.join(', ')} exposure`, pct:15, reason:`Missing sectors reduce diversification · Consider: ${topMissing.map(s=>sectorETFs[s]||s).join(', ')}`, type:'invest', action:`Add ${topMissing.map(s=>sectorETFs[s]||s).join(' and ')} to fill coverage gaps` }); } else if(holdings.length>0) { const underweight = holdings.filter(h=>h.alloc<3).sort((a,b)=>a.alloc-b.alloc)[0]; const topPick = underweight ? underweight.t : holdings[0].t; suggestions.push({ label:`📈 Build position: ${topPick}`, pct:15, reason:`${topPick} is your most underweight position (${underweight?underweight.alloc.toFixed(1):holdings[0].alloc.toFixed(1)}% of portfolio)`, type:'invest', action:`Buy more ${topPick} to increase allocation toward target weight` }); } else { suggestions.push({ label:'📈 Start Investing: VOO (S&P 500 ETF)', pct:15, reason:'Broad market exposure · Low cost (0.03% fee) · Historical avg 10%/yr', type:'invest', action:'Open a brokerage account and begin dollar-cost averaging into VOO' }); } // 5. Roth IRA (if not maxing tax-advantaged accounts) const rothMax = 7000; // 2026 limit suggestions.push({ label:'🏦 Roth IRA contribution', pct:10, reason:`Tax-free growth for retirement · 2026 limit: $${rothMax.toLocaleString()}/yr · Tax-free withdrawals after 59½`, type:'retirement', action:'Max your Roth IRA before taxable brokerage (open at Fidelity, Vanguard, or Schwab)' }); // 6. Upcoming goals const urgentGoals = goals.filter(g=>g.currentnew Date(a.targetDate)-new Date(b.targetDate)).slice(0,2); urgentGoals.forEach((g,i)=>{ const needed = g.target - g.current; suggestions.push({ label:`🎯 ${g.name}`, pct:5, reason:`$${needed.toLocaleString()} remaining · Target: ${new Date(g.targetDate).toLocaleDateString('en-US',{month:'short',year:'numeric'})}`, type:'goal', action:`Contribute $${Math.round(amount*0.05).toLocaleString()} this period toward ${g.name}` }); }); // Normalize percentages to 100% const totalPct = suggestions.reduce((s,x)=>s+x.pct,0); suggestions.forEach(s=>{ s.pct=Math.round(s.pct/totalPct*100); s.dollars=Math.round(amount*s.pct/100); }); el.innerHTML = suggestions.map(s=>{ const dollars = Math.round(amount * s.pct / 100); const colors = {safety:'#4ade80',retirement:'#38bdf8',debt:'#f97316',invest:'#c9a227',goal:'#a78bfa'}; const c = colors[s.type]||'#6b8c6a'; return `
${s.pct}%
${s.label}
${s.reason}
$${dollars.toLocaleString()}
`; }).join(''); } /* ════════════════════════════════════════════════════════ FINANCIAL HEALTH SCORE ════════════════════════════════════════════════════════ */ function updateFinancialHealth() { const metrics = []; let totalScore = 0; // 1. Emergency fund coverage (0-25 pts) const emergGoal = parseFloat(document.getElementById('b-emergency-goal')?.value)||0; const emergCurr = parseFloat(document.getElementById('b-emergency-current')?.value)||0; const emergPct = emergGoal>0 ? Math.min(100, emergCurr/emergGoal*100) : 0; const emergPts = emergPct * 0.25; metrics.push({label:'Emergency Fund', score:emergPts, max:25, pct:emergPct.toFixed(0)+'%', tip:'Target 3-6 months of expenses', color:emergPct>=100?'var(--green-light)':emergPct>=50?'var(--gold)':'var(--red)'}); totalScore += emergPts; // 2. Savings rate (0-25 pts) const monthly = payrollData.takeHomeAnnual ? payrollData.takeHomeAnnual/12 : 0; const contribThisMonth = contribLog.filter(c=>{const d=new Date(c.date);const now=new Date();return d.getMonth()===now.getMonth()&&d.getFullYear()===now.getFullYear();}).reduce((s,c)=>s+c.amount,0); const savRate = monthly>0 ? Math.min(100, contribThisMonth/monthly*100) : 0; const savPts = Math.min(25, savRate * 0.25); metrics.push({label:'Savings Rate', score:savPts, max:25, pct:savRate.toFixed(0)+'%', tip:'Target 20%+ of take-home pay', color:savRate>=20?'var(--green-light)':savRate>=10?'var(--gold)':'var(--red)'}); totalScore += savPts; // 3. Portfolio contribution adherence (0-20 pts) const recentContribs = contribLog.filter(c=>{const d=new Date(c.date);return (Date.now()-d)/(86400000*30)<3;}); const adherePts = Math.min(20, recentContribs.length * 4); metrics.push({label:'Contribution Consistency', score:adherePts, max:20, pct:recentContribs.length+' in 3 mo', tip:'Regular contributions beat market timing', color:adherePts>=16?'var(--green-light)':adherePts>=8?'var(--gold)':'var(--red)'}); totalScore += adherePts; // 4. Portfolio diversification (0-15 pts) const sectorCount = new Set(holdings.map(h=>h.s)).size; const divPts = Math.min(15, sectorCount * 2.5); metrics.push({label:'Diversification', score:divPts, max:15, pct:sectorCount+' sectors', tip:'Target 6+ sectors', color:sectorCount>=6?'var(--green-light)':sectorCount>=3?'var(--gold)':'var(--red)'}); totalScore += divPts; // 5. Employer benefits capture (0-15 pts) const matchCapture = payrollData.matchAnnual>0 ? (payrollData.k401Annual>0?15:0) : 15; metrics.push({label:'Benefits Capture', score:matchCapture, max:15, pct:payrollData.matchAnnual>0?(payrollData.k401Annual>0?'100%':'0%'):'N/A', tip:'Always capture full employer 401k match', color:matchCapture>=15?'var(--green-light)':'var(--red)'}); totalScore += matchCapture; const total = Math.round(totalScore); const grade = total>=90?'A+':total>=80?'A':total>=70?'B':total>=60?'C':total>=50?'D':'F'; const color = total>=80?'var(--green-light)':total>=60?'var(--gold)':'var(--red)'; // Update meter in header const arc = document.getElementById('fhArc'); if(arc) { const offset = 138 - (138 * total/100); arc.style.strokeDashoffset = offset; arc.style.stroke = color; } const scoreEl = document.getElementById('fhScore'); if(scoreEl) {scoreEl.textContent=total;scoreEl.style.color=color;} const gradeEl = document.getElementById('fhGrade'); if(gradeEl) {gradeEl.textContent=grade;gradeEl.style.color=color;} const el = document.getElementById('healthScoreDetail'); if(!el) return; el.innerHTML = `
${grade}
${total}/100
${total>=80?'Excellent — keep it up!':total>=60?'Good progress — a few areas to improve':total>=40?'Getting started — focus on fundamentals':'Take action on the basics first'}
${metrics.map(m=>`
${m.label}
${m.pct} ${Math.round(m.score)}/${m.max} pts
${m.tip}
`).join('')}
`; } /* ════════════════════════════════════════════════════════ AI FINANCIAL ASSISTANT (Anthropic claude-sonnet-4-20250514) User provides their own API key - free tier available ════════════════════════════════════════════════════════ */ let aiSettings = {key: '', enabled: false}; function loadAISettings() { try { const s = localStorage.getItem('bp_ai'); if(s) { aiSettings = JSON.parse(s); // Apply to UI const provEl = document.getElementById('ai-provider'); if(provEl && aiSettings.provider) provEl.value = aiSettings.provider; // Populate each provider's key/model ['anthropic','openai','gemini','perplexity'].forEach(function(p){ const keyEl = document.getElementById('ai-key-' + p); const modelEl = document.getElementById('ai-model-' + p); if(keyEl && aiSettings[p] && aiSettings[p].key) keyEl.value = aiSettings[p].key; if(modelEl && aiSettings[p] && aiSettings[p].model) modelEl.value = aiSettings[p].model; }); if(typeof onAIProviderChange === 'function') onAIProviderChange(); } } catch(e) { console.warn('AI settings load:', e); } } function closeAISettings(event) { const overlay = document.getElementById('aiModalOverlay'); if(overlay) overlay.style.display = 'none'; else { const m = document.getElementById('aiSettingsModal'); if(m) { const ov = m.closest('.modal-overlay'); if(ov) ov.style.display = 'none'; else m.style.display = 'none'; } } } function openAISettings() { // Show overlay by ID (most reliable approach) const overlay = document.getElementById('aiModalOverlay'); if(overlay) { overlay.style.display = 'flex'; } else { // Fallback: find via closest from modal-box const m = document.getElementById('aiSettingsModal'); if(!m) { console.warn('aiSettingsModal not found'); return; } const ov = m.closest('.modal-overlay'); if(ov) ov.style.display = 'flex'; else m.style.display = 'flex'; } // Load saved settings into form fields if(typeof loadAISettings === 'function') loadAISettings(); // Show correct provider pane if(typeof onAIProviderChange === 'function') onAIProviderChange(); } function saveAISettings() { try { const provEl = document.getElementById('ai-provider'); const provider = provEl ? provEl.value : 'anthropic'; aiSettings = aiSettings || {}; aiSettings.provider = provider; ['anthropic','openai','gemini','perplexity'].forEach(function(p){ const keyEl = document.getElementById('ai-key-' + p); const modelEl = document.getElementById('ai-model-' + p); if(keyEl || modelEl) { aiSettings[p] = aiSettings[p] || {}; if(keyEl) aiSettings[p].key = keyEl.value.trim(); if(modelEl) aiSettings[p].model = modelEl.value; } }); localStorage.setItem('bp_ai', JSON.stringify(aiSettings)); if(typeof closeAISettings === 'function') closeAISettings(); if(typeof showToast === 'function') showToast('âÅ"“ AI settings saved · ' + provider.charAt(0).toUpperCase()+provider.slice(1)); } catch(e) { console.error('AI settings save:', e); } } function onAIProviderChange() { const provEl = document.getElementById('ai-provider'); if(!provEl) return; const provider = provEl.value; ['anthropic','openai','gemini','perplexity'].forEach(function(p){ const pane = document.getElementById('ai-pane-' + p); if(pane) pane.style.display = (provider === p) ? 'block' : 'none'; }); } function getAIConfig() { // Returns {provider, key, model} for the active provider if(!aiSettings || !aiSettings.provider) return null; const p = aiSettings.provider; if(p === 'none') return null; const cfg = aiSettings[p]; if(!cfg || !cfg.key) return null; return { provider: p, key: cfg.key, model: cfg.model }; } function openAISettings() { const modal = document.getElementById('aiSettingsModal'); if(modal) { const k = document.getElementById('ai-key'); if(k) k.value = aiSettings.key; modal.classList.add('on'); } } function saveAISettingsForm() { aiSettings.key = document.getElementById('ai-key')?.value?.trim()||''; aiSettings.enabled = !!aiSettings.key; saveAISettings(); document.getElementById('aiSettingsModal')?.classList.remove('on'); showToast(aiSettings.enabled ? 'âÅ"“ AI Assistant enabled' : 'AI key cleared'); updateAIBadge(); } /* ── REBALANCE TARGETS PERSISTENCE ── */ function saveTargets() { try { localStorage.setItem('bp_targets', JSON.stringify(TARGETS)); localStorage.setItem('bp_asset_targets', JSON.stringify(ASSET_TARGETS)); } catch(e){} } function loadTargets() { try { const t = localStorage.getItem('bp_targets'); if(t) TARGETS = {...TARGETS, ...JSON.parse(t)}; const at = localStorage.getItem('bp_asset_targets'); if(at) ASSET_TARGETS = {...ASSET_TARGETS, ...JSON.parse(at)}; } catch(e){} } /* ── MONITOR RULES PERSISTENCE ── */ function saveMonitorRules() { try { const rules = { asset: parseFloat(document.getElementById('ruleAsset')?.value) || 10, sector: parseFloat(document.getElementById('ruleSector')?.value) || 15, value: parseFloat(document.getElementById('ruleValue')?.value) || 10, }; localStorage.setItem('bp_rules', JSON.stringify(rules)); } catch(e){} } function loadMonitorRules() { try { const r = localStorage.getItem('bp_rules'); if(!r) return; const rules = JSON.parse(r); const setV = (id, v) => { const el=document.getElementById(id); if(el&&v!=null) el.value=v; }; setV('ruleAsset', rules.asset); setV('ruleSector', rules.sector); setV('ruleValue', rules.value); } catch(e){} } /* ── PAYROLL FORM RESTORE ── */ function restorePayrollForm() { if(!payrollData || !payrollData.gross) return; const setV = (id, v) => { const el=document.getElementById(id); if(el && v!=null && v!==undefined) el.value=v; }; // Pay type const ptEl = document.getElementById('py-pay-type'); if(ptEl && payrollData.payType) { ptEl.value = payrollData.payType; if(typeof onPayTypeChange === 'function') onPayTypeChange(); } setV('py-gross', payrollData.gross); setV('py-hours', payrollData.hours || 40); setV('py-freq', payrollData.periods); setV('py-status', payrollData.status); const stateEl = document.getElementById('py-state'); if(stateEl && payrollData.stateRate != null) { for(let i=0;i { const el = document.getElementById(id); if(el && el.value) data[id] = el.value; }); localStorage.setItem('bp_budget', JSON.stringify(data)); } catch(e){} } function loadBudget() { try { const s = localStorage.getItem('bp_budget'); if(!s) return; const data = JSON.parse(s); BUDGET_FIELDS.forEach(id => { if(data[id] != null) { const el = document.getElementById(id); if(el) el.value = data[id]; } }); } catch(e){} } function toggleAIChat() { const panel = document.getElementById('aiChatPanel'); const btn = document.getElementById('aiChatBtn'); if(!panel||!btn) return; const isOpen = panel.style.display !== 'none'; panel.style.display = isOpen ? 'none' : 'flex'; btn.style.display = isOpen ? 'flex' : 'none'; if(!isOpen && !aiSettings.key) { setTimeout(()=>{ const out=document.getElementById('ai-chat-output'); if(out&&!out.querySelector('.ai-setup-prompt')){ out.innerHTML+='
âš™ No API key set.
'; } },100); } } function updateAIBadge() { const badge = document.getElementById('aiBadge'); if(!badge) return; badge.textContent = aiSettings.enabled ? 'ðŸ¤â€" AI Active' : 'ðŸ¤â€" AI Setup'; badge.style.color = aiSettings.enabled ? 'var(--green-light)' : 'var(--text-dim)'; } async function askAI(prompt, contextType='general') { const cfg = (typeof getAIConfig === 'function') ? getAIConfig() : null; if(!cfg) { openAISettings(); return null; } // Build rich financial context const context = { portfolio: { totalValue: totalValue, holdings: holdings.map(h=>({ticker:h.t, value:h.mv, allocation:h.alloc.toFixed(1)+'%', gain:h.gain})), sectorBreakdown: getSectorRollup(holdings), }, goals: goals.map(g=>({name:g.name, target:g.target, current:g.current, monthly:g.monthly})), payroll: payrollData, contributions: {total:contribLog.reduce((s,c)=>s+c.amount,0), count:contribLog.length}, budget: { housing: document.getElementById('b-housing')?.value||0, totalExpenses: ['b-housing','b-transport','b-food','b-utilities','b-insurance','b-subs','b-entertainment','b-other','b-debt-min'].reduce((s,id)=>s+(parseFloat(document.getElementById(id)?.value)||0),0), } }; const sysPrompt = `You are a friendly, knowledgeable personal financial planning assistant inside BellPathâ„¢, an investment dashboard app. You help users understand their financial situation and make better decisions. Always be honest that you provide educational information, not personalized financial advice. Keep responses concise (3-5 sentences max unless asked for detail). User's financial context: ${JSON.stringify(context)} Always end with a specific actionable next step.`; try { let res, data, text = null; if(cfg.provider === 'anthropic') { res = await fetch('https://api.anthropic.com/v1/messages', { method:'POST', headers:{ 'Content-Type':'application/json', 'x-api-key': cfg.key, 'anthropic-version':'2023-06-01', 'anthropic-dangerous-direct-browser-access':'true' }, body: JSON.stringify({ model: cfg.model || 'claude-sonnet-4-5', max_tokens: 1024, system: sysPrompt, messages: [{role:'user', content: prompt}] }) }); if(!res.ok) { return await handleAIError(res, 'Anthropic'); } data = await res.json(); text = data.content?.[0]?.text; } else if(cfg.provider === 'openai') { res = await fetch('https://api.openai.com/v1/chat/completions', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization': 'Bearer ' + cfg.key }, body: JSON.stringify({ model: cfg.model || 'gpt-4o', max_tokens: 1024, messages: [ {role:'system', content: sysPrompt}, {role:'user', content: prompt} ] }) }); if(!res.ok) { return await handleAIError(res, 'OpenAI'); } data = await res.json(); text = data.choices?.[0]?.message?.content; } else if(cfg.provider === 'gemini') { const model = cfg.model || 'gemini-1.5-pro'; res = await fetch('https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + encodeURIComponent(cfg.key), { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ systemInstruction: { parts: [{text: sysPrompt}] }, contents: [{ role:'user', parts: [{text: prompt}] }], generationConfig: { maxOutputTokens: 1024 } }) }); if(!res.ok) { return await handleAIError(res, 'Google Gemini'); } data = await res.json(); text = data.candidates?.[0]?.content?.parts?.[0]?.text; } else if(cfg.provider === 'perplexity') { res = await fetch('https://api.perplexity.ai/chat/completions', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization': 'Bearer ' + cfg.key }, body: JSON.stringify({ model: cfg.model || 'llama-3.1-sonar-large-128k-online', max_tokens: 1024, messages: [ {role:'system', content: sysPrompt}, {role:'user', content: prompt} ] }) }); if(!res.ok) { return await handleAIError(res, 'Perplexity'); } data = await res.json(); text = data.choices?.[0]?.message?.content; } return text || null; } catch(e) { console.error('AI error:', e); return 'âš  Network error: ' + (e.message || 'Could not reach AI provider') + '. Check your internet connection.'; } } async function handleAIError(res, providerName) { let errMsg = providerName + ' API error: ' + res.status; try { const err = await res.json(); if(err.error?.message) errMsg = err.error.message; if(res.status === 401 || res.status === 403) { showToast('âš  Invalid API key for ' + providerName); return 'âš  Authentication failed: ' + errMsg + '\n\nCheck your API key in AI Settings.'; } if(res.status === 429) { return '⏱ ' + providerName + ' rate limit reached. Wait a moment and try again.'; } } catch(e) {} return 'âš  ' + errMsg + '\n\nIf this persists, check your account credits.'; } async function submitAIChat() { const input = document.getElementById('ai-chat-input'); const output = document.getElementById('ai-chat-output'); if(!input||!output) return; const msg = input.value.trim(); if(!msg) return; // Add user message output.innerHTML += `
${msg}
`; input.value = ''; output.innerHTML += '
ðŸ¤â€" Thinking…
'; output.scrollTop = output.scrollHeight; const reply = await askAI(msg); document.getElementById('ai-thinking')?.remove(); if(reply) { output.innerHTML += `
${reply.replace(/\n/g,'
')}
`; } else { output.innerHTML += `
Could not get AI response. Check your API key in AI Settings.
`; } output.scrollTop = output.scrollHeight; } /* ═══════════════════════════════════════════════════ LIVE MARKET DATA ENGINE — BellPathâ„¢ v2.1 Primary: Finnhub (free · 60 req/min · CORS âÅ"“) Secondary: Alpha Vantage (free · 25 req/day · CORS âÅ"“) NOTE: GOOGLEFINANCE() is a Google Sheets formula ONLY. It cannot be called from a browser JavaScript app — there is no public Google Finance API. Yahoo Finance has no official API and blocks browser CORS. BellPathâ„¢ uses Finnhub and Alpha Vantage instead. ═══════════════════════════════════════════════════ */ let liveDataSettings={provider:'demo',finnhubKey:'',alphaKey:'',autoRefresh:false,refreshInterval:60,lastRefreshed:null}; let liveRefreshTimer=null; let livePriceCache={}; function loadLiveSettings(){try{const s=localStorage.getItem('bp_live');if(s)liveDataSettings={...liveDataSettings,...JSON.parse(s)};}catch(e){}} function saveLiveSettings(){try{localStorage.setItem('bp_live',JSON.stringify(liveDataSettings));}catch(e){}} async function fetchFinnhubPrice(ticker){ const key=liveDataSettings.finnhubKey.trim();if(!key)return null; let sym=ticker; if(ticker.endsWith('-USD'))sym='BINANCE:'+ticker.replace('-USD','')+'USDT'; try{ const r=await fetch(`https://finnhub.io/api/v1/quote?symbol=${encodeURIComponent(sym)}&token=${key}`,{signal:AbortSignal.timeout(6000)}); if(!r.ok)return null; const d=await r.json(); if(!d||!d.c||d.c===0)return null; return{price:d.c,change:+((d.c-d.pc)/d.pc*100).toFixed(2),high:d.h,low:d.l,source:'Finnhub',ts:Date.now()}; }catch(e){return null;} } async function fetchAlphaPrice(ticker){ const key=liveDataSettings.alphaKey.trim();if(!key)return null; try{ const r=await fetch(`https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${encodeURIComponent(ticker)}&apikey=${key}`,{signal:AbortSignal.timeout(8000)}); if(!r.ok)return null; const d=await r.json(); const q=d['Global Quote']; if(!q||!q['05. price'])return null; return{price:parseFloat(q['05. price']),change:parseFloat((q['10. change percent']||'0').replace('%','')),source:'Alpha Vantage',ts:Date.now()}; }catch(e){return null;} } async function refreshLivePrices(){ const tickers=[...new Set(portfolio.map(p=>p.t))]; if(!tickers.length){showToast('âš  Add positions first');return;} const noKey=liveDataSettings.provider==='demo'||(liveDataSettings.provider==='finnhub'&&!liveDataSettings.finnhubKey)||(liveDataSettings.provider==='alphavantage'&&!liveDataSettings.alphaKey); if(noKey){showToast('âš  Configure a data provider in âš™ Data Settings');openLiveDataSettings();return;} const btn=document.getElementById('refreshPricesBtn'); if(btn){btn.textContent='⏳ Fetching...';btn.disabled=true;} let fetched=0,failed=0; for(const ticker of tickers){ let result=null; if(liveDataSettings.provider==='finnhub')result=await fetchFinnhubPrice(ticker); else if(liveDataSettings.provider==='alphavantage'){result=await fetchAlphaPrice(ticker);await new Promise(r=>setTimeout(r,1300));} if(result){ livePriceCache[ticker]=result; const e=TICKER_DB.find(t=>t.t===ticker); if(e){e.p=result.price;e.c=result.change;} fetched++; }else failed++; } liveDataSettings.lastRefreshed=new Date().toISOString();saveLiveSettings(); if(fetched>0){ rebuildPortfolio();generateFeed(); const el=document.getElementById('lastRefreshed'); if(el)el.textContent='🟢 Live · '+new Date().toLocaleTimeString()+' · '+fetched+' prices'; showToast(`âÅ"“ ${fetched} live prices from ${liveDataSettings.provider==='finnhub'?'Finnhub':'Alpha Vantage'}${failed?' · '+failed+' kept demo':''}`); updateDataSourceBadge(); }else{showToast('âš  No prices fetched — check API key or network');} if(btn){btn.textContent='🔄 Refresh Prices';btn.disabled=false;} } async function fetchLiveQuoteForLookup(ticker){ if(liveDataSettings.provider==='finnhub'&&liveDataSettings.finnhubKey)return fetchFinnhubPrice(ticker); if(liveDataSettings.provider==='alphavantage'&&liveDataSettings.alphaKey)return fetchAlphaPrice(ticker); return null; } let alertHourlyTimer = null; function startAutoRefresh(){ if(liveRefreshTimer) clearInterval(liveRefreshTimer); if(liveDataSettings.autoRefresh&&liveDataSettings.provider!=='demo') liveRefreshTimer=setInterval(refreshLivePrices,liveDataSettings.refreshInterval*1000); // Load market news setTimeout(loadMarketNews, 800); // Alerts always sync every hour regardless of data provider if(alertHourlyTimer) clearInterval(alertHourlyTimer); alertHourlyTimer = setInterval(function(){ if(typeof runMonitorScan === 'function' && holdings.length > 0) { runMonitorScan(); generateFeed(); const el = document.getElementById('lastRefreshed'); if(el) el.textContent = '🔔 Alerts synced · ' + new Date().toLocaleTimeString(); } }, 3600000); // every 60 minutes } /* ── AI settings consolidated into Data Settings modal ── */ function loadAISettingsInDS() { try { var s = localStorage.getItem('bp_ai'); if(!s) { // default to "none" so the user has to actively pick a provider var p = document.getElementById('ds-ai-provider'); if(p) p.value = 'none'; onDSAIProviderChange(); return; } var ai = JSON.parse(s); var provEl = document.getElementById('ds-ai-provider'); if(provEl) provEl.value = ai.provider || 'none'; ['anthropic','openai','gemini','perplexity'].forEach(function(prov) { var keyEl = document.getElementById('ds-ai-key-' + prov); var modelEl = document.getElementById('ds-ai-model-' + prov); if(keyEl && ai[prov] && ai[prov].key) keyEl.value = ai[prov].key; if(modelEl && ai[prov] && ai[prov].model) modelEl.value = ai[prov].model; }); onDSAIProviderChange(); } catch(e) { console.warn('AI load:', e); } } function onDSAIProviderChange() { var provEl = document.getElementById('ds-ai-provider'); if(!provEl) return; var provider = provEl.value; ['anthropic','openai','gemini','perplexity'].forEach(function(p) { var pane = document.getElementById('ds-ai-pane-' + p); if(pane) pane.style.display = (provider === p) ? 'block' : 'none'; }); } function saveAISettingsInDS() { try { var provEl = document.getElementById('ds-ai-provider'); var provider = provEl ? provEl.value : 'none'; var ai = {}; try { ai = JSON.parse(localStorage.getItem('bp_ai') || '{}'); } catch(e) { ai = {}; } ai.provider = provider; ['anthropic','openai','gemini','perplexity'].forEach(function(p) { var keyEl = document.getElementById('ds-ai-key-' + p); var modelEl = document.getElementById('ds-ai-model-' + p); ai[p] = ai[p] || {}; if(keyEl) ai[p].key = keyEl.value.trim(); if(modelEl) ai[p].model = modelEl.value; }); // Update in-memory aiSettings too (used by askAI/getAIConfig) if(typeof aiSettings !== 'undefined') { aiSettings = ai; } localStorage.setItem('bp_ai', JSON.stringify(ai)); } catch(e) { console.warn('AI save:', e); } } function openLiveDataSettings(){ const m=document.getElementById('liveSettingsModal');if(!m)return; m.classList.add('on'); const p=document.getElementById('ls-provider');if(p)p.value=liveDataSettings.provider; const f=document.getElementById('ls-finnhub');if(f)f.value=liveDataSettings.finnhubKey; const a=document.getElementById('ls-alpha');if(a)a.value=liveDataSettings.alphaKey; const ar=document.getElementById('ls-autorefresh');if(ar)ar.checked=liveDataSettings.autoRefresh; const ri=document.getElementById('ls-interval');if(ri)ri.value=liveDataSettings.refreshInterval; toggleLSProvider(); loadAISettingsInDS(); } function closeLiveDataSettings(){document.getElementById('liveSettingsModal')?.classList.remove('on');} function toggleLSProvider(){ const p=document.getElementById('ls-provider')?.value; const f=document.getElementById('ls-finnhub-row');if(f)f.style.display=p==='finnhub'?'block':'none'; const a=document.getElementById('ls-alpha-row');if(a)a.style.display=p==='alphavantage'?'block':'none'; } function saveLiveDataSettings(){ liveDataSettings.provider=document.getElementById('ls-provider')?.value||'demo'; liveDataSettings.finnhubKey=document.getElementById('ls-finnhub')?.value?.trim()||''; liveDataSettings.alphaKey=document.getElementById('ls-alpha')?.value?.trim()||''; liveDataSettings.autoRefresh=document.getElementById('ls-autorefresh')?.checked||false; liveDataSettings.refreshInterval=parseInt(document.getElementById('ls-interval')?.value)||60; saveLiveSettings();startAutoRefresh();closeLiveDataSettings();updateDataSourceBadge(); showToast('âÅ"“ Live data settings saved'); saveAISettingsInDS(); } function updateDataSourceBadge(){ const b=document.getElementById('dataSourceBadge');if(!b)return; const p=liveDataSettings.provider; const hasKey=p==='finnhub'?!!liveDataSettings.finnhubKey:p==='alphavantage'?!!liveDataSettings.alphaKey:false; if(p==='demo'||!hasKey){ b.innerHTML='📊 DEMO DATA — not real prices'; }else{ const name=p==='finnhub'?'Finnhub':'Alpha Vantage'; b.innerHTML=`🟢 LIVE DATA · ${name}`; } } document.addEventListener('DOMContentLoaded',()=>{ if(typeof setupAutoLock === "function") setupAutoLock(); if(typeof loadSecuritySettings === "function") loadSecuritySettings(); // Load all persisted state FIRST, before any rendering loadStoredPortfolio(); loadGoals(); loadPaperAccount(); // Re-enrich portfolio from stored data holdings = enrichPortfolio(portfolio); totalValue = holdings.reduce((s,h)=>s+h.mv,0); renderMarketTicker(); initLineChart(); initDonutChart(); initBarChart(); initDriftChart(); renderHoldings(); initBaseline(); runMonitorScan(); renderQuickAdd(); renderPortfolioList(); renderWatchlist(); renderPortfolioSidebar(); updateSim(); generateFeed(); // New tools updateFearGreed(); updateDrawdownCoach(); renderGlossary(); // Goals + Paper Trade renderGoalAll(); renderPaperAccount(); // Wire the "previously held" toggle wirePrevHeldToggle(); // Final value card sync — catches any race conditions updateValueCard(); // Initialize literacy hub (pre-render for instant display) renderLiteracyHub(); // Auto-sync with debt resolution app autoSyncDebt(); // PIN lock — show first thing pinBoot(); // Load AI + payroll settings loadAISettings(); loadPayrollData(); renderContributions(); // Restore payroll form fields from saved data setTimeout(restorePayrollForm, 50); // Load watchlist loadWatchlist(); renderWatchlist(); // Load rebalance targets loadTargets(); // Load monitor rules loadMonitorRules(); // Load alert state loadAlertsState(); // Load budget loadBudget(); // Load paper trading loadPaperAccount(); // Load live data settings loadLiveSettings(); updateDataSourceBadge(); startAutoRefresh(); // Animate slider fills on load document.querySelectorAll('input[type=range]').forEach(sl=>{ const pct=((sl.value-sl.min)/(sl.max-sl.min))*100; sl.style.setProperty('--pct',pct+'%'); }); // Auto-tick market tape + Fear & Greed setInterval(()=>{ TICKER_DB.forEach(t=>{t.c+=(Math.random()-.5)*.04;t.p=+(t.p*(1+(Math.random()-.5)*.001)).toFixed(2);}); renderMarketTicker(); updateFearGreed(); },8000); // Drift timestamp + auto monitor scan + drawdown coach setInterval(()=>{ const dt=document.getElementById('driftTime'); if(dt) dt.textContent='Updated '+new Date().toLocaleTimeString(); holdings = enrichPortfolio(portfolio); totalValue = holdings.reduce((s,h)=>s+h.mv,0); updateValueCard(); runMonitorScan(); updateDrawdownCoach(); },30000); // Onboarding: only show on first visit try { if(!localStorage.getItem('bp_onboarded')) { setTimeout(showOnboard, 600); } } catch(e){ /* localStorage unavailable */ } }); /* ─── PIN LOCK LOGIC ─── */ var pinBuffer = ''; var pinMode = 'check'; // 'setup', 'confirm', 'check' var pinFirstEntry = ''; var pinAttempts = 0; async function pinHash(pin) { if(!pin) return ''; const buf = new TextEncoder().encode(pin + 'bellpath-salt-v1'); const h = await crypto.subtle.digest('SHA-256', buf); return Array.from(new Uint8Array(h)).map(b=>b.toString(16).padStart(2,'0')).join(''); } function pinUpdateDots() { document.querySelectorAll('#pinDots .pin-dot').forEach(function(d,i){ d.classList.toggle('filled', i < pinBuffer.length); }); } function pinDigit(d) { if(pinBuffer.length >= 4) return; pinBuffer += d; pinUpdateDots(); if(pinBuffer.length === 4) { setTimeout(pinSubmit, 200); } } function pinResetPrompt() { var ok = confirm('Reset your PIN?\n\nThis clears your PIN lock so you can set a new one. Your data (portfolio, goals, settings) is NOT deleted \u2014 only the PIN lock is removed.\n\nNote: this is a casual privacy lock, so anyone with access to this device can reset it.'); if(!ok) return; try { localStorage.removeItem('bp_pin_hash'); localStorage.removeItem('bp_pin_skip'); sessionStorage.removeItem('bp_session_unlocked'); localStorage.removeItem('bp_session_unlocked'); } catch(e){} if(typeof pinShowSetup === 'function') { pinShowSetup(); } else { try{ document.getElementById('pinOverlay').classList.remove('on'); document.body.style.overflow=''; }catch(e){} location.reload(); } } function pinClear() { pinBuffer = ''; pinUpdateDots(); pinMsg(''); } function pinBackspace() { pinBuffer = pinBuffer.slice(0,-1); pinUpdateDots(); } function pinMsg(text, type) { const el = document.getElementById('pinMsg'); if(!el) return; el.textContent = text || '\u00A0'; el.className = 'pin-msg' + (type ? ' ' + type : ''); } async function pinSubmit() { if(pinMode === 'setup') { pinFirstEntry = pinBuffer; pinBuffer = ''; pinUpdateDots(); pinMode = 'confirm'; document.getElementById('pinTitle').textContent = 'Confirm PIN'; document.getElementById('pinSub').textContent = 'Enter the same 4 digits again'; pinMsg(''); return; } if(pinMode === 'confirm') { if(pinBuffer === pinFirstEntry) { const hash = await pinHash(pinBuffer); try { localStorage.setItem('bp_pin_hash', hash); } catch(e) {} pinMsg('âÅ"“ PIN set successfully', 'success'); setTimeout(pinUnlock, 800); } else { pinMsg('PINs don\'t match — try again', 'error'); pinBuffer = ''; pinFirstEntry = ''; pinMode = 'setup'; document.getElementById('pinTitle').textContent = 'Set Your PIN'; document.getElementById('pinSub').textContent = 'Choose a 4-digit PIN'; setTimeout(function(){ pinUpdateDots(); pinMsg(''); }, 1500); } return; } // Check mode const stored = localStorage.getItem('bp_pin_hash'); const entered = await pinHash(pinBuffer); if(entered === stored) { pinMsg('âÅ"“ Unlocked', 'success'); setTimeout(pinUnlock, 400); } else { pinAttempts++; pinMsg('Incorrect PIN' + (pinAttempts >= 3 ? ' (' + pinAttempts + ' attempts)' : ''), 'error'); pinBuffer = ''; setTimeout(function(){ pinUpdateDots(); }, 600); if(pinAttempts >= 5) { pinMsg('Too many attempts. Reload to try again.', 'error'); document.querySelectorAll('.pin-key').forEach(function(b){b.disabled=true;}); } } } function pinSkip() { if(pinMode === 'check') return; // can't skip when checking if(confirm('Skip PIN setup? You can enable it later in Settings.')) { try { localStorage.setItem('bp_pin_skip','1'); } catch(e) {} pinUnlock(); } } function pinUnlock() { try{ sessionStorage.setItem('bp_session_unlocked','1'); }catch(e){} document.getElementById('pinOverlay').classList.remove('on'); document.body.style.overflow = ''; } function pinShowSetup() { var _fb=document.getElementById('pinForgotBtn'); if(_fb) _fb.style.display='none'; pinMode = 'setup'; pinBuffer = ''; pinFirstEntry = ''; document.getElementById('pinTitle').textContent = 'Set Your PIN'; document.getElementById('pinSub').textContent = 'Choose a 4-digit PIN to lock your data'; document.getElementById('pinSkipBtn').style.display = 'inline-block'; pinUpdateDots(); pinMsg(''); document.getElementById('pinOverlay').classList.add('on'); document.body.style.overflow = 'hidden'; } function pinShowCheck() { pinMode = 'check'; pinBuffer = ''; document.getElementById('pinTitle').textContent = 'Unlock BellPathâ„¢'; document.getElementById('pinSub').textContent = 'Enter your 4-digit PIN'; document.getElementById('pinSkipBtn').style.display = 'none'; var _fb=document.getElementById('pinForgotBtn'); if(_fb) _fb.style.display='block'; pinUpdateDots(); pinMsg(''); document.getElementById('pinOverlay').classList.add('on'); document.body.style.overflow = 'hidden'; } function pinReset() { if(!confirm('Remove your PIN? Anyone with browser access will be able to open BellPathâ„¢ without entering a PIN.')) return; try { localStorage.removeItem('bp_pin_hash'); localStorage.removeItem('bp_pin_skip'); } catch(e) {} alert('PIN removed. You will be asked to set a new one next time you open BellPathâ„¢.'); } function pinChange() { pinShowSetup(); } // Boot: check if PIN exists, show appropriate screen function pinBoot() { try { const hash = localStorage.getItem('bp_pin_hash'); const skipped = localStorage.getItem('bp_pin_skip'); var __sess=false; try{ __sess = sessionStorage.getItem('bp_session_unlocked')==='1' || localStorage.getItem('bp_session_unlocked')==='1'; }catch(e){} if(hash && __sess){ try{ sessionStorage.setItem('bp_session_unlocked','1'); }catch(e){} pinUnlock(); return; } if(hash) { pinShowCheck(); } else if(!skipped) { pinShowSetup(); } } catch(e) {} } // Keyboard input document.addEventListener('keydown', function(e) { const overlay = document.getElementById('pinOverlay'); if(!overlay || !overlay.classList.contains('on')) return; if(/^[0-9]$/.test(e.key)) { pinDigit(e.key); e.preventDefault(); } else if(e.key === 'Backspace') { pinBackspace(); e.preventDefault(); } else if(e.key === 'Escape') { pinClear(); e.preventDefault(); } }); /* ════════════════════════════════════════ SECURITY POLICIES — encryption, auto-lock, data export ════════════════════════════════════════ */ // ── AES-256 encryption helpers (WebCrypto) ── async function bp_getEncryptionKey() { // Derive a stable per-device key from a generated salt stored in localStorage let salt = localStorage.getItem('bp_sec_salt'); if(!salt) { const a = new Uint8Array(16); crypto.getRandomValues(a); salt = Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join(''); localStorage.setItem('bp_sec_salt', salt); } const enc = new TextEncoder(); const baseKey = await crypto.subtle.importKey('raw', enc.encode('bellpath-device-key-v1'), {name:'PBKDF2'}, false, ['deriveKey']); const saltBuf = enc.encode(salt); return crypto.subtle.deriveKey( {name:'PBKDF2', salt:saltBuf, iterations:100000, hash:'SHA-256'}, baseKey, {name:'AES-GCM', length:256}, false, ['encrypt','decrypt']); } async function bp_encrypt(plaintext) { if(!plaintext) return ''; try { const key = await bp_getEncryptionKey(); const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = new TextEncoder().encode(plaintext); const ct = await crypto.subtle.encrypt({name:'AES-GCM', iv}, key, enc); const ctArr = new Uint8Array(ct); const combined = new Uint8Array(iv.length + ctArr.length); combined.set(iv); combined.set(ctArr, iv.length); return 'enc:' + btoa(String.fromCharCode.apply(null, combined)); } catch(e) { console.warn('Encrypt failed, storing plain:', e); return plaintext; } } async function bp_decrypt(ciphertext) { if(!ciphertext) return ''; if(!ciphertext.startsWith('enc:')) return ciphertext; // plaintext legacy try { const key = await bp_getEncryptionKey(); const data = Uint8Array.from(atob(ciphertext.slice(4)), c=>c.charCodeAt(0)); const iv = data.slice(0,12); const ct = data.slice(12); const pt = await crypto.subtle.decrypt({name:'AES-GCM', iv}, key, ct); return new TextDecoder().decode(pt); } catch(e) { console.warn('Decrypt failed:', e); return ''; } } // ── Security settings ── function loadSecuritySettings() { try { const s = JSON.parse(localStorage.getItem('bp_security') || '{}'); const sel = document.getElementById('sec-autolock'); if(sel && s.autolockSec !== undefined) sel.value = s.autolockSec; } catch(e) {} } function saveSecuritySettings() { try { const sel = document.getElementById('sec-autolock'); const s = { autolockSec: sel ? parseInt(sel.value) : 900 }; localStorage.setItem('bp_security', JSON.stringify(s)); setupAutoLock(); if(typeof showToast === 'function') showToast('âÅ"“ Security settings saved'); } catch(e) {} } // ── Session auto-lock ── var bp_autoLockTimer = null; function setupAutoLock() { if(bp_autoLockTimer) clearTimeout(bp_autoLockTimer); let secs = 900; // default 15min try { const s = JSON.parse(localStorage.getItem('bp_security') || '{}'); if(s.autolockSec !== undefined) secs = parseInt(s.autolockSec); } catch(e) {} if(secs <= 0) return; // disabled if(!localStorage.getItem('bp_pin_hash')) return; // no PIN set bp_autoLockTimer = setTimeout(function() { const ov = document.getElementById('pinOverlay'); if(ov) { ov.classList.add('on'); if(typeof showToast==='function') showToast('🔆Locked due to inactivity'); } }, secs * 1000); } ['click','keydown','mousemove','scroll','touchstart'].forEach(function(ev) { document.addEventListener(ev, function() { setupAutoLock(); }, {passive:true}); }); // ── Modal open/close ── function openSecurityModal() { const ov = document.getElementById('securityModalOverlay'); if(ov) ov.style.display = 'flex'; loadSecuritySettings(); } function closeSecurityModal() { const ov = document.getElementById('securityModalOverlay'); if(ov) ov.style.display = 'none'; } // ── Data export ── function exportAllData() { try { const data = {}; for(let i=0; i localStorage.removeItem(k)); alert('âÅ"“ All BellPathâ„¢ data has been cleared. Reloading…'); location.reload(); } catch(e) { alert('Clear failed: ' + e.message); } } /* ── Hamburger nav toggle (mobile) ── */ function bp_toggleNavMenu() { var navs = document.querySelectorAll('.nav-tabs, nav.nav-tabs, .tabs'); navs.forEach(function(n) { n.classList.toggle('mobile-open'); }); } // Close menu when a tab is clicked (mobile) document.addEventListener('click', function(e) { var t = e.target; if(t && (t.classList.contains('nav-tab') || t.classList.contains('tab')) && window.innerWidth <= 900) { var navs = document.querySelectorAll('.nav-tabs.mobile-open, .tabs.mobile-open'); navs.forEach(function(n) { n.classList.remove('mobile-open'); }); } }); // Close when clicking outside document.addEventListener('click', function(e) { if(e.target.closest('.nav-hamburger') || e.target.closest('.nav-tabs') || e.target.closest('.tabs')) return; document.querySelectorAll('.nav-tabs.mobile-open, .tabs.mobile-open').forEach(function(n) { n.classList.remove('mobile-open'); }); }); function bp_toggleSubtabDropdown(wrapId) { var w = document.getElementById(wrapId); if(!w) return; w.classList.toggle('open'); } function bp_updateSubtabDropdownLabel(wrapId, label) { var w = document.getElementById(wrapId); if(!w) return; var btn = w.querySelector('.subtab-dropdown-btn .label'); if(btn) btn.textContent = label; w.classList.remove('open'); } document.addEventListener('click', function(e) { if(e.target.closest('.subtab-dropdown-wrap')) return; document.querySelectorAll('.subtab-dropdown-wrap.open').forEach(function(w) { w.classList.remove('open'); }); });