feat(frontend/admin): M10 后台管理面板(/admin hash 路由 + 15 大模块 + RBAC(owner/admin/viewer) + 审计 + Miuix);Docs/api-client.md v0.3 契约补全;tsc+vite build 通过
This commit is contained in:
1 parent
4c48e94659
commit
f43eb17030
26 files changed
+2253
-4
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import { useState } from "react";
|
||||
import { useAppStore } from "../../store/useAppStore";
|
||||
import { useAuditStore } from "../audit";
|
||||
import { CardGrid, Notice, PageHeader, StatCard } from "../components";
|
||||
import { Button, Surface, Tag } from "../../components/primitives";
|
||||
|
||||
type Tab = "logs" | "trace" | "metrics" | "audit";
|
||||
|
||||
const TABS: ReadonlyArray<{ id: Tab; label: string }> = [
|
||||
{ id: "logs", label: "实时日志" },
|
||||
{ id: "trace", label: "Tracing" },
|
||||
{ id: "metrics", label: "指标" },
|
||||
{ id: "audit", label: "审计" },
|
||||
];
|
||||
|
||||
export function ObservabilityModule(): React.ReactNode {
|
||||
const [tab, setTab] = useState<Tab>("logs");
|
||||
const deviceUpdates = useAppStore((s) => s.deviceUpdates);
|
||||
const audit = useAuditStore((s) => s.records);
|
||||
const clearAudit = useAuditStore((s) => s.clear);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader title="可观测性" subtitle="实时日志 tail · Tracing(LLM/工具/编排) · 指标 · 审计" />
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`rounded-[var(--mu-radius)] px-3 py-1.5 text-xs font-medium transition-all duration-200 ease-[var(--mu-spring)] ${
|
||||
tab === t.id ? "bg-[var(--mu-accent)]/20 text-[var(--mu-accent-hi)]" : "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)]"
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{tab === "logs" ? <LogsTail lines={deviceUpdates} /> : null}
|
||||
{tab === "trace" ? <TraceView /> : null}
|
||||
{tab === "metrics" ? <MetricsView /> : null}
|
||||
{tab === "audit" ? <AuditView records={audit} onClear={clearAudit} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LogsTail({ lines }: { lines: string[] }): React.ReactNode {
|
||||
return (
|
||||
<Surface className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">实时日志流(WS 事件回显)</h3>
|
||||
<Tag tone="ok">tail</Tag>
|
||||
</div>
|
||||
{lines.length === 0 ? (
|
||||
<Notice>暂无实时事件。接入 /ws/chat 后,此处回显 stream / tool / device_update / reminder 等事件。</Notice>
|
||||
) : (
|
||||
<pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 font-mono text-xs text-[var(--mu-text-mid)]">
|
||||
{lines.join("\n")}
|
||||
</pre>
|
||||
)}
|
||||
<Notice>完整后端日志分级 tail / 过滤 / 导出需后端 M10+ 提供。</Notice>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
function TraceView(): React.ReactNode {
|
||||
return (
|
||||
<Surface className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Tracing 视图</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
{["LLM 调用链", "工具链", "编排链"].map((x) => (
|
||||
<div key={x} className="rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-4 text-center text-xs text-[var(--mu-text-mid)]">
|
||||
<div className="mb-2 text-lg">{x}</div>
|
||||
{x} 占位 —— 后端 M10+ 提供 span/trace 数据
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricsView(): React.ReactNode {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<CardGrid>
|
||||
<StatCard icon="⚙" label="请求 QPS" value="—" sub="网关指标占位" tone="accent" />
|
||||
<StatCard icon="🤖" label="LLM 调用次数" value="—" sub="用量/费用占位" tone="neutral" />
|
||||
<StatCard icon="🧩" label="子代理事件" value="—" sub="编排指标占位" tone="ok" />
|
||||
<StatCard icon="🧠" label="工时/Token" value="—" sub="成本占位" tone="warn" />
|
||||
</CardGrid>
|
||||
<Notice>指标时序图 / 导出需后端 M10+ 提供。</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface AuditViewProps {
|
||||
records: ReturnType<typeof useAuditStore.getState>["records"];
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
function AuditView({ records, onClear }: AuditViewProps): React.ReactNode {
|
||||
return (
|
||||
<Surface className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">审计日志(who / what / when / result)</h3>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">管理 + 敏感操作全程落审计</span>
|
||||
<span className="ml-auto">
|
||||
<Button size="sm" variant="ghost" onClick={onClear}>
|
||||
清空本地
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
{records.length === 0 ? (
|
||||
<Notice>暂无审计记录。本面板的管理/敏感操作(策略写入、主人档案编辑、API key 签发等)都会在此登记。</Notice>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[var(--mu-glass-border)] text-left text-xs uppercase tracking-wider text-[var(--mu-text-lo)]">
|
||||
<th className="px-2 py-1.5">who</th>
|
||||
<th className="px-2 py-1.5">what</th>
|
||||
<th className="px-2 py-1.5">when</th>
|
||||
<th className="px-2 py-1.5">result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((r) => (
|
||||
<tr key={r.id} className="border-b border-[var(--mu-glass-border)]/60 last:border-0">
|
||||
<td className="px-2 py-2 text-xs">{r.who}</td>
|
||||
<td className="px-2 py-2 text-xs font-mono">{r.what}</td>
|
||||
<td className="px-2 py-2 text-xs text-[var(--mu-text-lo)]">{new Date(r.when).toLocaleString()}</td>
|
||||
<td className="px-2 py-2"><Tag tone={r.result === "ok" ? "ok" : "err"}>{r.result}</Tag></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user