From a1f8702988f92a1bc4ec6d03d9a7500eb57596c8 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Mar 2026 02:14:39 -0700 Subject: [PATCH] feat(rMortgage): aggregate pool viz, earnings comparison, fewer tranches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- modules/rflows/components/folk-flows-app.ts | 241 +++++++++++++++----- 1 file changed, 180 insertions(+), 61 deletions(-) diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts index 8d4795e..58c9688 100644 --- a/modules/rflows/components/folk-flows-app.ts +++ b/modules/rflows/components/folk-flows-app.ts @@ -166,7 +166,7 @@ class FolkFlowsApp extends HTMLElement { // Borrower options state private borrowerMonthlyBudget = 1500; - private borrowerOptionsVisible = false; + private showPoolOverview = false; // Tour engine private _tour!: TourEngine; @@ -5155,6 +5155,18 @@ class FolkFlowsApp extends HTMLElement { 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', () => { const budgetEl = this.shadow.querySelector('[data-borrower="budget"]') as HTMLInputElement; if (budgetEl) this.borrowerMonthlyBudget = Math.max(parseFloat(budgetEl.value) || 100, 100); @@ -5278,15 +5290,35 @@ class FolkFlowsApp extends HTMLElement { 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 { if (this.loading) return '
Loading mortgage data...
'; - const totalPool = this.mortgagePositions.reduce((s, m) => s + m.principal, 0); - 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 pool = this.computePoolStats(); const avgApy = this.reinvestmentPositions.length > 0 - ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length - : 0; + ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 0; const selectedLender = this.selectedLenderId ? this.mortgagePositions.find(m => m.id === this.selectedLenderId) || null @@ -5300,14 +5332,16 @@ class FolkFlowsApp extends HTMLElement { BETA - -
- ${this.renderPoolCard('Total Pool', this.fmtUsd(totalPool), 'Aggregate mortgage capital', '#3b82f6')} - ${this.renderPoolCard('Deployed to DeFi', this.fmtUsd(deployed), 'Idle capital reinvested', '#10b981')} - ${this.renderPoolCard('Yield Earned', this.fmtUsd(currentYieldValue), 'From reinvestment positions', '#f59e0b')} - ${this.renderPoolCard('Avg APY', avgApy.toFixed(2) + '%', 'Weighted pool return', '#8b5cf6')} + +
+ ${this.renderPoolCard('Total Borrowed', this.fmtUsd(pool.totalBorrowed), `${this.mortgagePositions.filter(m => m.status === 'active').length} active positions`, '#3b82f6')} + ${this.renderPoolCard('Total Repaid', this.fmtUsd(pool.totalRepaid), `${Math.round(pool.totalRepaid / (pool.totalBorrowed || 1) * 100)}% of principal`, '#60a5fa')} + ${this.renderPoolCard('Reinvested', this.fmtUsd(pool.totalReinvested), `Earning ${avgApy.toFixed(1)}% APY`, '#10b981')} + ${this.renderPoolCard('Total Earnings', this.fmtUsd(pool.totalLoanEarnings + pool.totalReinvestEarnings), `Interest + yield`, '#f59e0b')}
+ ${this.showPoolOverview ? this.renderPoolOverview(pool) : ''} +

Active Mortgages

@@ -5336,7 +5370,7 @@ class FolkFlowsApp extends HTMLElement { ${this.renderBorrowerOptions()} - +

Reinvestment Positions

@@ -5354,8 +5388,6 @@ class FolkFlowsApp extends HTMLElement { `).join('')} ${this.reinvestmentPositions.length === 0 ? '
No reinvestment positions yet
' : ''}
- -

Live DeFi Rates

${this.liveRates.map(r => ` @@ -5401,6 +5433,75 @@ class FolkFlowsApp extends HTMLElement {
`; } + private renderPoolOverview(pool: ReturnType): 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 ` +
+
+

Aggregate Pool Breakdown

+ +
+
+ +
+ + + + + + + + + ${outstandingPct > 12 ? `${Math.round(outstandingPct)}% Outstanding` : ''} + ${(repaidPct - reinvestedPct) > 12 ? `${Math.round(repaidPct - reinvestedPct)}% Repaid` : ''} + ${reinvestedPct > 8 ? `${Math.round(reinvestedPct)}% Reinvested` : ''} + +
${this.fmtUsd(pool.totalBorrowed)} total
+
+ + +
+
+
+
Outstanding
+
${this.fmtUsd(pool.totalBorrowed - pool.totalRepaid)}
+
+
+
Repaid
+
${this.fmtUsd(pool.totalRepaid)}
+
+
+
Reinvested
+
${this.fmtUsd(pool.totalReinvested)}
+
+
+ + +
Earnings Breakdown: ${this.fmtUsd(totalEarnings)}
+
+
${loanPct > 15 ? 'Interest ' + this.fmtUsd(pool.totalLoanEarnings) : ''}
+
${reinvPct > 15 ? 'Reinvestment ' + this.fmtUsd(pool.totalReinvestEarnings) : ''}
+
+
+ Loan interest: ${this.fmtUsd(pool.totalLoanEarnings)} + Reinvestment yield: ${this.fmtUsd(pool.totalReinvestEarnings)} +
+ ${pool.totalReinvestEarnings > pool.totalLoanEarnings + ? `
Reinvesting returns earns ${((pool.totalReinvestEarnings / (pool.totalLoanEarnings || 1) - 1) * 100).toFixed(0)}% more than interest alone. Compounding idle capital is the most profitable strategy.
` + : ''} +
+
+
`; + } + private renderPoolCard(title: string, value: string, subtitle: string, color: string): string { return `
@@ -5427,26 +5528,32 @@ class FolkFlowsApp extends HTMLElement { } private renderLenderDetail(m: MortgagePosition): string { - // Calculate repayment progress const monthsElapsed = Math.floor((Date.now() - m.startDate) / (86400000 * 30)); const totalPaid = m.monthlyPayment * monthsElapsed; const interestPaid = totalPaid - (totalPaid * m.principal / (m.monthlyPayment * m.termMonths)); 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; // 60% of repaid principal goes to reinvestment + const idleCapital = principalRepaid * 0.6; const reinvestApy = this.reinvestmentPositions.length > 0 - ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length - : 4.0; + ? this.reinvestmentPositions.reduce((s, r) => s + r.apy, 0) / this.reinvestmentPositions.length : 4.0; const reinvestEarnings = idleCapital * (reinvestApy / 100) * (monthsElapsed / 12); const loanEarnings = interestPaid; + const totalEarnings = loanEarnings + reinvestEarnings; - // Vessel proportions - const vesselTotal = m.principal; - const repaidPct = (principalRepaid / vesselTotal) * 100; - const reinvestedPct = (idleCapital / vesselTotal) * 100; - const emptyPct = Math.max(100 - repaidPct - reinvestedPct, 0); + // Vessel: outstanding at top (empty), repaid-idle in middle (blue), reinvested at bottom (green) + const total = m.principal || 1; + const outstandingPct = Math.max(((m.principal - principalRepaid) / total) * 100, 0); + const repaidIdlePct = ((principalRepaid - idleCapital) / total) * 100; + 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 `
@@ -5454,48 +5561,60 @@ class FolkFlowsApp extends HTMLElement {

Lender Pool: ${this.esc(m.borrower)}

- -
- +
+
- - - - - + + + + + + - - - - - ${repaidPct > 10 ? `${Math.round(repaidPct)}% Repaid` : ''} - ${reinvestedPct > 10 ? `${Math.round(reinvestedPct)}% Reinvested` : ''} + + ${outstandingPct > 15 ? `${Math.round(outstandingPct)}%` : ''} + ${repaidIdlePct > 12 ? `${Math.round(repaidIdlePct)}% Idle` : ''} + ${reinvestedPct > 10 ? `${Math.round(reinvestedPct)}% DeFi` : ''} -
Pool: ${this.fmtUsd(vesselTotal)}
+
${this.fmtUsd(m.principal)} pool
- -
-
-
Repaid Principal
-
${this.fmtUsd(principalRepaid)}
+
+ +
+
+
Repaid
+
${this.fmtUsd(principalRepaid)}
+
+
+
Reinvested
+
${this.fmtUsd(idleCapital)}
+
+
+
Outstanding
+
${this.fmtUsd(m.principal - principalRepaid)}
+
-
-
Reinvested Capital
-
${this.fmtUsd(idleCapital)}
+ + +
Earnings: ${this.fmtUsd(totalEarnings)} ${monthsElapsed}mo of ${m.termMonths}mo
+
+
${loanPct > 20 ? this.fmtUsd(loanEarnings) : ''}
+
${reinvPct > 20 ? this.fmtUsd(reinvestEarnings) : ''}
-
-
Loan Interest Earned
-
${this.fmtUsd(loanEarnings)}
+
+ Interest ${this.fmtUsd(loanEarnings)} + Reinvestment ${this.fmtUsd(reinvestEarnings)}
-
-
Reinvestment Yield
-
${this.fmtUsd(reinvestEarnings)}
-
-
-
Total Earnings
-
${this.fmtUsd(loanEarnings + reinvestEarnings)}
-
${monthsElapsed} months elapsed of ${m.termMonths}mo term
+ + +
+
Reinvestment advantage
+
+ Without reinvestment: ${this.fmtUsd(noReinvestEarnings)} + → With full reinvestment: ${this.fmtUsd(fullReinvestEarnings)} + (+${((fullReinvestEarnings / (noReinvestEarnings || 1) - 1) * 100).toFixed(0)}%) +
@@ -5503,7 +5622,7 @@ class FolkFlowsApp extends HTMLElement { } 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; // Build lender pool: each active lender has available capital (simulated from repaid principal)