feat: accounts and history pages are in place

This commit is contained in:
Alkim Ake Gozen 2026-05-23 23:43:08 +09:00
parent 09fc5dc367
commit 8c42d246ea
7 changed files with 1135 additions and 3 deletions

View file

@ -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 (
<div className="hist-filters">
<div className="hist-filter-row">
<span className="hist-filter-lbl">KIND</span>
<div className="hist-filter-chips">
<button className={`hist-chip ${activeKinds.size === 0 ? "active" : ""}`}
onClick={() => activeKinds.size > 0 && [...activeKinds].forEach(toggleKind)}>
ALL
</button>
{kinds.map(k => (
<button key={k}
className={`hist-chip ${activeKinds.has(k) ? "active" : ""}`}
onClick={() => toggleKind(k)}>
<span className="hist-chip-glyph" style={{ color: KIND_META[k].color }}>{KIND_META[k].glyph}</span>
{KIND_META[k].label}
</button>
))}
</div>
</div>
<div className="hist-filter-row">
<span className="hist-filter-lbl">ACTOR</span>
<div className="hist-filter-chips">
{["all", "you", "system", "agent"].map(a => (
<button key={a}
className={`hist-chip ${actor === a ? "active" : ""}`}
onClick={() => setActor(a)}>
{a.toUpperCase()}
</button>
))}
</div>
<span className="hist-filter-lbl" style={{ marginLeft: 24 }}>RANGE</span>
<div className="hist-filter-chips">
{[
{ id: "24h", label: "24H" },
{ id: "7d", label: "7D" },
{ id: "30d", label: "30D" },
{ id: "all", label: "ALL" },
].map(r => (
<button key={r.id}
className={`hist-chip ${range === r.id ? "active" : ""}`}
onClick={() => setRange(r.id)}>
{r.label}
</button>
))}
</div>
</div>
</div>
);
}
// 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 (
<div className="hist-row">
<div className="hist-col-time">
<span className="hist-time mono">{row.at.split(" ")[1]}</span>
</div>
<div className="hist-col-kind">
<span className="hist-glyph" style={{ color: meta.color }}>{meta.glyph}</span>
<span className="hist-kind-lbl">{meta.label}</span>
</div>
<div className="hist-col-actor">
<span className={`hist-actor hist-actor-${row.actor}`}>{ACTOR_LABEL[row.actor]}</span>
</div>
<div className="hist-col-entity">
<span className={`entity-badge ${entityClass[row.entity] || "all"}`}>{entityShort[row.entity]}</span>
</div>
<div className="hist-col-summary">
<div className="hist-summary">{row.summary}</div>
<div className="hist-detail mono">{row.detail}</div>
</div>
<div className="hist-col-ref mono">{row.ref}</div>
</div>
);
}
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 (
<div className="history-screen">
<div className="screen-head">
<div>
<div className="screen-eyebrow">HISTORY · {entity === "all" ? "ALL ENTITIES" : entity.toUpperCase()}</div>
<h1 className="screen-title">Audit log</h1>
<div className="screen-sub">
{counts.total} events · append-only · backed by git on <span className="mono">main</span>
</div>
</div>
<div className="screen-actions">
<button className="btn">Export log</button>
<button className="btn">Diff vs. last commit</button>
</div>
</div>
{/* Summary strip */}
<div className="hist-summary-strip">
<div className="hist-summary-cell">
<div className="lbl">EVENTS</div>
<div className="val">{counts.total}</div>
</div>
<div className="hist-summary-cell">
<div className="lbl">BY YOU</div>
<div className="val">{counts.byActor.you || 0}</div>
</div>
<div className="hist-summary-cell">
<div className="lbl">BY SYSTEM</div>
<div className="val">{counts.byActor.system || 0}</div>
</div>
<div className="hist-summary-cell">
<div className="lbl">BY AGENT</div>
<div className="val">{counts.byActor.agent || 0}</div>
</div>
<div className="hist-summary-cell">
<div className="lbl">COMMITS</div>
<div className="val">{counts.byKind.commit || 0}</div>
</div>
<div className="hist-summary-cell">
<div className="lbl">ERRORS</div>
<div className={`val ${(counts.byKind.error || 0) > 0 ? "neg" : ""}`}>{counts.byKind.error || 0}</div>
</div>
</div>
<HistoryFilters
activeKinds={activeKinds}
toggleKind={toggleKind}
actor={actor} setActor={setActor}
range={range} setRange={setRange}
/>
<div className="hist-table">
<div className="hist-table-head">
<div className="hist-col-time">TIME</div>
<div className="hist-col-kind">KIND</div>
<div className="hist-col-actor">ACTOR</div>
<div className="hist-col-entity">ENT</div>
<div className="hist-col-summary">EVENT</div>
<div className="hist-col-ref">REF</div>
</div>
{grouped.length === 0 && (
<div className="hist-empty">
<div className="hist-empty-lbl">NO EVENTS MATCH</div>
<div className="hist-empty-sub">Try widening the range or clearing filters.</div>
</div>
)}
{grouped.map(([day, dayRows]) => (
<div key={day} className="hist-day">
<div className="hist-day-head">
<span className="hist-day-lbl">{dayLabel(day)}</span>
<span className="hist-day-date mono">{day}</span>
<span className="hist-day-count">{dayRows.length} events</span>
<span className="hist-day-rule"></span>
</div>
<div className="hist-day-rows">
{dayRows.map(r => <HistoryRow key={r.id} row={r} />)}
</div>
</div>
))}
</div>
</div>
);
}
Object.assign(window, { HistoryScreen });
})();