feat: accounts and history pages are in place
This commit is contained in:
parent
09fc5dc367
commit
8c42d246ea
7 changed files with 1135 additions and 3 deletions
245
ui_kits/web/HistoryScreen.jsx
Normal file
245
ui_kits/web/HistoryScreen.jsx
Normal 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 });
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue