// DashboardScreen — overview: KPI cards + charts // All data derived from window.AKEFIN_DATA where possible. (function () { // ---- Synthesised series for charts (deterministic; based on the seed) -- // 30-day cashflow (income/expense in KRW equivalent). Negative = spend. const CASHFLOW = (() => { // Pseudo-random but stable. Mix of small-spend days, larger ones, and one big income. const seedSpend = [42, 86, 31, 124, 58, 73, 18, 96, 240, 64, 47, 39, 28, 71, 110, 56, 33, 88, 49, 62, 38, 145, 72, 80, 41, 53, 117, 64, 91, 38]; // ×1000 KRW const seedIncomeIdx = { 4: 2400, 12: 1240, 19: 1180, 27: 2400 }; // ×1000 KRW positive on these indexes return seedSpend.map((s, i) => ({ day: i + 1, spend: -s * 1000, income: (seedIncomeIdx[i] || 0) * 1000, })); })(); // Spend by category (current month, KRW) const CATEGORIES = [ { name: "Groceries", amount: 481600, entity: "personal" }, { name: "Food · Delivery", amount: 224000, entity: "personal" }, { name: "Housing · Maint", amount: 286000, entity: "personal" }, { name: "Food · Coffee", amount: 84600, entity: "personal" }, { name: "Convenience", amount: 62400, entity: "personal" }, { name: "Software", amount: 96000, entity: "tfox" }, { name: "Infrastructure", amount: 42800, entity: "tfox" }, { name: "Transit", amount: 38000, entity: "personal" }, { name: "Clothing", amount: 28400, entity: "personal" }, ]; // Confidence distribution (count of transactions currently staged across history) const CONFIDENCE_DIST = [ { tier: "rules", count: 287, label: "Rules" }, { tier: "high", count: 124, label: "LLM ≥ 0.85"}, { tier: "mid", count: 46, label: "0.70–0.85" }, { tier: "low", count: 18, label: "< 0.70" }, ]; // Sparkline series for KPI cards (last 14 points) const SPARK = { staged: [12, 18, 21, 16, 24, 29, 22, 25, 31, 27, 23, 28, 19, 23], spend: [128, 142, 119, 156, 138, 161, 149, 152, 174, 168, 145, 159, 138, 142], // ×1000 KRW income: [0, 0, 2400, 0, 0, 1240, 0, 0, 0, 2400, 0, 1180, 0, 0], conf: [0.81, 0.83, 0.79, 0.84, 0.86, 0.85, 0.87, 0.88, 0.86, 0.89, 0.87, 0.88, 0.90, 0.89], }; // ---- Pure SVG components ---------------------------------------------- function Sparkline({ data, color = "var(--fg-muted)", height = 28, width = 96, fill = false }) { if (!data || data.length === 0) return null; const min = Math.min(...data, 0); const max = Math.max(...data, 0.0001); const span = max - min || 1; const stepX = width / (data.length - 1); const points = data.map((v, i) => [i * stepX, height - ((v - min) / span) * (height - 2) - 1]); const d = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" "); const dFill = `${d} L${width},${height} L0,${height} Z`; return ( {fill && } {/* end dot */} ); } function StatCard({ eyebrow, value, unit, delta, deltaDir, spark, sparkColor, footnote }) { return (
{eyebrow}
{value} {unit && {unit}}
{spark && }
{delta && ( {deltaDir === "up" ? "↑" : deltaDir === "down" ? "↓" : "·"} {delta} )} {footnote && {footnote}}
); } // Cashflow bar chart — bars per day, income up (green) / spend down (ink), 30 days function CashflowChart({ data }) { const W = 720, H = 220, PAD_L = 44, PAD_R = 12, PAD_T = 16, PAD_B = 28; const plotW = W - PAD_L - PAD_R; const plotH = H - PAD_T - PAD_B; const maxPos = Math.max(...data.map(d => d.income), 1); const maxNeg = Math.max(...data.map(d => -d.spend), 1); const maxAbs = Math.max(maxPos, maxNeg); const zeroY = PAD_T + plotH / 2; const half = plotH / 2; const colW = plotW / data.length; const barW = Math.max(3, colW * 0.55); const yScale = (v) => zeroY - (v / maxAbs) * half; // Y-axis ticks: 0, ±50%, ±100% const ticks = [-1, -0.5, 0, 0.5, 1]; const fmtTick = (frac) => { if (frac === 0) return "0"; const v = Math.round(frac * maxAbs / 1000); return `${frac > 0 ? "+" : "−"}${Math.abs(v).toLocaleString()}k`; }; return ( {/* Gridlines */} {ticks.map(t => ( {fmtTick(t)} ))} {/* Bars */} {data.map((d, i) => { const cx = PAD_L + i * colW + colW / 2; const sH = (Math.abs(d.spend) / maxAbs) * half; const iH = (d.income / maxAbs) * half; return ( {d.spend !== 0 && ( )} {d.income !== 0 && ( )} ); })} {/* X axis labels (every 5 days) */} {data.map((d, i) => { if (i % 5 !== 0 && i !== data.length - 1) return null; const cx = PAD_L + i * colW + colW / 2; return ( Feb {6 + d.day} ); })} ); } // Donut for confidence distribution function Donut({ data, size = 168, thickness = 22 }) { const total = data.reduce((a, b) => a + b.count, 0); const r = (size - thickness) / 2; const cx = size / 2, cy = size / 2; const colors = { rules: "var(--conf-rules)", high: "var(--conf-high)", mid: "var(--conf-mid)", low: "var(--conf-low)", }; let cum = 0; const segs = data.map(d => { const start = cum / total; cum += d.count; const end = cum / total; return { ...d, start, end }; }); const arc = (s, e) => { const a0 = s * Math.PI * 2 - Math.PI / 2; const a1 = e * Math.PI * 2 - Math.PI / 2; const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0); const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1); const large = e - s > 0.5 ? 1 : 0; return `M${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1}`; }; return ( {/* track */} {segs.map((s, i) => ( ))} {total} CATEGORIZED · MAR ); } // ---- Sub-cards -------------------------------------------------------- function CashflowCard() { const totalIn = CASHFLOW.reduce((a, b) => a + b.income, 0); const totalOut = CASHFLOW.reduce((a, b) => a + Math.abs(b.spend), 0); const net = totalIn - totalOut; return (
CASHFLOW · LAST 30 DAYS
Net = 0 ? "pos" : "neg"}> {net >= 0 ? "+ " : "− "}{Math.abs(net).toLocaleString()} KRW
INCOME SPEND
INCOME + {totalIn.toLocaleString()} KRW
SPEND − {totalOut.toLocaleString()} KRW
AVG. CONF. 0.87
RUNS 12
); } function ConfidenceCard() { return (
CATEGORIZATION · BY TIER
Confidence mix
{CONFIDENCE_DIST.map(d => { const total = CONFIDENCE_DIST.reduce((a, b) => a + b.count, 0); const pct = ((d.count / total) * 100).toFixed(0); return (
{pct}% {d.label} {d.count}
); })}
AUTO-POSTED 86%
NEED REVIEW 64
); } function CategoryCard() { const max = Math.max(...CATEGORIES.map(c => c.amount)); const total = CATEGORIES.reduce((a, b) => a + b.amount, 0); const entityColor = { personal: "var(--entity-personal)", tfox: "var(--entity-9tfox)", finacode: "var(--entity-finacode)", }; return (
SPEND · BY CATEGORY · MAR 2026
− {total.toLocaleString()} KRW
{CATEGORIES.map((c, i) => { const w = (c.amount / max) * 100; const pct = ((c.amount / total) * 100).toFixed(1); return (
{c.name}
− {c.amount.toLocaleString()} KRW
{pct}%
); })}
); } function EntitySplitCard() { const entities = [ { id: "personal", label: "Personal", income: 0, spend: 1205000, txCount: 38, color: "var(--entity-personal)" }, { id: "tfox", label: "9TFox", income: 4800000, spend: 138800, txCount: 17, color: "var(--entity-9tfox)" }, { id: "finacode", label: "Finacode", income: 1810000, spend: 0, txCount: 3, color: "var(--entity-finacode)" }, ]; const total = entities.reduce((a, b) => a + b.income + b.spend, 0); return (
ENTITY · ACTIVITY MIX
3 entities · 58 transactions
{/* Stacked bar across entities */}
{entities.map(e => { const w = ((e.income + e.spend) / total) * 100; return (
{e.label}
); })}
INCOME SPEND NET TX
{entities.map(e => (
{e.label} {e.income ? `+ ${e.income.toLocaleString()}` : "—"} KRW {e.spend ? `− ${e.spend.toLocaleString()}` : "—"} KRW = 0 ? "pos" : ""}`}> {e.income - e.spend >= 0 ? "+ " : "− "} {Math.abs(e.income - e.spend).toLocaleString()} KRW {e.txCount}
))}
); } function PipelineStatusCard() { const lines = [ { t: "09:14", src: "Toss · 신한 입출금", rows: 247, msg: "153 auto · 44 high · 35 review · 15 failed", status: "warn" }, { t: "08:02", src: "Kakao Bank · 9TFox", rows: 62, msg: "58 auto · 3 high · 1 review", status: "ok" }, { t: "07:48", src: "Wise · 9TFox", rows: 14, msg: "12 auto · 2 high", status: "ok" }, { t: "06:01", src: "Garanti BBVA", rows: 89, msg: "61 auto · 14 high · 11 review · 3 failed", status: "warn" }, ]; return (
PIPELINE · LAST 24 HOURS
4 imports · 412 rows · 50 staged
{lines.map((l, i) => (
{l.t} {l.src} {l.rows} rows {l.msg}
))}
); } function DashboardScreen({ entity, onJump }) { return (
OVERVIEW · MAR 2026 · {entity === "all" ? "ALL ENTITIES" : entity.toUpperCase()}

Ledger status

8 days into the period · 58 transactions · 64 staged for review · ledger clean on main
{/* KPI row */}
{/* Main 2-col grid */}
{/* Secondary 2-col grid */}
{/* Pipeline */}
); } Object.assign(window, { DashboardScreen }); })();