// 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 (
);
}
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 (
);
}
// 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 (
);
}
// ---- 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 });
})();