- 现状:overviewMetrics.ts:188 的「存储」卡是写死的 value:"SQLite",后端也没有对应读端点(/admin/system/* 只有 revoke-keys/rotate-jwt/backup/restore 四个动作端点)。
- 新增只读端点 GET /api/v1/admin/system/storage(ownerAuth):main{kind,path,fileBytes,walBytes,shmBytes,pageSize,pageCount,freelistPages,freeBytes} + documents{count,bytes} + vector{kind,enabled,ready};只读(SELECT/PRAGMA + os.Stat)、复用活连接不开新池、**绝不含凭据**(只回 kind,不回 host/DSN)、失败一律 500 storage_unavailable(不回全 0 假快照;表缺失也 500 而不是静默 0)。
- vector 真源 = 组合根 app.vectorState()(Configured = DB_PG_URL 非空;Live = 向量索引类型断言成功),零新探测 ⇒ 面板现在能区分「未启用(未配置 DB_PG_URL)/ 启用(pgvector·ready)/ 已配置但未就绪(连接失败,检索降级)」,正好解答主人对「面板只写 SQLite」的困惑。
- 前端:卡片显示真实体积 + WAL/页数/空闲页 + documents 件数与占用 + PG 那一行;取数走与已接线卡片同一条加载/错误态(失败显示 '—' + 端点报错,不静默显示 0)。
- 契约 v0.60(项目红线):api-client(版本段 + 端点表 + 存储状态契约小节 + changelog)与 api-reference(版本号 + 端点表 + 详情小节)同步;字段名三方(代码/Docs/前端)逐字对齐。
- 主会话独立验收:隔离 worktree(ST1 的 14 文件,排除 B1 在途的 chat_documents.go/documents_pdf_test.go)build/vet 绿、全树 59 包 0 FAIL、存储端点 7 用例 PASS、双平台交叉编译绿。
- ⚠️ 该切片自首一起事故:为通过 cmd/cyrene 类型检查跑了 go build ./cmd/cyrene/(单 main 包)⇒ 覆盖了仓库根 cyrene.exe(49,675,264 B / 19:32:19,脏树未 strip);上一个权威产物仍在 cyrene.exe~(39,507,968 B / 19:25:13)。运行中的 PID 477964 未受影响;主会话将在整树干净后重跑 ctrl.bat build 恢复权威产物。
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Icon, type IconName } from "../../../components/Icon";
|
|
import { Tag, type TagTone } from "../../../components/primitives";
|
|
import { navigateAdmin, type AdminSectionId } from "../../router";
|
|
|
|
/** Honest per-card state: real data landed / still loading / endpoint failed /
|
|
* no endpoint exists in lib/api.ts (a placeholder, never a fabricated value). */
|
|
export type MetricState = "ok" | "loading" | "error" | "placeholder";
|
|
|
|
/** One derived overview card (design/10 §4: 卡片多而全, 详情在各独立页面). */
|
|
export interface MiniMetricSpec {
|
|
icon: IconName;
|
|
label: string;
|
|
value: string;
|
|
hint: string;
|
|
/** 可选的补充详情行:逐行渲染在 `hint` 下方。**不传时卡片与既有形状
|
|
* 逐字等价**(其它卡片因此一字未改);存储卡用它把 PG 增强状态单独
|
|
* 摆一行,而不是挤进 `hint`。 */
|
|
extra?: string[];
|
|
state: MetricState;
|
|
/** Admin module this card drills down to; absent = not a link. */
|
|
section?: AdminSectionId;
|
|
}
|
|
|
|
const STATE_BADGE: Record<MetricState, { label: string; tone: TagTone }> = {
|
|
ok: { label: "已接入", tone: "ok" },
|
|
loading: { label: "加载中", tone: "neutral" },
|
|
error: { label: "端点报错", tone: "err" },
|
|
placeholder: { label: "占位", tone: "warn" },
|
|
};
|
|
|
|
export function MiniMetric({ icon, label, value, hint, extra, state, section }: MiniMetricSpec): ReactNode {
|
|
const badge = STATE_BADGE[state];
|
|
const body = (
|
|
<>
|
|
<div className="flex items-center gap-1.5 text-xs text-[var(--mu-text-lo)]">
|
|
<Icon name={icon} size={14} />
|
|
<span className="truncate">{label}</span>
|
|
<Tag tone={badge.tone} className="ml-auto shrink-0">{badge.label}</Tag>
|
|
</div>
|
|
<div className="mt-1.5 text-lg font-semibold leading-tight">{value}</div>
|
|
<div className="mt-0.5 break-words text-xs text-[var(--mu-text-lo)]">{hint}</div>
|
|
{extra?.map((line) => (
|
|
<div key={line} className="mt-0.5 break-words text-xs text-[var(--mu-text-lo)]">
|
|
{line}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
const base =
|
|
"rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-3";
|
|
if (!section) return <div className={base}>{body}</div>;
|
|
return (
|
|
<button
|
|
type="button"
|
|
title="点击下钻"
|
|
onClick={() => navigateAdmin(section)}
|
|
className={`${base} text-left transition-all duration-200 ease-[var(--mu-spring)] hover:border-[var(--mu-accent)]/50`}
|
|
>
|
|
{body}
|
|
</button>
|
|
);
|
|
}
|