diff --git a/ui_kits/web/AccountsScreen.jsx b/ui_kits/web/AccountsScreen.jsx new file mode 100644 index 0000000..b8c34ae --- /dev/null +++ b/ui_kits/web/AccountsScreen.jsx @@ -0,0 +1,244 @@ +// AccountsScreen — connected data sources + chart of accounts + +(function () { + const fmt = (n, ccy) => { + if (n == null || isNaN(n)) return "—"; + const abs = Math.abs(n); + const isInt = ccy === "KRW" || ccy === "JPY" || Math.abs(abs - Math.round(abs)) < 0.005; + return (n < 0 ? "− " : "") + abs.toLocaleString(undefined, { + minimumFractionDigits: isInt ? 0 : 2, + maximumFractionDigits: isInt ? 0 : 2, + }); + }; + + const entityShort = { personal: "P/", tfox: "9T/", finacode: "FC/" }; + const entityClass = { personal: "personal", tfox: "tfox", finacode: "finacode" }; + + // ----- Status pill -------------------------------------------------- + function StatusPill({ status }) { + const map = { + ok: { lbl: "OK", cls: "ok" }, + warn: { lbl: "WARN", cls: "warn" }, + fail: { lbl: "FAILED", cls: "fail" }, + idle: { lbl: "IDLE", cls: "idle" }, + }; + const m = map[status] || map.idle; + return {m.lbl}; + } + + function MethodTag({ method }) { + const map = { + "api": "API", + "csv-poll": "CSV · POLL", + "csv-drop": "CSV · DROP", + "manual": "MANUAL", + }; + return {map[method] || method.toUpperCase()}; + } + + // ----- Connected source card --------------------------------------- + function SourceCard({ src, selected, onClick }) { + return ( + + ); + } + + // ----- Chart of accounts tree -------------------------------------- + // accountsByEntity rows have full path; we render them grouped per entity, indented by depth. + function ChartOfAccounts({ entity }) { + const data = window.AKEFIN_DATA.accountsByEntity; + const sets = entity === "all" + ? [["Personal", data.personal, "personal"], ["9TFox", data.tfox, "tfox"], ["Finacode", data.finacode, "finacode"]] + : [[entity === "tfox" ? "9TFox" : entity[0].toUpperCase() + entity.slice(1), data[entity], entity]]; + + return ( +
+ {sets.map(([title, rows, cls]) => ( +
+
+ {entityShort[cls]} + {title} + {rows.length} accounts +
+
+ {rows.map((r) => { + const depth = (r.path.match(/:/g) || []).length; + const leaf = r.path.split(":").pop(); + return ( +
+ + {r.kind === "branch" ? "▸" : "·"} + {leaf} + + {r.path} + + {r.balance >= 0 ? "+ " : "− "}{fmt(Math.abs(r.balance), r.ccy)} + {r.ccy} + +
+ ); + })} +
+
+ ))} +
+ ); + } + + // ----- Detail panel for a selected source -------------------------- + function SourceDetail({ src }) { + if (!src) { + return ( +
+
SELECT A SOURCE
+
Click a connected account to inspect sync settings, recent imports, and rotation.
+
+ ); + } + return ( +
+
+
+
{src.kind.toUpperCase()} · {src.country}
+

{src.name}

+
{src.maskedNo}
+
+ +
+ +
+
+
BALANCE
+
{fmt(src.balance, src.ccy)} {src.ccy}
+
+
+
TX · 30D
+
{src.txCount30d}
+
+
+
LAST SYNC
+
{src.lastSync.split(" ")[1]}
+
{src.lastSync.split(" ")[0]}
+
+
+
NEXT
+
{src.nextRun}
+
+
+ +
+
SYNC
+
METHOD
+
ENTITY{entityShort[src.entity]} {src.entity === "tfox" ? "9TFox" : src.entity[0].toUpperCase() + src.entity.slice(1)}
+
SCHEDULE{src.method === "api" ? "every 4 hours" : src.method === "csv-poll" ? "every 12 hours" : "on demand"}
+
CCY{src.ccy}
+
+ +
+
RECENT IMPORTS
+
+
+ {src.lastSync.split(" ")[1]} + Last successful sync · {src.txCount30d} rows in 30d +
+ {src.status === "fail" && ( +
+ {src.lastSync.split(" ")[1]} + CSV parse failed · encoding mismatch +
+ )} + {src.status === "warn" && ( +
+ {src.lastSync.split(" ")[1]} + 11 rows queued for review · 3 failed +
+ )} +
+
+ +
+ + + +
+
+ ); + } + + // ----- Top-level screen -------------------------------------------- + function AccountsScreen({ entity }) { + const [tab, setTab] = React.useState("sources"); // sources | coa + const sources = window.AKEFIN_DATA.connectedAccounts; + const visible = sources.filter(s => entity === "all" || s.entity === entity); + const [selectedId, setSelectedId] = React.useState(visible[0]?.id); + React.useEffect(() => { + if (visible.length && !visible.find(s => s.id === selectedId)) { + setSelectedId(visible[0].id); + } + }, [entity]); + const selected = visible.find(s => s.id === selectedId); + + // Totals (in KRW equivalent) + const totalKrw = visible.reduce((a, s) => a + window.AKEFIN_DATA.convert(s.balance, s.ccy, "KRW"), 0); + const okCount = visible.filter(s => s.status === "ok").length; + const warnCount = visible.filter(s => s.status === "warn" || s.status === "fail").length; + + return ( +
+
+
+
ACCOUNTS · {entity === "all" ? "ALL ENTITIES" : entity.toUpperCase()}
+

Connected sources

+
{visible.length} sources · {okCount} healthy · {warnCount} need attention · combined ≈ {fmt(totalKrw, "KRW")} KRW
+
+
+ + +
+
+ +
+ + +
+ + {tab === "sources" && ( +
+
+ {visible.map(s => ( + setSelectedId(s.id)} /> + ))} +
+ +
+ )} + + {tab === "coa" && } +
+ ); + } + + Object.assign(window, { AccountsScreen }); +})(); diff --git a/ui_kits/web/App.jsx b/ui_kits/web/App.jsx index d1ab5e8..a16010a 100644 --- a/ui_kits/web/App.jsx +++ b/ui_kits/web/App.jsx @@ -91,6 +91,8 @@ function App() { case "go-rules": setScreen("rules"); break; case "go-ledger": setScreen("ledger"); break; case "go-import": setScreen("import"); break; + case "go-accounts": setScreen("accounts"); break; + case "go-history": setScreen("history"); break; case "toggle-theme": setTheme(t => t === "light" ? "dark" : "light"); break; default: showToast(`${cmd.label}`); } @@ -143,6 +145,8 @@ function App() { {screen === "rules" && } {screen === "ledger" && } {screen === "import" && setCsvOpen(true)} />} + {screen === "accounts" && } + {screen === "history" && } setPaletteOpen(false)} onRun={runCommand} /> diff --git a/ui_kits/web/Chrome.jsx b/ui_kits/web/Chrome.jsx index 04d0d23..eb2f220 100644 --- a/ui_kits/web/Chrome.jsx +++ b/ui_kits/web/Chrome.jsx @@ -85,8 +85,16 @@ function Sidebar({ screen, setScreen, counts }) { ))}
SETTINGS
- - + +
diff --git a/ui_kits/web/HistoryScreen.jsx b/ui_kits/web/HistoryScreen.jsx new file mode 100644 index 0000000..684af88 --- /dev/null +++ b/ui_kits/web/HistoryScreen.jsx @@ -0,0 +1,245 @@ +// HistoryScreen — append-only audit log + +(function () { + const KIND_META = { + import: { label: "IMPORT", glyph: "↓", color: "var(--fg-strong)" }, + poll: { label: "POLL", glyph: "↻", color: "var(--fg-muted)" }, + approve: { label: "APPROVE", glyph: "✓", color: "var(--conf-rules)" }, + override: { label: "OVERRIDE", glyph: "✎", color: "var(--conf-high)" }, + skip: { label: "SKIP", glyph: "↩", color: "var(--conf-mid)" }, + rule: { label: "RULE", glyph: "§", color: "var(--conf-high)" }, + commit: { label: "COMMIT", glyph: "●", color: "var(--fg-strong)" }, + fx: { label: "FX", glyph: "◇", color: "var(--fg-muted)" }, + categorize: { label: "AGENT", glyph: "△", color: "var(--conf-high)" }, + error: { label: "ERROR", glyph: "✕", color: "var(--conf-low)" }, + }; + const ACTOR_LABEL = { you: "you", system: "system", agent: "agent" }; + const entityShort = { all: "ALL", personal: "P/", tfox: "9T/", finacode: "FC/" }; + const entityClass = { personal: "personal", tfox: "tfox", finacode: "finacode" }; + + function HistoryFilters({ activeKinds, toggleKind, actor, setActor, range, setRange }) { + const kinds = Object.keys(KIND_META); + return ( +
+
+ KIND +
+ + {kinds.map(k => ( + + ))} +
+
+
+ ACTOR +
+ {["all", "you", "system", "agent"].map(a => ( + + ))} +
+ RANGE +
+ {[ + { id: "24h", label: "24H" }, + { id: "7d", label: "7D" }, + { id: "30d", label: "30D" }, + { id: "all", label: "ALL" }, + ].map(r => ( + + ))} +
+
+
+ ); + } + + // Group log entries by date + function groupByDay(rows) { + const groups = new Map(); + rows.forEach(r => { + const day = r.at.split(" ")[0]; + if (!groups.has(day)) groups.set(day, []); + groups.get(day).push(r); + }); + return [...groups.entries()]; + } + + function dayLabel(iso) { + const d = new Date(iso + "T00:00:00"); + const today = new Date("2026-03-08T00:00:00"); + const diff = Math.round((today - d) / (24 * 3600 * 1000)); + if (diff === 0) return "TODAY"; + if (diff === 1) return "YESTERDAY"; + const wd = d.toLocaleDateString("en-US", { weekday: "short" }).toUpperCase(); + const md = d.toLocaleDateString("en-US", { month: "short", day: "numeric" }).toUpperCase(); + return `${wd} · ${md}`; + } + + function HistoryRow({ row }) { + const meta = KIND_META[row.kind]; + return ( +
+
+ {row.at.split(" ")[1]} +
+
+ {meta.glyph} + {meta.label} +
+
+ {ACTOR_LABEL[row.actor]} +
+
+ {entityShort[row.entity]} +
+
+
{row.summary}
+
{row.detail}
+
+
{row.ref}
+
+ ); + } + + function HistoryScreen({ entity }) { + const all = window.AKEFIN_DATA.history; + const [activeKinds, setActiveKinds] = React.useState(new Set()); + const [actor, setActor] = React.useState("all"); + const [range, setRange] = React.useState("7d"); + + const toggleKind = (k) => { + setActiveKinds(prev => { + const next = new Set(prev); + if (next.has(k)) next.delete(k); else next.add(k); + return next; + }); + }; + + // Range filter (anchored to 2026-03-08 — the demo's "today") + const TODAY = new Date("2026-03-08T23:59:59"); + const RANGE_DAYS = { "24h": 1, "7d": 7, "30d": 30, "all": 99999 }; + const cutoff = new Date(TODAY.getTime() - RANGE_DAYS[range] * 24 * 3600 * 1000); + + const rows = all.filter(r => { + if (entity !== "all" && r.entity !== entity && r.entity !== "all") return false; + if (activeKinds.size > 0 && !activeKinds.has(r.kind)) return false; + if (actor !== "all" && r.actor !== actor) return false; + const d = new Date(r.at.replace(" ", "T")); + if (d < cutoff) return false; + return true; + }); + + // Aggregates for header strip + const counts = rows.reduce((acc, r) => { + acc.total++; + acc.byActor[r.actor] = (acc.byActor[r.actor] || 0) + 1; + acc.byKind[r.kind] = (acc.byKind[r.kind] || 0) + 1; + return acc; + }, { total: 0, byActor: {}, byKind: {} }); + + const grouped = groupByDay(rows); + + return ( +
+
+
+
HISTORY · {entity === "all" ? "ALL ENTITIES" : entity.toUpperCase()}
+

Audit log

+
+ {counts.total} events · append-only · backed by git on main +
+
+
+ + +
+
+ + {/* Summary strip */} +
+
+
EVENTS
+
{counts.total}
+
+
+
BY YOU
+
{counts.byActor.you || 0}
+
+
+
BY SYSTEM
+
{counts.byActor.system || 0}
+
+
+
BY AGENT
+
{counts.byActor.agent || 0}
+
+
+
COMMITS
+
{counts.byKind.commit || 0}
+
+
+
ERRORS
+
0 ? "neg" : ""}`}>{counts.byKind.error || 0}
+
+
+ + + +
+
+
TIME
+
KIND
+
ACTOR
+
ENT
+
EVENT
+
REF
+
+ + {grouped.length === 0 && ( +
+
NO EVENTS MATCH
+
Try widening the range or clearing filters.
+
+ )} + + {grouped.map(([day, dayRows]) => ( +
+
+ {dayLabel(day)} + {day} + {dayRows.length} events + +
+
+ {dayRows.map(r => )} +
+
+ ))} +
+
+ ); + } + + Object.assign(window, { HistoryScreen }); +})(); diff --git a/ui_kits/web/dashboard.css b/ui_kits/web/dashboard.css index a501fa1..9495aa0 100644 --- a/ui_kits/web/dashboard.css +++ b/ui_kits/web/dashboard.css @@ -1969,3 +1969,601 @@ button { font-family: inherit; cursor: pointer; } .dash-log-rows { color: var(--fg-on-ink-muted); font-variant-numeric: tabular-nums; } .dash-log-msg { color: var(--fg-on-ink-muted); } +/* ========================================================================= + ACCOUNTS SCREEN + ========================================================================= */ +.accounts-screen { + padding: 24px; + overflow-y: auto; + height: 100%; +} + +.acct-tabs { + display: flex; gap: 0; + border-bottom: 1px solid var(--rule); + margin-bottom: 20px; + margin-top: 4px; +} +.acct-tab { + background: transparent; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 16px 10px 0; + margin-right: 24px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.16em; + color: var(--fg-muted); + display: inline-flex; align-items: center; gap: 8px; +} +.acct-tab.active { color: var(--fg-strong); border-bottom-color: var(--fg-strong); } +.acct-tab:hover { color: var(--fg-strong); } +.acct-tab-count { + background: var(--rule); + color: var(--fg-strong); + font-size: 10px; + letter-spacing: 0.04em; + padding: 1px 6px; + border-radius: 999px; +} + +/* ---- Layout: cards grid + detail ----------------------------------- */ +.acct-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 16px; + align-items: start; +} +.acct-src-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +/* ---- Source card --------------------------------------------------- */ +.acct-src-card { + text-align: left; + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + padding: 14px 16px; + display: flex; flex-direction: column; + gap: 8px; + position: relative; + transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); +} +.acct-src-card:hover { + border-color: var(--fg-muted); +} +.acct-src-card.selected { + border-color: var(--fg-strong); + background: color-mix(in srgb, var(--paper) 96%, var(--fg-strong) 4%); +} +.acct-src-card.selected::before { + content: ""; + position: absolute; + left: -1px; top: -1px; bottom: -1px; + width: 3px; + background: var(--fg-strong); + border-radius: 1px 0 0 1px; +} +.acct-src-head { + display: flex; align-items: center; justify-content: space-between; +} +.acct-src-name { + font-family: var(--font-sans-kr); + font-size: 14.5px; + font-weight: 500; + color: var(--fg-strong); +} +.acct-src-meta { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.04em; + color: var(--fg-subtle); + display: flex; gap: 6px; +} +.acct-src-balance { + font-family: var(--font-mono); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--fg-strong); + letter-spacing: -0.01em; + margin-top: 2px; +} +.acct-src-balance .ccy { + color: var(--fg-subtle); + font-size: 12px; + font-weight: 400; +} +.acct-src-foot { + display: flex; align-items: center; justify-content: space-between; + padding-top: 8px; + border-top: 1px dashed var(--rule); + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.14em; + color: var(--fg-subtle); +} +.acct-src-sync { font-variant-numeric: tabular-nums; } +.acct-method { + display: inline-block; + padding: 2px 6px; + border: 1px solid var(--rule); + border-radius: 2px; + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.16em; + color: var(--fg-muted); +} + +/* ---- Status pill --------------------------------------------------- */ +.acct-pill { + display: inline-flex; align-items: center; gap: 5px; + height: 18px; + padding: 0 7px; + border-radius: 2px; + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 500; + letter-spacing: 0.16em; +} +.acct-pill-dot { + width: 5px; height: 5px; + border-radius: 50%; +} +.acct-pill-ok { background: color-mix(in srgb, var(--conf-rules) 14%, transparent); color: var(--conf-rules); } +.acct-pill-ok .acct-pill-dot { background: var(--conf-rules); } +.acct-pill-warn { background: color-mix(in srgb, var(--conf-mid) 16%, transparent); color: var(--conf-mid); } +.acct-pill-warn .acct-pill-dot { background: var(--conf-mid); } +.acct-pill-fail { background: color-mix(in srgb, var(--conf-low) 14%, transparent); color: var(--conf-low); } +.acct-pill-fail .acct-pill-dot { background: var(--conf-low); } +.acct-pill-idle { background: var(--rule); color: var(--fg-muted); } +.acct-pill-idle .acct-pill-dot { background: var(--fg-muted); } + +/* ---- Detail panel -------------------------------------------------- */ +.acct-detail { + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + padding: 18px 20px; + display: flex; flex-direction: column; + gap: 18px; + position: sticky; + top: 0; +} +.acct-detail-empty { + align-items: center; justify-content: center; + text-align: center; + min-height: 240px; + color: var(--fg-subtle); +} +.acct-detail-empty-lbl { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg-muted); + margin-bottom: 8px; +} +.acct-detail-empty-sub { + font-size: 12.5px; + max-width: 240px; + line-height: 1.5; +} +.acct-detail-head { + display: flex; align-items: flex-start; justify-content: space-between; + gap: 12px; +} +.acct-detail-eyebrow { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.16em; + color: var(--fg-subtle); + margin-bottom: 4px; +} +.acct-detail-title { + margin: 0; + font-family: var(--font-sans-kr); + font-size: 18px; + font-weight: 500; + color: var(--fg-strong); +} +.acct-detail-sub { + font-size: 11.5px; + color: var(--fg-muted); + letter-spacing: 0.04em; + margin-top: 4px; +} +.acct-detail-stat-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0; + border: 1px solid var(--rule); + border-radius: var(--r-sm); +} +.acct-detail-stat { + padding: 10px 12px; + border-right: 1px solid var(--rule); + border-bottom: 1px solid var(--rule); +} +.acct-detail-stat:nth-child(2n) { border-right: none; } +.acct-detail-stat:nth-last-child(-n+2) { border-bottom: none; } +.acct-detail-stat .lbl { + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.16em; + color: var(--fg-subtle); + margin-bottom: 4px; +} +.acct-detail-stat .val { + font-family: var(--font-mono); + font-size: 16px; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--fg-strong); +} +.acct-detail-stat .val .ccy { color: var(--fg-subtle); font-weight: 400; font-size: 11px; } +.acct-detail-stat .sub { + font-family: var(--font-mono); + font-size: 10px; + color: var(--fg-subtle); + letter-spacing: 0.04em; + margin-top: 2px; +} +.acct-detail-section { display: flex; flex-direction: column; gap: 0; } +.acct-detail-section-head { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.18em; + color: var(--fg-subtle); + margin-bottom: 8px; +} +.acct-kv-row { + display: flex; justify-content: space-between; align-items: center; + padding: 7px 0; + border-bottom: 1px dashed var(--rule); + font-size: 12.5px; +} +.acct-kv-row:last-child { border-bottom: none; } +.acct-kv-row .k { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.14em; + color: var(--fg-subtle); +} +.acct-kv-row .v { color: var(--fg); display: inline-flex; align-items: center; gap: 4px; } +.acct-mini-log { + background: var(--surface); + border: 1px solid var(--rule-ink); + border-radius: var(--r-sm); + padding: 4px 0; +} +.acct-mini-row { + display: grid; + grid-template-columns: 60px 1fr; + gap: 10px; + padding: 7px 12px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--fg-on-ink); + border-bottom: 1px dashed var(--rule-ink); +} +.acct-mini-row:last-child { border-bottom: none; } +.acct-mini-row .t { color: var(--fg-on-ink-muted); } +.acct-mini-row.fail .msg { color: var(--conf-low); } +.acct-mini-row.warn .msg { color: var(--conf-mid); } +.acct-detail-actions { + display: flex; flex-wrap: wrap; gap: 6px; + margin-top: 4px; +} +.acct-detail-actions .btn { flex: 1; min-width: 110px; } +.acct-detail-actions .btn.ghost { color: var(--conf-low); border-color: color-mix(in srgb, var(--conf-low) 30%, var(--rule)); } +.acct-detail-actions .btn.ghost:hover { background: color-mix(in srgb, var(--conf-low) 8%, transparent); } + +/* ---- Chart of accounts -------------------------------------------- */ +.acct-coa { + display: flex; flex-direction: column; + gap: 20px; +} +.acct-coa-group { + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + overflow: hidden; +} +.acct-coa-head { + display: flex; align-items: center; gap: 10px; + padding: 12px 16px; + border-bottom: 1px solid var(--rule); + background: color-mix(in srgb, var(--paper) 94%, black 6%); +} +.acct-coa-title { + font-family: var(--font-sans-kr); + font-size: 14px; + font-weight: 500; + color: var(--fg-strong); +} +.acct-coa-count { + margin-left: auto; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.14em; + color: var(--fg-subtle); +} +.acct-coa-rows { padding: 4px 0; } +.acct-coa-row { + display: grid; + grid-template-columns: 1fr 2fr auto; + gap: 16px; + align-items: center; + padding: 7px 16px; + border-bottom: 1px solid var(--rule); + font-size: 12.5px; +} +.acct-coa-row:last-child { border-bottom: none; } +.acct-coa-row.branch { + background: color-mix(in srgb, var(--paper) 96%, black 4%); +} +.acct-coa-row.branch .acct-coa-leaf { + font-weight: 500; + color: var(--fg-strong); +} +.acct-coa-tree { + display: inline-flex; align-items: center; gap: 6px; +} +.acct-coa-glyph { + font-family: var(--font-mono); + color: var(--fg-subtle); + width: 12px; + text-align: center; +} +.acct-coa-leaf { + font-family: var(--font-sans-kr); + color: var(--fg); +} +.acct-coa-path { + font-size: 11px; + color: var(--fg-subtle); + letter-spacing: 0.02em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.acct-coa-amt { + font-size: 13px; + font-variant-numeric: tabular-nums; + color: var(--fg-strong); + text-align: right; +} +.acct-coa-amt.pos { color: var(--conf-rules); } +.acct-coa-amt.neg { color: var(--fg-strong); } +.acct-coa-amt .ccy { color: var(--fg-subtle); font-weight: 400; } + +/* ========================================================================= + HISTORY SCREEN + ========================================================================= */ +.history-screen { + padding: 24px; + overflow-y: auto; + height: 100%; +} + +/* Summary strip */ +.hist-summary-strip { + display: grid; + grid-template-columns: repeat(6, 1fr); + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + margin-bottom: 16px; + overflow: hidden; +} +.hist-summary-cell { + padding: 12px 16px; + border-right: 1px solid var(--rule); + display: flex; flex-direction: column; + gap: 4px; +} +.hist-summary-cell:last-child { border-right: none; } +.hist-summary-cell .lbl { + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.16em; + color: var(--fg-subtle); +} +.hist-summary-cell .val { + font-family: var(--font-mono); + font-size: 20px; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--fg-strong); + line-height: 1; +} +.hist-summary-cell .val.neg { color: var(--conf-low); } + +/* Filters */ +.hist-filters { + display: flex; flex-direction: column; + gap: 10px; + padding: 12px 16px; + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + margin-bottom: 16px; +} +.hist-filter-row { + display: flex; align-items: center; gap: 12px; + flex-wrap: wrap; +} +.hist-filter-lbl { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.16em; + color: var(--fg-subtle); + min-width: 48px; +} +.hist-filter-chips { display: flex; flex-wrap: wrap; gap: 4px; } +.hist-chip { + display: inline-flex; align-items: center; gap: 6px; + height: 24px; + padding: 0 10px; + background: transparent; + border: 1px solid var(--rule); + border-radius: 2px; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + color: var(--fg-muted); + text-transform: uppercase; +} +.hist-chip:hover { color: var(--fg-strong); border-color: var(--fg-muted); } +.hist-chip.active { + background: var(--fg-strong); + border-color: var(--fg-strong); + color: var(--bg); +} +.hist-chip.active .hist-chip-glyph { color: inherit !important; } +.hist-chip-glyph { font-size: 12px; } + +/* Table */ +.hist-table { + background: var(--paper); + border: 1px solid var(--rule); + border-radius: var(--r-md); + overflow: hidden; +} +.hist-table-head { + display: grid; + grid-template-columns: 78px 110px 80px 60px 1fr 140px; + gap: 14px; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid var(--rule); + background: color-mix(in srgb, var(--paper) 94%, black 6%); + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.18em; + color: var(--fg-subtle); +} + +.hist-day { display: flex; flex-direction: column; } +.hist-day-head { + display: flex; align-items: center; gap: 12px; + padding: 14px 16px 6px; + border-bottom: 1px solid var(--rule); +} +.hist-day-lbl { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.18em; + color: var(--fg-strong); +} +.hist-day-date { + font-size: 11px; + letter-spacing: 0.06em; + color: var(--fg-subtle); +} +.hist-day-count { + margin-left: auto; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.14em; + color: var(--fg-subtle); +} + +.hist-day-rows { display: flex; flex-direction: column; } +.hist-row { + display: grid; + grid-template-columns: 78px 110px 80px 60px 1fr 140px; + gap: 14px; + align-items: center; + padding: 11px 16px; + border-bottom: 1px solid var(--rule); +} +.hist-row:last-child { border-bottom: none; } +.hist-row:hover { background: color-mix(in srgb, var(--paper) 96%, var(--fg-strong) 4%); } +.hist-time { + font-size: 11.5px; + color: var(--fg-muted); + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} +.hist-col-kind { + display: inline-flex; align-items: center; gap: 6px; +} +.hist-glyph { + font-family: var(--font-mono); + font-size: 13px; + line-height: 1; + width: 14px; + text-align: center; +} +.hist-kind-lbl { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + color: var(--fg-strong); +} +.hist-actor { + display: inline-block; + padding: 1px 6px; + border-radius: 2px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.1em; + font-weight: 500; +} +.hist-actor-you { background: var(--fg-strong); color: var(--bg); } +.hist-actor-system { background: var(--rule); color: var(--fg); } +.hist-actor-agent { background: color-mix(in srgb, var(--conf-high) 18%, transparent); color: var(--conf-high); } + +.hist-col-summary { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.hist-summary { + font-size: 13px; + color: var(--fg); + font-family: var(--font-sans-kr); +} +.hist-detail { + font-size: 11px; + color: var(--fg-subtle); + letter-spacing: 0.02em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.hist-col-ref { + font-size: 11px; + color: var(--fg-muted); + letter-spacing: 0.02em; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.hist-empty { + padding: 48px 16px; + text-align: center; + color: var(--fg-subtle); +} +.hist-empty-lbl { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.18em; + color: var(--fg-muted); + margin-bottom: 6px; +} +.hist-empty-sub { font-size: 12.5px; } + +/* "all entity" badge (used in history for system events) */ +.entity-badge.all { + background: var(--rule); + color: var(--fg-muted); +} +.entity-badge.all::before { background: var(--fg-muted); } + diff --git a/ui_kits/web/data.js b/ui_kits/web/data.js index decfa9c..eaa0e08 100644 --- a/ui_kits/web/data.js +++ b/ui_kits/web/data.js @@ -50,6 +50,37 @@ window.AKEFIN_DATA = (() => { ], }; + // Connected data sources (banks, exchanges, file drops) + const connectedAccounts = [ + { id: "src01", name: "Toss · 신한 입출금", kind: "bank", country: "KR", entity: "personal", maskedNo: "•••• 4823", balance: 14200000, ccy: "KRW", lastSync: "2026-03-08 09:14", method: "csv-poll", status: "ok", nextRun: "in 4h", txCount30d: 247 }, + { id: "src02", name: "Kakao Bank · 9TFox", kind: "bank", country: "KR", entity: "tfox", maskedNo: "•••• 1102", balance: 18400000, ccy: "KRW", lastSync: "2026-03-07 18:02", method: "csv-poll", status: "ok", nextRun: "in 12h", txCount30d: 62 }, + { id: "src03", name: "Wise · multi-ccy", kind: "exchange", country: "BE", entity: "personal", maskedNo: "USR-93281", balance: 2840, ccy: "EUR", lastSync: "2026-03-07 12:48", method: "api", status: "ok", nextRun: "in 1h", txCount30d: 41 }, + { id: "src04", name: "Wise · 9TFox", kind: "exchange", country: "BE", entity: "tfox", maskedNo: "USR-93281-B", balance: 176.40, ccy: "EUR", lastSync: "2026-03-07 12:48", method: "api", status: "ok", nextRun: "in 1h", txCount30d: 14 }, + { id: "src05", name: "Garanti BBVA", kind: "bank", country: "TR", entity: "personal", maskedNo: "•••• 7012", balance: 62400, ccy: "TRY", lastSync: "2026-03-06 22:01", method: "csv-drop", status: "warn", nextRun: "manual", txCount30d: 89 }, + { id: "src06", name: "Finacode · partner", kind: "manual", country: "EU", entity: "finacode", maskedNo: "—", balance: 6320, ccy: "EUR", lastSync: "2026-03-03 11:20", method: "manual", status: "idle", nextRun: "—", txCount30d: 3 }, + { id: "src07", name: "MUFG · JPY savings", kind: "bank", country: "JP", entity: "personal", maskedNo: "•••• 9981", balance: 148000, ccy: "JPY", lastSync: "2026-03-05 03:11", method: "csv-drop", status: "fail", nextRun: "manual", txCount30d: 8 }, + ]; + + // Append-only audit history — most recent first + const history = [ + { id: "h001", at: "2026-03-08 11:42:18", actor: "you", kind: "commit", entity: "all", summary: "Committed ledger to git", detail: "e4a82c1 · 23 entries · +1.2k −0.1k", ref: "main" }, + { id: "h002", at: "2026-03-08 11:38:02", actor: "you", kind: "approve", entity: "personal", summary: "Approved 18 transactions ≥ 0.85 confidence", detail: "batch via ⌘⇧A", ref: "t14…t31" }, + { id: "h003", at: "2026-03-08 11:36:40", actor: "you", kind: "override", entity: "personal", summary: "Overrode category on 1 transaction", detail: "GS25 역삼점 → Personal:Expenses:Convenience", ref: "t05" }, + { id: "h004", at: "2026-03-08 11:30:09", actor: "you", kind: "rule", entity: "personal", summary: "Promoted rule · 스타벅스 *", detail: "→ Personal:Expenses:Food:Coffee · 14 matches", ref: "r01" }, + { id: "h005", at: "2026-03-08 09:14:00", actor: "system", kind: "import", entity: "personal", summary: "Imported Toss · 신한 입출금 CSV", detail: "247 rows · 153 auto · 44 high · 35 review · 15 failed", ref: "i01" }, + { id: "h006", at: "2026-03-08 09:11:22", actor: "system", kind: "poll", entity: "personal", summary: "Polled Toss for new transactions", detail: "247 new rows · password decrypt ok", ref: "src01" }, + { id: "h007", at: "2026-03-08 06:00:01", actor: "system", kind: "fx", entity: "all", summary: "FX rates refreshed", detail: "KRW=1 · EUR=1456.20 · TRY=41.32 · JPY=9.21", ref: "fx-snap" }, + { id: "h008", at: "2026-03-07 22:48:50", actor: "agent", kind: "categorize", entity: "tfox", summary: "Categorized 12 transactions", detail: "avg confidence 0.89 · 1 unmatched", ref: "agent-run-128" }, + { id: "h009", at: "2026-03-07 18:02:00", actor: "system", kind: "import", entity: "tfox", summary: "Imported Kakao Bank · 9TFox CSV", detail: "62 rows · 58 auto · 3 high · 1 review", ref: "i02" }, + { id: "h010", at: "2026-03-07 12:48:00", actor: "system", kind: "import", entity: "tfox", summary: "Imported Wise · 9TFox via API", detail: "14 rows · 12 auto · 2 high", ref: "i03" }, + { id: "h011", at: "2026-03-07 09:14:30", actor: "you", kind: "skip", entity: "personal", summary: "Skipped & re-queued 1 transaction", detail: "ATM 출금 강남역 · low confidence (0.42)", ref: "t13" }, + { id: "h012", at: "2026-03-06 22:01:00", actor: "system", kind: "import", entity: "personal", summary: "Imported Garanti BBVA CSV", detail: "89 rows · 61 auto · 14 high · 11 review · 3 failed", ref: "i04" }, + { id: "h013", at: "2026-03-06 20:55:14", actor: "you", kind: "rule", entity: "tfox", summary: "Edited rule · Hetzner *", detail: "target → 9TFox:Expenses:Infrastructure", ref: "r03" }, + { id: "h014", at: "2026-03-06 14:12:08", actor: "you", kind: "commit", entity: "all", summary: "Committed ledger to git", detail: "3a17fb9 · 47 entries · pushed to origin/main", ref: "main" }, + { id: "h015", at: "2026-03-05 03:11:00", actor: "system", kind: "error", entity: "personal", summary: "MUFG · JPY savings — CSV parse failed", detail: "Encoding mismatch · expected Shift-JIS, got UTF-8", ref: "src07" }, + { id: "h016", at: "2026-03-04 18:32:11", actor: "you", kind: "approve", entity: "tfox", summary: "Approved 7 transactions", detail: "manual review queue", ref: "t44…t50" }, + ]; + const importRuns = [ { id: "i01", at: "2026-03-08 09:14", source: "Toss · 신한 입출금", rows: 247, auto: 153, high: 44, review: 35, failed: 15, entity: "personal" }, { id: "i02", at: "2026-03-07 18:02", source: "Kakao Bank · 9TFox", rows: 62, auto: 58, high: 3, review: 1, failed: 0, entity: "tfox" }, @@ -110,5 +141,5 @@ window.AKEFIN_DATA = (() => { "Income:Consulting", "Income:PartnerShare", "Cash", ]; - return { transactions, ruleSuggestions, accountsByEntity, importRuns, entities, allAccounts, fx, convert, commands, categories }; + return { transactions, ruleSuggestions, accountsByEntity, importRuns, entities, allAccounts, fx, convert, commands, categories, connectedAccounts, history }; })(); diff --git a/ui_kits/web/index.html b/ui_kits/web/index.html index 644f719..8e7a10a 100644 --- a/ui_kits/web/index.html +++ b/ui_kits/web/index.html @@ -25,6 +25,8 @@ + +