Files
oO/apps/admin/src/components/AuditLog.tsx
alvis e62c726ea4 feat: M1 admin console — all 10 remaining pages + signal/quality/ops infrastructure
Admin console (issues #63–72):
- Event stream viewer: live-tail ring buffer (500 events) with subject/user filters
- Feature store browser: per-user feature vector history from ml/serving
- Model registry panel: MLflow embed at /admin/models
- Experiment dashboard: LinUCB per-user stats (pulls, reward, θ) + bandit reset
- Recommendation log: per-tip explainability (policy, score, features, latency)
- Reward analytics: daily reaction breakdown + per-policy compare
- Data quality widget: missing-feature rate, stale-token rate, daily completeness
- Ops actions: replay-signal, policy enable/disable; user actions link to Users page
- SQL runner: read-only SELECT runner with saved queries
- Health rollup: fan-out to api/ml/sqlite/event-bus with auto-refresh

Backend:
- tip_scores table: logs features+policy+score+latency at every scoring call (#67)
- saved_queries table: per-admin saved SQL (#71)
- Event bus: 500-event ring buffer + tail() API (#63)
- Admin routes: /events, /tips, /reward-analytics, /data-quality, /health,
  /policies, /replay-signal, /sql, /saved-queries endpoints
- /api/ml/* admin-gated proxy to ml/serving (#64, #66)
- Shadow-policy registry in recommender (#56)

ML serving:
- /reset/{user_id}: clear bandit state + feature history (#66)
- /stats/{user_id}: pulls, cumulative reward, estimated mean, θ (#66)
- /features/{user_id}: last 100 feature vectors logged at scoring time (#64)
- Meta (pulls, rewards) persisted alongside A/b matrices

Web:
- Tip action sheet adds Helpful / Not helpful buttons (#62)
- TipFeedback type extended with helpful/not_helpful actions
- Rewards mapped: helpful=+0.5, not_helpful=−0.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 03:56:48 +00:00

113 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState } from 'react';
import { getAuditLog, type AuditAction } from '@/lib/api';
const PAGE_SIZE = 50;
export function AuditLog() {
const [rows, setRows] = useState<AuditAction[]>([]);
const [total, setTotal] = useState(0);
const [offset, setOffset] = useState(0);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
getAuditLog(PAGE_SIZE, offset)
.then(({ actions, total }) => {
setRows(actions);
setTotal(total);
})
.catch((e) => setError(String(e.message)))
.finally(() => setLoading(false));
}, [offset]);
if (error) return <p className="text-red-400 text-sm">Error: {error}</p>;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">Audit log</h1>
<span className="text-sm text-gray-500">{total} entries</span>
</div>
<div className="rounded-lg border border-gray-800 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
{['Time', 'Admin', 'Action', 'Target'].map((h) => (
<th
key={h}
className="text-left px-4 py-2.5 text-xs text-gray-500 font-medium uppercase tracking-wide"
>
{h}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{loading ? (
<tr>
<td colSpan={4} className="px-4 py-6 text-center text-gray-500">
Loading
</td>
</tr>
) : rows.length === 0 ? (
<tr>
<td colSpan={4} className="px-4 py-6 text-center text-gray-500">
No actions logged yet.
</td>
</tr>
) : (
rows.map((a) => (
<tr key={a.id} className="hover:bg-gray-900 transition-colors">
<td className="px-4 py-2.5 text-xs tabular-nums text-gray-400">
{a.createdAt.slice(0, 19).replace('T', ' ')}
</td>
<td className="px-4 py-2.5 font-mono text-xs text-gray-300 truncate max-w-[8rem]">
{a.adminId.slice(0, 8)}
</td>
<td className="px-4 py-2.5">
<span className="px-1.5 py-0.5 rounded bg-gray-800 text-xs text-gray-200 font-mono">
{a.action}
</span>
</td>
<td className="px-4 py-2.5 text-xs text-gray-400">
{a.targetType && (
<span className="text-gray-500">{a.targetType}: </span>
)}
<span className="font-mono">{a.targetId?.slice(0, 12) ?? '—'}</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{total > PAGE_SIZE && (
<div className="flex items-center gap-3 text-sm">
<button
disabled={offset === 0}
onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
className="px-3 py-1.5 rounded border border-gray-700 disabled:opacity-30 hover:border-gray-500 transition-colors"
>
Previous
</button>
<span className="text-gray-500">
{offset + 1}{Math.min(offset + PAGE_SIZE, total)} of {total}
</span>
<button
disabled={offset + PAGE_SIZE >= total}
onClick={() => setOffset(offset + PAGE_SIZE)}
className="px-3 py-1.5 rounded border border-gray-700 disabled:opacity-30 hover:border-gray-500 transition-colors"
>
Next
</button>
</div>
)}
</div>
);
}