diff --git a/internal/plugins/webui/dashboard.css b/internal/plugins/webui/dashboard.css
index bc43e3f..e829b4c 100644
--- a/internal/plugins/webui/dashboard.css
+++ b/internal/plugins/webui/dashboard.css
@@ -2740,3 +2740,74 @@
border-color: var(--text-primary);
box-shadow: 0 0 0 2px var(--accent-bg);
}
+
+ /* ===== 总览 KPI(图标 + 数字,静态骨架)=====
+ 节点只在首帧建一次,刷新只改值与类名 —— 这是总览页不再闪的前提。 */
+ .ov-kpis {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
+ gap: 10px;
+ }
+ .ov-kpi {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 12px 14px 11px;
+ border-radius: var(--radius-md);
+ background: var(--bg-input);
+ border: 1px solid var(--border-color);
+ min-width: 0;
+ }
+ .ov-ico svg {
+ width: 18px;
+ height: 18px;
+ fill: none;
+ stroke: var(--accent);
+ stroke-width: 1.7;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ display: block;
+ }
+ .ov-val {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 1.15;
+ min-width: 0;
+ }
+ .ov-val b {
+ font-weight: 700;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .ov-lab {
+ font-size: 10px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ }
+ .ov-dot {
+ flex: 0 0 auto;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--text-muted);
+ }
+ .ov-dot.ok {
+ background: var(--success);
+ box-shadow: 0 0 8px rgba(23, 169, 100, 0.55);
+ }
+ .ov-dot.warn {
+ background: var(--warning);
+ }
+ .ov-dot.bad {
+ background: var(--error);
+ }
+ .ov-foot {
+ margin-top: 10px;
+ font-size: 11px;
+ color: var(--text-muted);
+ }
diff --git a/internal/plugins/webui/dashboard.js b/internal/plugins/webui/dashboard.js
index 77f8920..bc045a4 100644
--- a/internal/plugins/webui/dashboard.js
+++ b/internal/plugins/webui/dashboard.js
@@ -567,12 +567,12 @@
function startUptimeTicker() {
if (uptimeTick) clearInterval(uptimeTick);
uptimeTick = setInterval(function () {
- var el = document.querySelector("#uptime-val");
+ var el = document.querySelector("#ov-uptime");
if (el && state.startedAt) {
var now = Date.now();
el.textContent = fmtUptime(now - state.startedAt);
} else if (!state.startedAt) {
- var el2 = document.querySelector("#uptime-val");
+ var el2 = document.querySelector("#ov-uptime");
if (el2) el2.textContent = "-";
}
}, 1000);
@@ -1180,140 +1180,121 @@
}, 3000);
}
+ // ===== Overview =====
+ //
+ // 总览页是**静态骨架 + 数据绑定**:DOM 只在首帧建一次,之后所有刷新只改
+ // 文本/类名,绝不重建节点。此前 renderAll 每 15s 把整个总览(含运行态面板)
+ // innerHTML 重建一遍 —— 那是页面上最刺眼的周期性闪烁的来源。
+ //
+ // 文字也压到最少:状态用彩色圆点表达,其余只留数字与两字标签,不再堆
+ // 「未初始化 / 不可用」这类整句描述。
+ var OV_ICONS = {
+ activity:
+ '',
+ clock:
+ '',
+ puzzle:
+ '',
+ tag:
+ '',
+ brain:
+ '',
+ db:
+ '',
+ file:
+ '',
+ cpu:
+ '',
+ };
+
+ function ovKpi(id, icon, label, dot) {
+ return (
+ '
' +
+ icon +
+ '' +
+ (dot ? '' : "") +
+ '—' +
+ label +
+ "
"
+ );
+ }
+
function renderOverview() {
- // 总览页会整块重建 #rt-panel(面板本身是空的),所以必须让运行态的
- // 数据签名失效,否则 renderRuntime 会以为“没变化”而不去填这块空面板。
- _rtSig = null;
+ var host = document.getElementById("tab-overview");
+ if (!host) return;
+ // 只在首帧建骨架;之后(renderAll 每 15s 一次)只 updateOverview。
+ // 一个节点都不重建,所以不会闪。
+ if (!document.getElementById("ov-kpis")) {
+ host.innerHTML =
+ '' +
+ '' +
+ ovKpi("status", OV_ICONS.activity, __("状态", "Status"), true) +
+ ovKpi("uptime", OV_ICONS.clock, __("运行", "Uptime")) +
+ ovKpi("plugins", OV_ICONS.puzzle, __("插件", "Plugins")) +
+ ovKpi("version", OV_ICONS.tag, __("版本", "Version")) +
+ ovKpi("llm", OV_ICONS.brain, "LLM", true) +
+ ovKpi("memory", OV_ICONS.db, __("记忆", "Memory")) +
+ ovKpi("docs", OV_ICONS.file, __("文档", "Docs")) +
+ ovKpi("runtime", OV_ICONS.cpu, __("运行时", "Runtime")) +
+ '
';
+ }
+ updateOverview();
+ }
+
+ function ovSet(id, text) {
+ var el = document.getElementById(id);
+ if (el && el.textContent !== String(text)) el.textContent = String(text);
+ }
+ function ovDot(id, cls) {
+ var el = document.getElementById(id);
+ if (el && el.className !== cls) el.className = cls;
+ }
+
+ // updateOverview 只写值与类名,从不改结构。
+ function updateOverview() {
var s = state.status || {};
var k = state.kernel;
- var html =
- '' +
- '' +
- __("系统概览", "System Overview") +
- "
" +
- '
' +
- __("运行状态", "Status") +
- '' +
- escHtml(s.status || "unknown") +
- "
" +
- '
' +
- __("运行时间", "Uptime") +
- '' +
- (state.startedAt ? fmtUptime(Date.now() - state.startedAt) : "-") +
- "
" +
- '
' +
- __("插件", "Plugins") +
- '' +
- ((k?.plugins || []).length || 0) +
- "
" +
- '
' +
- __("版本", "Version") +
- '' +
- escHtml(
- k?.build?.version
- ? (k.build.kernel_name || "HomeAgent") + " v" + k.build.version
- : s.version
- ? "v" + s.version
- : "-",
- ) +
- "
" +
- // AGPL-3.0 §13:向网络使用者提供取得对应源码的入口。
- // 地址来自内核 meta.SourceURL(构建时可 -ldflags 覆盖),
- // 修改后对外部署的分支必须把它指向自己的源码仓库。
- (k?.build?.source_url
- ? '
"
- : "") +
- '
' +
- __("构建", "Build") +
- '' +
- escHtml(fmtBuild(k?.build, s)) +
- "
";
- if (k) {
- html +=
- '' +
- __("LLM 状态", "LLM Status") +
- "
" +
- '
Provider' +
- (k.llm?.provider || __("未配置", "Not configured")) +
- "
" +
- '
' +
- __("可用源", "Sources") +
- '' +
- (k.llm?.sources || 0) +
- "
" +
- '
' +
- __("状态", "Status") +
- '' +
- (k.llm?.available
- ? __("运行中", "Running")
- : __("不可用", "Unavailable")) +
- "
";
- html +=
- '' +
- __("记忆状态", "Memory Status") +
- "
" +
- '
' +
- __("图记忆", "Graph Memory") +
- '' +
- (k.memory?.available
- ? k.memory.entity_count +
- __(" 实体, ", " entities, ") +
- k.memory.relation_count +
- __(" 关系", " relations")
- : __("未初始化", "Uninitialized")) +
- "
" +
- '
' +
- __("文档记忆", "Document Memory") +
- '' +
- (k.documents?.available
- ? k.documents.doc_count + __(" 文档", " docs")
- : __("未初始化", "Uninitialized")) +
- "
" +
- '
' +
- __("文本记忆", "Text Memory") +
- '' +
- (k.text_memory?.available
- ? k.text_memory.file_count + __(" 文件", " files")
- : __("未初始化", "Uninitialized")) +
- "
" +
- '
' +
- __("知识库", "Knowledge") +
- '' +
- (k.knowledge?.available
- ? k.knowledge.item_count + __(" 项", " items")
- : __("未初始化", "Uninitialized")) +
- "
";
- html +=
- '' +
- __("运行时", "Runtime") +
- "
" +
- '
Goroutines' +
- (k.runtime?.goroutines || "-") +
- "
" +
- '
' +
- __("内存", "Memory") +
- '' +
- (k.runtime?.memory_mb ? k.runtime.memory_mb + " MB" : "-") +
- "
" +
- '
Go ' +
- __("版本", "Version") +
- '' +
- escHtml(k.runtime?.go_version || "-") +
- "
";
+ __("源码", "Source") +
+ ""
+ : "";
+ if (foot.__html !== want) {
+ foot.__html = want;
+ foot.innerHTML = want;
+ }
}
- document.getElementById("tab-overview").innerHTML = html;
}
// ===== Chat =====