diff --git a/app_accounts.html b/app_accounts.html
index 43d7f7d..748b34c 100644
--- a/app_accounts.html
+++ b/app_accounts.html
@@ -20,6 +20,9 @@ td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
.chart{width:100%;height:120px;margin:10px 0}.chart polyline{fill:none;stroke:var(--accent);stroke-width:2}.chart circle{fill:var(--accent)}.chart text{fill:var(--mute);font-size:11px}
.facts{display:flex;gap:22px;flex-wrap:wrap;margin-top:4px}.facts div{font-size:12px;color:var(--mute)}.facts b{display:block;font-size:14px;color:var(--ink)}
.empty{text-align:center;padding:36px 16px}
+.payoff{margin:14px 0 4px;padding:14px 16px;border:1px solid var(--line);border-radius:10px;background:var(--none)}
+.payoff .big{font-size:22px}.payoff .row{align-items:center}.payoff input{width:90px;text-align:right;font:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:5px 8px}
+.payoff .win{color:var(--ok);font-weight:600}
.tabs{position:sticky;top:0;z-index:5;display:flex;gap:4px;margin:0 0 16px;padding:10px 0;background:var(--bg)}
.tabs a{flex:1;max-width:170px;text-align:center;padding:8px 14px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--mute);font-weight:600;font-size:14px;text-decoration:none}
.tabs a:hover{color:var(--ink)}.tabs a.on{background:var(--accent);border-color:var(--accent);color:#fff}
@@ -49,6 +52,41 @@ function chart(pts){ // pts: [[date, balance]] oldest first
xs.map((x,i)=>`${mshort(pts[i][0])}: ${money(pts[i][1])}`).join("")+
`${mshort(pts[0][0])}${mshort(pts[pts.length-1][0])}`;
}
+// Walk the loan forward month by month: interest = balance x rate/12, then the payment comes off (matches your statements to the cent).
+function payoff(bal,rate,pay,extra,first){
+ let n=0,interest=0,d=new Date(first+"T12:00:00");const day=d.getDate();
+ while(bal>0.005&&n<1200){
+ const i=bal*rate/1200,p=pay+extra;interest+=i;n++;
+ if(p>=bal+i){bal=0;break}
+ bal=bal+i-p;d=new Date(d.getFullYear(),d.getMonth()+1,Math.min(day,28));
+ if(day>28){const last=new Date(d.getFullYear(),d.getMonth()+1,0).getDate();d.setDate(Math.min(day,last))}
+ }
+ return{n,interest,date:d,ok:bal<=0.005};
+}
+function togo(date){
+ const now=new Date();let y=date.getFullYear()-now.getFullYear(),m=date.getMonth()-now.getMonth(),dd=date.getDate()-now.getDate();
+ if(dd<0){m--;dd+=new Date(date.getFullYear(),date.getMonth(),0).getDate()}if(m<0){y--;m+=12}
+ const part=(n,w)=>n?`${n} ${w}${n===1?"":"s"}`:"";
+ return [part(y,"year"),part(m,"month"),part(dd,"day")].filter(Boolean).join(", ")||"today";
+}
+const dlong=d=>MON[d.getMonth()]+" "+d.getDate()+", "+d.getFullYear();
+function payoffHTML(idx,L){
+ if(!(L.balance>0&&L.rate&&L.payment))return"";
+ return `
At today’s rate and payment, this loan is paid off on
+
`;
+}
+function wirePayoff(idx,L){
+ const el=$("#poe"+idx);if(!el)return;
+ const first=L.due||L.date,base=payoff(L.balance,L.rate,L.payment,0,first);
+ const run=()=>{
+ const x=Math.max(0,+el.value||0),r=payoff(L.balance,L.rate,L.payment,x,first);
+ $("#pod"+idx).textContent=dlong(base.date);
+ $("#pot"+idx).innerHTML=`${togo(base.date)} to go · ${base.n} payments left · about ${money(base.interest,0)} interest still to pay`;
+ const k=base.n-r.n;
+ $("#pow"+idx).innerHTML=x>0&&r.ok?`Paid off ${dlong(r.date)}: ${Math.floor(k/12)} yr ${k%12} mo sooner, saving about ${money(base.interest-r.interest,0)} in interest.`:"";
+ };
+ el.oninput=run;run();
+}
async function load(){
const j=await (await fetch("/api/accounts")).json();
if(j.error){$("#out").innerHTML=`${esc(j.error)}
`;return}
@@ -71,11 +109,12 @@ async function load(){
if(isLoan){if(L.rate!=null)h+=`Interest rate${L.rate}%
`;if(L.payment!=null)h+=`Monthly payment${money(L.payment)}
`;if(L.due)h+=`Next due${dfmt(L.due)}
`;
const pr=rs.reduce((t,r)=>t+(r.principal||0),0),it=rs.reduce((t,r)=>t+(r.interest||0),0);h+=`Principal paid (${rs.length} mo)${money(pr)}
Interest paid (${rs.length} mo)${money(it)}
`}
else{h+=`Interest this year${money(L.ytd)}
`;if(L.rate)h+=`APY earned${L.rate}%
`}
- h+=`${chart(rs.map(r=>[r.date,r.balance]))}`;
+ h+=`${isLoan?payoffHTML(a.name.replace(/\W+/g,"_"),L):""}${chart(rs.map(r=>[r.date,r.balance]))}`;
h+=`| Statement | Balance | Interest | ${isLoan?'Principal | Payment | ':'Deposits | Withdrawals | '}
`+
rs.slice().reverse().slice(0,12).map(r=>`| ${dfmt(r.date)} | ${money(r.balance)} | ${money(r.interest)} | ${isLoan?`${money(r.principal)} | ${money(r.payment)} | `:`${money(r.deposits)} | ${money(r.withdrawals)} | `}
`).join("")+`
`;
}
$("#out").innerHTML=h;
+ for(const a of loan)wirePayoff(a.name.replace(/\W+/g,"_"),a.last);
}
load();