fix(admin): 仪表盘读不到数据时不再画成 0/「全部在线」
与渠道页同一类误导(主人本次误读的同源问题):六个 HTTP 卡片(人格/会话/记忆/插件/渠道/IoT)原用 `data ?? []` + `?? 0`,后端不可达时显示 0 且「全部在线」(tone=ok),把「读不到」画成好消息。 - 改为复用 channels/channelStatus 的列表三态:unknown → 「—」+「读取失败(与后端失联)」(warn);pending → 「—」+「读取中…」(neutral) - WS 驱动的 Agent / 后台思考卡片不依赖 HTTP,保持不变 - 验收:npm run build 绿 + channelStatus 45 断言 + messageSync 54 断言回归
This commit is contained in:
1 parent
37fb3cd265
commit
52ee8e53aa
1 file changed
+50
-16
@@ -14,6 +14,7 @@ import { Tag } from "../../components/primitives";
|
||||
import { Icon, type IconName } from "../../components/Icon";
|
||||
import { useAutoReload } from "../hooks";
|
||||
import { OverviewSection } from "./dashboard/OverviewSection";
|
||||
import { channelListState } from "./channels/channelStatus";
|
||||
|
||||
interface DashCard {
|
||||
id: AdminSectionId;
|
||||
@@ -43,25 +44,58 @@ export function DashboardModule(): React.ReactNode {
|
||||
const backthink = useAppStore((s) => s.backthink);
|
||||
const wsStatus = useAppStore((s) => s.wsStatus);
|
||||
|
||||
const nPersonas = personas.data?.length ?? 0;
|
||||
const nSessions = sessions.data?.length ?? 0;
|
||||
const nMemories = memories.data?.length ?? 0;
|
||||
const nPlugins = plugins.data?.length ?? 0;
|
||||
const nChannels = channels.data?.length ?? 0;
|
||||
const nDevices = iot.data?.length ?? 0;
|
||||
const offlineChannels = (channels.data ?? []).filter((c) => !c.connected).length;
|
||||
const onlineDevices = (iot.data ?? []).filter((d) => d.online).length;
|
||||
const runningAgents = agents.filter((a) => a.status === "running").length;
|
||||
|
||||
type Stat = { value: number | string; sub: string; tone: DashCard["tone"] };
|
||||
/** One HTTP-backed stat card. A list we could not read is "unknown" (—), never a
|
||||
* fabricated 0 / 「全部在线」: the dashboard is the first thing the owner opens
|
||||
* when something feels wrong, so it must not turn a dead backend into good news.
|
||||
* Reuses the channel pages' list tri-state so the rule stays in one place. */
|
||||
const stat = <T,>(
|
||||
req: { data: readonly T[] | null; error: string | null },
|
||||
ready: (list: readonly T[]) => Stat,
|
||||
): Stat => {
|
||||
switch (channelListState(req.data, req.error)) {
|
||||
case "unknown":
|
||||
return { value: "—", sub: "读取失败(与后端失联)", tone: "warn" };
|
||||
case "pending":
|
||||
return { value: "—", sub: "读取中…", tone: "neutral" };
|
||||
default:
|
||||
return ready(req.data as readonly T[]);
|
||||
}
|
||||
};
|
||||
|
||||
const cards: DashCard[] = [
|
||||
{ id: "personas", label: "人格", icon: "dna", value: nPersonas, sub: "启停/默认/路由", tone: "accent" },
|
||||
{ id: "sessions", label: "活跃会话", icon: "chat", value: nSessions, sub: "渠道 × actor", tone: "ok" },
|
||||
{ id: "memories", label: "记忆", icon: "memory", value: nMemories, sub: "三类作用域隔离", tone: "accent" },
|
||||
{ id: "plugins", label: "插件与能力", icon: "plugin", value: nPlugins, sub: "含 MCP/渠道", tone: "neutral" },
|
||||
{ id: "channels", label: "渠道", icon: "channel", value: `${nChannels - offlineChannels}/${nChannels}`,
|
||||
sub: offlineChannels > 0 ? `${offlineChannels} 离线` : "全部在线", tone: offlineChannels > 0 ? "warn" : "ok" },
|
||||
{ id: "iot", label: "IoT 设备", icon: "device", value: `${onlineDevices}/${nDevices}`,
|
||||
sub: onlineDevices === nDevices && nDevices > 0 ? "全部在线" : "设备状态", tone: nDevices > 0 && onlineDevices === nDevices ? "ok" : "neutral" },
|
||||
{ id: "personas", label: "人格", icon: "dna", ...stat(personas, (l) => ({ value: l.length, sub: "启停/默认/路由", tone: "accent" })) },
|
||||
{ id: "sessions", label: "活跃会话", icon: "chat", ...stat(sessions, (l) => ({ value: l.length, sub: "渠道 × actor", tone: "ok" })) },
|
||||
{ id: "memories", label: "记忆", icon: "memory", ...stat(memories, (l) => ({ value: l.length, sub: "三类作用域隔离", tone: "accent" })) },
|
||||
{ id: "plugins", label: "插件与能力", icon: "plugin", ...stat(plugins, (l) => ({ value: l.length, sub: "含 MCP/渠道", tone: "neutral" })) },
|
||||
{
|
||||
id: "channels",
|
||||
label: "渠道",
|
||||
icon: "channel",
|
||||
...stat(channels, (l) => {
|
||||
const offline = l.filter((c) => !c.connected).length;
|
||||
return {
|
||||
value: `${l.length - offline}/${l.length}`,
|
||||
sub: offline > 0 ? `${offline} 离线` : "全部在线",
|
||||
tone: offline > 0 ? "warn" : "ok",
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "iot",
|
||||
label: "IoT 设备",
|
||||
icon: "device",
|
||||
...stat(iot, (l) => {
|
||||
const online = l.filter((d) => d.online).length;
|
||||
return {
|
||||
value: `${online}/${l.length}`,
|
||||
sub: online === l.length && l.length > 0 ? "全部在线" : "设备状态",
|
||||
tone: l.length > 0 && online === l.length ? "ok" : "neutral",
|
||||
};
|
||||
}),
|
||||
},
|
||||
{ id: "agents", label: "Agent 运行", icon: "agent", value: runningAgents, sub: `${agents.length} 个已登记`, tone: runningAgents > 0 ? "accent" : "neutral" },
|
||||
{ id: "backthink", label: "后台思考活跃度", icon: "night", value: backthink.length, sub: "实时 (WS)", tone: backthink.length > 0 ? "warn" : "neutral" },
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user