feat(rMortgage): aggregate pool viz, earnings comparison, fewer tranches

- Pool summary cards clickable → aggregate breakdown vessel showing
  total outstanding/repaid/reinvested with earnings comparison bar
- Lender detail: earnings bar (interest vs reinvestment), advantage
  callout showing % gain from reinvesting returns
- Borrower options reduced from 6 to 3 tranches (10yr, 20yr, 30yr)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-15 02:14:39 -07:00
parent d955d18af4
commit a1f8702988
1 changed files with 180 additions and 61 deletions

View File

@ -166,7 +166,7 @@ class FolkFlowsApp extends HTMLElement {
// Borrower options state // Borrower options state
private borrowerMonthlyBudget = 1500; private borrowerMonthlyBudget = 1500;
private borrowerOptionsVisible = false; private showPoolOverview = false;
// Tour engine // Tour engine
private _tour!: TourEngine; private _tour!: TourEngine;
@ -5155,6 +5155,18 @@ class FolkFlowsApp extends HTMLElement {
this.render(); this.render();
}); });
this.shadow.querySelector('[data-action="toggle-pool"]')?.addEventListener('click', () => {
this.showPoolOverview = !this.showPoolOverview;
this.selectedLenderId = null;
this.render();
});
this.shadow.querySelector('[data-action="close-pool"]')?.addEventListener('click', (e) => {
e.stopPropagation();
this.showPoolOverview = false;
this.render();
});
this.shadow.querySelector('[data-action="update-borrower"]')?.addEventListener('click', () => { this.shadow.querySelector('[data-action="update-borrower"]')?.addEventListener('click', () => {
const budgetEl = this.shadow.querySelector('[data-borrower="budget"]') as HTMLInputElement; const budgetEl = this.shadow.querySelector('[data-borrower="budget"]') as HTMLInputElement;
if (budgetEl) this.borrowerMonthlyBudget = Math.max(parseFloat(budgetEl.value) || 100, 100); if (budgetEl) this.borrowerMonthlyBudget = Math.max(parseFloat(budgetEl.value) || 100, 100);
@ -5278,15 +5290,35 @@ class FolkFlowsApp extends HTMLElement {
this.render(); this.render();
} }
/** Compute aggregate pool stats across all positions */
private computePoolStats() {
const reinvestApy = this.reinvestmentPositions.length > 0
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 4.0;
let totalBorrowed = 0, totalRepaid = 0, totalReinvested = 0, totalLoanEarnings = 0, totalReinvestEarnings = 0;
for (const m of this.mortgagePositions) {
if (m.status !== 'active' && m.status !== 'paid-off') continue;
const me = Math.floor((Date.now() - m.startDate) / (86400000 * 30));
const paid = m.monthlyPayment * me;
const intPaid = paid - (paid * m.principal / (m.monthlyPayment * m.termMonths));
const prinRepaid = Math.min(paid - intPaid, m.principal);
const idle = prinRepaid * 0.6;
totalBorrowed += m.principal;
totalRepaid += prinRepaid;
totalReinvested += idle;
totalLoanEarnings += intPaid;
totalReinvestEarnings += idle * (reinvestApy / 100) * (me / 12);
}
const deployed = this.reinvestmentPositions.reduce((s, r) => s + r.deposited, 0);
const yieldValue = this.reinvestmentPositions.reduce((s, r) => s + (r.currentValue - r.deposited), 0);
return { totalBorrowed, totalRepaid, totalReinvested, totalLoanEarnings, totalReinvestEarnings, deployed, yieldValue, reinvestApy };
}
private renderMortgageTab(): string { private renderMortgageTab(): string {
if (this.loading) return '<div class="flows-loading">Loading mortgage data...</div>'; if (this.loading) return '<div class="flows-loading">Loading mortgage data...</div>';
const totalPool = this.mortgagePositions.reduce((s, m) => s + m.principal, 0); const pool = this.computePoolStats();
const deployed = this.reinvestmentPositions.reduce((s, r) => s + r.deposited, 0);
const currentYieldValue = this.reinvestmentPositions.reduce((s, r) => s + (r.currentValue - r.deposited), 0);
const avgApy = this.reinvestmentPositions.length > 0 const avgApy = this.reinvestmentPositions.length > 0
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 0;
: 0;
const selectedLender = this.selectedLenderId const selectedLender = this.selectedLenderId
? this.mortgagePositions.find(m => m.id === this.selectedLenderId) || null ? this.mortgagePositions.find(m => m.id === this.selectedLenderId) || null
@ -5300,14 +5332,16 @@ class FolkFlowsApp extends HTMLElement {
<span style="background: rgba(16,185,129,0.15); color: #10b981; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">BETA</span> <span style="background: rgba(16,185,129,0.15); color: #10b981; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">BETA</span>
</div> </div>
<!-- Pool Summary Cards --> <!-- Pool Summary clickable to toggle aggregate view -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px;"> <div data-action="toggle-pool" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; cursor: pointer;" title="Click to view aggregate pool breakdown">
${this.renderPoolCard('Total Pool', this.fmtUsd(totalPool), 'Aggregate mortgage capital', '#3b82f6')} ${this.renderPoolCard('Total Borrowed', this.fmtUsd(pool.totalBorrowed), `${this.mortgagePositions.filter(m => m.status === 'active').length} active positions`, '#3b82f6')}
${this.renderPoolCard('Deployed to DeFi', this.fmtUsd(deployed), 'Idle capital reinvested', '#10b981')} ${this.renderPoolCard('Total Repaid', this.fmtUsd(pool.totalRepaid), `${Math.round(pool.totalRepaid / (pool.totalBorrowed || 1) * 100)}% of principal`, '#60a5fa')}
${this.renderPoolCard('Yield Earned', this.fmtUsd(currentYieldValue), 'From reinvestment positions', '#f59e0b')} ${this.renderPoolCard('Reinvested', this.fmtUsd(pool.totalReinvested), `Earning ${avgApy.toFixed(1)}% APY`, '#10b981')}
${this.renderPoolCard('Avg APY', avgApy.toFixed(2) + '%', 'Weighted pool return', '#8b5cf6')} ${this.renderPoolCard('Total Earnings', this.fmtUsd(pool.totalLoanEarnings + pool.totalReinvestEarnings), `Interest + yield`, '#f59e0b')}
</div> </div>
${this.showPoolOverview ? this.renderPoolOverview(pool) : ''}
<!-- Active Mortgages --> <!-- Active Mortgages -->
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid var(--rs-border, #2a2a3e);"> <div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid var(--rs-border, #2a2a3e);">
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Active Mortgages</h3> <h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Active Mortgages</h3>
@ -5336,7 +5370,7 @@ class FolkFlowsApp extends HTMLElement {
<!-- Borrower Options --> <!-- Borrower Options -->
${this.renderBorrowerOptions()} ${this.renderBorrowerOptions()}
<!-- Reinvestment Tracker --> <!-- Reinvestment & Rates -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px;">
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);"> <div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Reinvestment Positions</h3> <h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Reinvestment Positions</h3>
@ -5354,8 +5388,6 @@ class FolkFlowsApp extends HTMLElement {
`).join('')} `).join('')}
${this.reinvestmentPositions.length === 0 ? '<div style="color: var(--rs-text-secondary); font-size: 14px;">No reinvestment positions yet</div>' : ''} ${this.reinvestmentPositions.length === 0 ? '<div style="color: var(--rs-text-secondary); font-size: 14px;">No reinvestment positions yet</div>' : ''}
</div> </div>
<!-- Live Rates -->
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);"> <div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e);">
<h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Live DeFi Rates</h3> <h3 style="margin: 0 0 16px; font-size: 16px; color: var(--rs-text-primary);">Live DeFi Rates</h3>
${this.liveRates.map(r => ` ${this.liveRates.map(r => `
@ -5401,6 +5433,75 @@ class FolkFlowsApp extends HTMLElement {
</div>`; </div>`;
} }
private renderPoolOverview(pool: ReturnType<typeof FolkFlowsApp.prototype.computePoolStats>): string {
const total = pool.totalBorrowed || 1;
const repaidPct = (pool.totalRepaid / total) * 100;
const reinvestedPct = (pool.totalReinvested / total) * 100;
const outstandingPct = Math.max(100 - repaidPct, 0);
const totalEarnings = pool.totalLoanEarnings + pool.totalReinvestEarnings;
// Earnings comparison — show reinvestment dominance
const loanPct = totalEarnings > 0 ? (pool.totalLoanEarnings / totalEarnings) * 100 : 50;
const reinvPct = totalEarnings > 0 ? (pool.totalReinvestEarnings / totalEarnings) * 100 : 50;
return `
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #8b5cf6;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Aggregate Pool Breakdown</h3>
<button data-action="close-pool" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">&times;</button>
</div>
<div style="display: grid; grid-template-columns: 180px 1fr; gap: 24px;">
<!-- Aggregate vessel -->
<div style="display: flex; flex-direction: column; align-items: center;">
<svg viewBox="0 0 120 200" width="140" height="200" style="margin-bottom: 8px;">
<rect x="10" y="10" width="100" height="180" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/>
<!-- Outstanding (empty) top -->
<rect x="12" y="12" width="96" height="${outstandingPct * 1.76}" rx="10" fill="rgba(255,255,255,0.04)"/>
<!-- Repaid (blue) fills from bottom -->
<rect x="12" y="${12 + outstandingPct * 1.76}" width="96" height="${(repaidPct - reinvestedPct) * 1.76}" fill="rgba(59,130,246,0.4)"/>
<!-- Reinvested (green) bottom portion of repaid -->
<rect x="12" y="${12 + (100 - reinvestedPct) * 1.76}" width="96" height="${reinvestedPct * 1.76}" rx="10" fill="rgba(16,185,129,0.5)"/>
${outstandingPct > 12 ? `<text x="60" y="${12 + outstandingPct * 0.88}" text-anchor="middle" fill="var(--rs-text-secondary)" font-size="10">${Math.round(outstandingPct)}% Outstanding</text>` : ''}
${(repaidPct - reinvestedPct) > 12 ? `<text x="60" y="${12 + outstandingPct * 1.76 + (repaidPct - reinvestedPct) * 0.88}" text-anchor="middle" fill="#60a5fa" font-size="10" font-weight="600">${Math.round(repaidPct - reinvestedPct)}% Repaid</text>` : ''}
${reinvestedPct > 8 ? `<text x="60" y="${12 + (100 - reinvestedPct * 0.5) * 1.76}" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">${Math.round(reinvestedPct)}% Reinvested</text>` : ''}
</svg>
<div style="font-size: 11px; color: var(--rs-text-secondary);">${this.fmtUsd(pool.totalBorrowed)} total</div>
</div>
<!-- Aggregate stats + earnings comparison -->
<div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 20px;">
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Outstanding</div>
<div style="font-size: 18px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(pool.totalBorrowed - pool.totalRepaid)}</div>
</div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid</div>
<div style="font-size: 18px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(pool.totalRepaid)}</div>
</div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested</div>
<div style="font-size: 18px; font-weight: 700; color: #10b981;">${this.fmtUsd(pool.totalReinvested)}</div>
</div>
</div>
<!-- Earnings comparison bar -->
<div style="margin-bottom: 8px; font-size: 13px; font-weight: 600; color: var(--rs-text-primary);">Earnings Breakdown: ${this.fmtUsd(totalEarnings)}</div>
<div style="height: 32px; border-radius: 8px; overflow: hidden; display: flex; margin-bottom: 8px;">
<div style="width: ${loanPct}%; background: #f59e0b; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;">${loanPct > 15 ? 'Interest ' + this.fmtUsd(pool.totalLoanEarnings) : ''}</div>
<div style="width: ${reinvPct}%; background: #10b981; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; font-weight: 600;">${reinvPct > 15 ? 'Reinvestment ' + this.fmtUsd(pool.totalReinvestEarnings) : ''}</div>
</div>
<div style="display: flex; gap: 16px; font-size: 12px;">
<span style="display: flex; align-items: center; gap: 4px;"><span style="width: 10px; height: 10px; border-radius: 2px; background: #f59e0b; display: inline-block;"></span> Loan interest: ${this.fmtUsd(pool.totalLoanEarnings)}</span>
<span style="display: flex; align-items: center; gap: 4px;"><span style="width: 10px; height: 10px; border-radius: 2px; background: #10b981; display: inline-block;"></span> Reinvestment yield: ${this.fmtUsd(pool.totalReinvestEarnings)}</span>
</div>
${pool.totalReinvestEarnings > pool.totalLoanEarnings
? `<div style="margin-top: 12px; padding: 10px 14px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 8px; font-size: 12px; color: #10b981;">Reinvesting returns earns ${((pool.totalReinvestEarnings / (pool.totalLoanEarnings || 1) - 1) * 100).toFixed(0)}% more than interest alone. Compounding idle capital is the most profitable strategy.</div>`
: ''}
</div>
</div>
</div>`;
}
private renderPoolCard(title: string, value: string, subtitle: string, color: string): string { private renderPoolCard(title: string, value: string, subtitle: string, color: string): string {
return ` return `
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e); border-top: 3px solid ${color};"> <div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; border: 1px solid var(--rs-border, #2a2a3e); border-top: 3px solid ${color};">
@ -5427,26 +5528,32 @@ class FolkFlowsApp extends HTMLElement {
} }
private renderLenderDetail(m: MortgagePosition): string { private renderLenderDetail(m: MortgagePosition): string {
// Calculate repayment progress
const monthsElapsed = Math.floor((Date.now() - m.startDate) / (86400000 * 30)); const monthsElapsed = Math.floor((Date.now() - m.startDate) / (86400000 * 30));
const totalPaid = m.monthlyPayment * monthsElapsed; const totalPaid = m.monthlyPayment * monthsElapsed;
const interestPaid = totalPaid - (totalPaid * m.principal / (m.monthlyPayment * m.termMonths)); const interestPaid = totalPaid - (totalPaid * m.principal / (m.monthlyPayment * m.termMonths));
const principalRepaid = Math.min(totalPaid - interestPaid, m.principal); const principalRepaid = Math.min(totalPaid - interestPaid, m.principal);
const remaining = Math.max(m.principal - principalRepaid, 0);
// Simulate reinvestment of idle capital from this position const idleCapital = principalRepaid * 0.6;
const idleCapital = principalRepaid * 0.6; // 60% of repaid principal goes to reinvestment
const reinvestApy = this.reinvestmentPositions.length > 0 const reinvestApy = this.reinvestmentPositions.length > 0
? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 4.0;
: 4.0;
const reinvestEarnings = idleCapital * (reinvestApy / 100) * (monthsElapsed / 12); const reinvestEarnings = idleCapital * (reinvestApy / 100) * (monthsElapsed / 12);
const loanEarnings = interestPaid; const loanEarnings = interestPaid;
const totalEarnings = loanEarnings + reinvestEarnings;
// Vessel proportions // Vessel: outstanding at top (empty), repaid-idle in middle (blue), reinvested at bottom (green)
const vesselTotal = m.principal; const total = m.principal || 1;
const repaidPct = (principalRepaid / vesselTotal) * 100; const outstandingPct = Math.max(((m.principal - principalRepaid) / total) * 100, 0);
const reinvestedPct = (idleCapital / vesselTotal) * 100; const repaidIdlePct = ((principalRepaid - idleCapital) / total) * 100;
const emptyPct = Math.max(100 - repaidPct - reinvestedPct, 0); const reinvestedPct = (idleCapital / total) * 100;
// Earnings comparison
const earningsTotal = totalEarnings || 1;
const loanPct = (loanEarnings / earningsTotal) * 100;
const reinvPct = (reinvestEarnings / earningsTotal) * 100;
// Project: what if they reinvest ALL repaid principal vs none
const noReinvestEarnings = loanEarnings;
const fullReinvestEarnings = loanEarnings + (principalRepaid * (reinvestApy / 100) * (monthsElapsed / 12));
return ` return `
<div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #3b82f6;"> <div style="background: var(--rs-surface, #1a1a2e); border-radius: 12px; padding: 20px; margin-bottom: 24px; border: 1px solid #3b82f6;">
@ -5454,48 +5561,60 @@ class FolkFlowsApp extends HTMLElement {
<h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Lender Pool: ${this.esc(m.borrower)}</h3> <h3 style="margin: 0; font-size: 16px; color: var(--rs-text-primary);">Lender Pool: ${this.esc(m.borrower)}</h3>
<button data-action="close-lender" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">&times;</button> <button data-action="close-lender" style="background: none; border: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 18px;">&times;</button>
</div> </div>
<div style="display: grid; grid-template-columns: 160px 1fr; gap: 24px;">
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 24px;"> <!-- Vessel -->
<!-- Vessel visualization -->
<div style="display: flex; flex-direction: column; align-items: center;"> <div style="display: flex; flex-direction: column; align-items: center;">
<svg viewBox="0 0 120 180" width="120" height="180" style="margin-bottom: 8px;"> <svg viewBox="0 0 120 200" width="130" height="200" style="margin-bottom: 8px;">
<!-- Vessel outline --> <rect x="10" y="10" width="100" height="180" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/>
<rect x="10" y="10" width="100" height="160" rx="12" fill="none" stroke="var(--rs-text-secondary)" stroke-width="2" opacity="0.3"/> <!-- Outstanding (empty) -->
<!-- Empty (remaining principal) --> <rect x="12" y="12" width="96" height="${outstandingPct * 1.76}" rx="10" fill="rgba(255,255,255,0.04)"/>
<rect x="12" y="${12 + (repaidPct + reinvestedPct) * 1.56}" width="96" height="${emptyPct * 1.56}" rx="10" fill="rgba(255,255,255,0.05)"/> <!-- Repaid idle (blue) -->
<rect x="12" y="${12 + outstandingPct * 1.76}" width="96" height="${repaidIdlePct * 1.76}" fill="rgba(59,130,246,0.4)"/>
<!-- Reinvested (green) --> <!-- Reinvested (green) -->
<rect x="12" y="${12 + repaidPct * 1.56}" width="96" height="${reinvestedPct * 1.56}" rx="${reinvestedPct > 0 ? 0 : 10}" fill="rgba(16,185,129,0.4)"/> <rect x="12" y="${12 + (outstandingPct + repaidIdlePct) * 1.76}" width="96" height="${reinvestedPct * 1.76}" rx="10" fill="rgba(16,185,129,0.5)"/>
<!-- Repaid (blue) --> ${outstandingPct > 15 ? `<text x="60" y="${12 + outstandingPct * 0.88}" text-anchor="middle" fill="var(--rs-text-secondary)" font-size="10">${Math.round(outstandingPct)}%</text>` : ''}
<rect x="12" y="12" width="96" height="${repaidPct * 1.56}" rx="10" fill="rgba(59,130,246,0.4)"/> ${repaidIdlePct > 12 ? `<text x="60" y="${12 + outstandingPct * 1.76 + repaidIdlePct * 0.88}" text-anchor="middle" fill="#60a5fa" font-size="10" font-weight="600">${Math.round(repaidIdlePct)}% Idle</text>` : ''}
<!-- Labels --> ${reinvestedPct > 10 ? `<text x="60" y="${12 + (outstandingPct + repaidIdlePct) * 1.76 + reinvestedPct * 0.88}" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">${Math.round(reinvestedPct)}% DeFi</text>` : ''}
${repaidPct > 10 ? `<text x="60" y="${12 + repaidPct * 0.78}" text-anchor="middle" fill="#60a5fa" font-size="11" font-weight="600">${Math.round(repaidPct)}% Repaid</text>` : ''}
${reinvestedPct > 10 ? `<text x="60" y="${12 + repaidPct * 1.56 + reinvestedPct * 0.78}" text-anchor="middle" fill="#10b981" font-size="11" font-weight="600">${Math.round(reinvestedPct)}% Reinvested</text>` : ''}
</svg> </svg>
<div style="font-size: 11px; color: var(--rs-text-secondary); text-align: center;">Pool: ${this.fmtUsd(vesselTotal)}</div> <div style="font-size: 11px; color: var(--rs-text-secondary);">${this.fmtUsd(m.principal)} pool</div>
</div> </div>
<!-- Stats breakdown --> <div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;"> <!-- Stats row -->
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;"> <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 16px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid Principal</div> <div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 20px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(principalRepaid)}</div> <div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Repaid</div>
<div style="font-size: 16px; font-weight: 700; color: #60a5fa;">${this.fmtUsd(principalRepaid)}</div>
</div>
<div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested</div>
<div style="font-size: 16px; font-weight: 700; color: #10b981;">${this.fmtUsd(idleCapital)}</div>
</div>
<div style="padding: 10px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase;">Outstanding</div>
<div style="font-size: 16px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(m.principal - principalRepaid)}</div>
</div>
</div> </div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvested Capital</div> <!-- Earnings comparison bar -->
<div style="font-size: 20px; font-weight: 700; color: #10b981;">${this.fmtUsd(idleCapital)}</div> <div style="font-size: 13px; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 6px;">Earnings: ${this.fmtUsd(totalEarnings)} <span style="font-size: 11px; color: var(--rs-text-secondary); font-weight: 400;">${monthsElapsed}mo of ${m.termMonths}mo</span></div>
<div style="height: 28px; border-radius: 6px; overflow: hidden; display: flex; margin-bottom: 6px;">
<div style="width: ${loanPct}%; background: #f59e0b; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: 600;">${loanPct > 20 ? this.fmtUsd(loanEarnings) : ''}</div>
<div style="width: ${reinvPct}%; background: #10b981; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: 600;">${reinvPct > 20 ? this.fmtUsd(reinvestEarnings) : ''}</div>
</div> </div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;"> <div style="display: flex; gap: 14px; font-size: 11px; color: var(--rs-text-secondary); margin-bottom: 12px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Loan Interest Earned</div> <span><span style="width: 8px; height: 8px; border-radius: 2px; background: #f59e0b; display: inline-block; margin-right: 4px;"></span>Interest ${this.fmtUsd(loanEarnings)}</span>
<div style="font-size: 20px; font-weight: 700; color: #f59e0b;">${this.fmtUsd(loanEarnings)}</div> <span><span style="width: 8px; height: 8px; border-radius: 2px; background: #10b981; display: inline-block; margin-right: 4px;"></span>Reinvestment ${this.fmtUsd(reinvestEarnings)}</span>
</div> </div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px;">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Reinvestment Yield</div> <!-- Reinvestment advantage callout -->
<div style="font-size: 20px; font-weight: 700; color: #10b981;">${this.fmtUsd(reinvestEarnings)}</div> <div style="padding: 10px 14px; background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.25); border-radius: 8px; font-size: 12px;">
</div> <div style="color: #10b981; font-weight: 600; margin-bottom: 4px;">Reinvestment advantage</div>
<div style="padding: 12px; background: var(--rs-bg, #0f0f1a); border-radius: 8px; grid-column: span 2;"> <div style="color: var(--rs-text-secondary);">
<div style="font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase;">Total Earnings</div> Without reinvestment: <strong style="color: var(--rs-text-primary);">${this.fmtUsd(noReinvestEarnings)}</strong>
<div style="font-size: 24px; font-weight: 700; color: var(--rs-text-primary);">${this.fmtUsd(loanEarnings + reinvestEarnings)}</div> &rarr; With full reinvestment: <strong style="color: #10b981;">${this.fmtUsd(fullReinvestEarnings)}</strong>
<div style="font-size: 12px; color: var(--rs-text-secondary);">${monthsElapsed} months elapsed of ${m.termMonths}mo term</div> <span style="color: #10b981; font-weight: 600;"> (+${((fullReinvestEarnings / (noReinvestEarnings || 1) - 1) * 100).toFixed(0)}%)</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -5503,7 +5622,7 @@ class FolkFlowsApp extends HTMLElement {
} }
private renderBorrowerOptions(): string { private renderBorrowerOptions(): string {
const termOptions = [60, 120, 180, 240, 300, 360]; // 5yr, 10yr, 15yr, 20yr, 25yr, 30yr const termOptions = [120, 240, 360]; // 10yr, 20yr, 30yr
const budget = this.borrowerMonthlyBudget; const budget = this.borrowerMonthlyBudget;
// Build lender pool: each active lender has available capital (simulated from repaid principal) // Build lender pool: each active lender has available capital (simulated from repaid principal)