diff --git a/internal/plugins/webui/dashboard.css b/internal/plugins/webui/dashboard.css
index 2947c09..0caa046 100644
--- a/internal/plugins/webui/dashboard.css
+++ b/internal/plugins/webui/dashboard.css
@@ -2191,6 +2191,16 @@
.rt-lv-3 > i { background: #ffa657; }
.rt-lv-2 > i { background: var(--sakura-400); }
.rt-lv-1 > i { background: var(--frost-300); }
+ /* 第五条:排队队列(无级别)。用虚线分隔 + 中性色,与“有级别”的四条区分开——
+ 它不是优先级,而是另一**类别**(排队 vs 中断),同一套配色会误导。 */
+ .rt-level.rt-level-queued {
+ border-top: 1px dashed rgba(255, 255, 255, 0.14);
+ margin-top: 5px;
+ padding-top: 7px;
+ }
+ .rt-level .rt-lv-track.rt-lv-q > i {
+ background: #a3be8c;
+ }
/* 中断栈:栈顶在上,做出"层叠现场"的观感 */
.rt-stack {
display: flex;
diff --git a/internal/plugins/webui/dashboard.js b/internal/plugins/webui/dashboard.js
index 5c228b9..6099e25 100644
--- a/internal/plugins/webui/dashboard.js
+++ b/internal/plugins/webui/dashboard.js
@@ -655,11 +655,34 @@
var residents = rt.residents || [];
var channels = rt.channels || [];
var inputs = rt.input_channels || [];
- // 数据签名(**不含 uptime**——它每秒都变,带上就等于没缓存):
- // 与上次相同则直接返回,一个字节都不动(“首页一闪一闪”的根因)。
+
+ // 数据签名(**不含 uptime**——它每秒都变,带上就等于没缓存):整体没变就整块跳过。
var sig = JSON.stringify([sc, residents, channels, inputs]);
if (sig === _rtSig) return;
_rtSig = sig;
+
+ // 外壳只建一次;随后**逐段**更新。
+ //
+ // 为什么不是整块 innerHTML:设备通道列表这类数据本来就会来回变(实测远程设备
+ // 通道 11→9 条),整块重建会把没变的段落(含条形 transition)也推倒重来,
+ // 视觉上就是「一闪一闪」。逐段比较后只替换真正变了的那一段。
+ if (!document.getElementById("rt-sec-tiles")) {
+ el.innerHTML =
+ '
' + __("运行态", "Runtime") + "
";
- // 四个数字块
- html += '
';
- html += rtTile(ready, __("排队任务", "Ready queue"), __("等待执行的输入", "inputs waiting"), ready ? Math.min(100, ready * 20) : 0, ready > 0);
- html += rtTile(pending, __("待处理中断", "Pending interrupts"), __("四级队列 + 立即抢占", "queued + immediate"), pending ? Math.min(100, pending * 25) : 0, pending > 0, pending > 0);
- html += rtTile(stack + "/" + maxStack, __("中断栈", "Interrupt stack"), __("嵌套抢占的现场", "nested frames"), (stack / (maxStack || 4)) * 100, stack > 0);
+ // ---- 段 1:四个数字块 ----
+ var h = '
';
+ h += rtTile(ready, __("排队任务", "Ready queue"), __("等待执行的输入", "inputs waiting"), ready ? Math.min(100, ready * 20) : 0, ready > 0);
+ h += rtTile(pending, __("待处理中断", "Pending interrupts"), __("四级队列 + 立即抢占", "queued + immediate"), pending ? Math.min(100, pending * 25) : 0, pending > 0, pending > 0);
+ h += rtTile(stack + "/" + maxStack, __("中断栈", "Interrupt stack"), __("嵌套抢占的现场", "nested frames"), (stack / (maxStack || 4)) * 100, stack > 0);
var rFull = residents.filter(function (r) { return r.context_full; }).length;
- html += rtTile(residents.length, __("驻留子 Agent", "Resident agents"), rFull ? rFull + __(" 个上下文已满", " context-full") : __("常驻子任务", "long-lived children"), residents.length ? Math.min(100, residents.length * 20) : 0, residents.length > 0, rFull > 0);
- html += "
";
+ h += rtTile(residents.length, __("驻留子 Agent", "Resident agents"), rFull ? rFull + __(" 个上下文已满", " context-full") : __("常驻子任务", "long-lived children"), residents.length ? Math.min(100, residents.length * 20) : 0, residents.length > 0, rFull > 0);
+ h += "
";
+ // 抢占/背压计数(设计 §11.6 E4 的按级别口径)
+ h += '
' + __("累计", "totals") + ": " +
+ __("入队", "enqueued") + " " + (sc.enqueued || 0) + " · " +
+ __("执行", "executed") + " " + (sc.executed || 0) + " · " +
+ __("抢占", "preempted") + " " + (sc.preempted || 0) + " · " +
+ __("挂起/恢复", "susp/res") + " " + (sc.suspended || 0) + "/" + (sc.resumed || 0) + " · " +
+ __("拒绝", "rejected") + " " + (sc.rejected || 0) + " · " +
+ __("背压", "backpressure") + " " + (sc.backpressure || 0) + "
";
+ put("tiles", h);
- // 四级中断队列
- html += '
' + __("中断队列(按级别)", "Interrupt queues by level") + "
";
- var maxQ = Math.max(1, q[1] || 0, q[2] || 0, q[3] || 0, q[4] || 0);
+ // ---- 段 2:队列(四级中断 + 一条排队)----
+ //
+ // 设计是「四条中断队列(L1–L4)+ 一条排队队列」共五个,所以必须画五行:
+ // 只画四条会让「排队输入」这条线在运行态里凭空消失,而它正是
+ // 「不需要及时处理」的那一半输入。排队队列**无级别**,故用不同配色 + 虚线。
+ h = '
' + __("队列(四级中断 + 排队)", "Queues (4 interrupt levels + queued)") + "
";
+ var maxQ = Math.max(1, ready, q[1] || 0, q[2] || 0, q[3] || 0, q[4] || 0);
var maxReg = 1;
var maxPre = 1;
RT_LEVELS.forEach(function (L) {
maxReg = Math.max(maxReg, byLv[L.lv] || 0);
maxPre = Math.max(maxPre, preLv[L.lv] || 0);
});
- html += '
';
+ h += '
';
RT_LEVELS.forEach(function (L) {
var depth = q[L.lv] || 0;
var reg = byLv[L.lv] || 0;
var pre = preLv[L.lv] || 0;
- html +=
+ h +=
'
' + L.name + "" +
'
";
});
- html += "
";
+ h += "
";
+ // 第五条:排队队列(无级别,纯 FIFO)
+ h += '
' +
+ __("排队(无级别)", "queued (no level)") + "" +
+ '' +
+ '' + ready + " · " +
+ __("FIFO,可被任何中断打断", "FIFO, preempted by any interrupt") + "
";
+ h += "
";
if (sc.immediate) {
- html += '⚡ ' + __("立即运行", "immediate") + ":" +
+ h += '
⚡ ' + __("立即运行", "immediate") + ":" +
escHtml(sc.immediate.kind || "") + " #" + sc.immediate.id +
'L' + (sc.immediate.level || 0) + "
";
}
+ put("levels", h);
- // 中断栈(栈顶在上 → 用 column-reverse)
- html += '
' + __("中断栈(栈顶在上)", "Interrupt stack (top first)") + "
";
+ // ---- 段 3:中断栈 ----
+ h = '
' + __("中断栈(栈顶在上)", "Interrupt stack (top first)") + "
";
if (frames.length) {
- html += '
';
+ h += '
';
frames.forEach(function (f, i) {
var t = (f && f.task) || {};
- html += '
' + escHtml(t.kind || "") + " #" + (t.id || "?") +
+ h += '
' + escHtml(t.kind || "") + " #" + (t.id || "?") +
'L' + (t.level || 0) +
(i === frames.length - 1 ? " · " + __("栈顶", "top") : "") + "
";
});
- html += "
";
+ h += "
";
} else {
- html += '
' + __("中断栈为空(当前无被抢占的现场)", "stack empty (nothing preempted)") + "
";
+ h += '
' + __("中断栈为空(当前无被抢占的现场)", "stack empty (nothing preempted)") + "
";
}
if (sc.running) {
- html += '
' + __("正在运行", "running") + ":" +
+ h += '
' + __("正在运行", "running") + ":" +
escHtml(sc.running.kind || "") + " #" + sc.running.id + " (L" + (sc.running.level || 0) + ")
";
}
+ put("stack", h);
- // 通道**分配(按归属)**:inputch 是输入路由单位,而登记表由根 agent 与
- // 驻留子**共用同一份**——所以“这条输入归谁”必须画出来。只画设备能力
- // (下面的拓扑)等于把“路由发生在进内核之前”这条设计事实藏起来,
- // 于是驻留子的通道分配在界面上完全不可见。
- html += '
' + __("通道分配(按归属)", "Input channels by owner") + "
";
+ // ---- 段 4:通道分配(按归属)----
+ //
+ // inputch 是输入路由单位,登记表由根 agent 与驻留子**共用同一份**——
+ // 「这条输入归谁」必须画出来。只画设备能力(段 5)等于把「路由发生在
+ // 进内核之前」这条设计事实藏起来,驻留子的通道分配就完全不可见。
+ h = '
' + __("通道分配(按归属)", "Input channels by owner") + "
";
var maxCap = 1;
inputs.forEach(function (c) {
if ((c.capacity || 0) > maxCap) maxCap = c.capacity;
@@ -748,8 +794,8 @@
}
byOwner[o].push(c);
});
- // 驻留子即使一条 inputch 也没划到,也要出现在分配图里——
- // 否则“子存在但界面上看不见”与“子不存在”无法区分。
+ // 驻留子即使一条 inputch 都没划到也要出现在图里——否则「子存在但看不见」
+ // 与「子不存在」无法区分。
residents.forEach(function (r) {
var o = r.id || "";
if (o && !byOwner[o]) {
@@ -763,7 +809,7 @@
return a < b ? -1 : 1;
});
if (!owners.length) {
- html += '
' + __("暂无通道登记", "no channel registered") + "
";
+ h += '
' + __("暂无通道登记", "no channel registered") + "
";
}
owners.forEach(function (o) {
var list = byOwner[o] || [];
@@ -773,8 +819,8 @@
if (r.id === o) res = r;
});
}
- html += '
';
- html += '
' +
+ h += '';
+ h += '
' +
(o ? "▸ " : "◆ ") +
(o ? __("驻留子 ", "resident ") + escHtml(o) : __("根 agent / 内核默认", "root agent / kernel default")) +
'' + list.length + " " + __("条通道", "channels") +
@@ -784,7 +830,7 @@
if (list.length) {
list.forEach(function (c) {
var cap = c.capacity || 0;
- html += '' +
+ h += '
' +
'
' + escHtml(c.name) + "" +
(c.plugin ? '
' + escHtml(c.plugin) + "" : "") +
rtSlider(cap, maxCap, __("容量", "cap"), cap ? String(cap) : __("默认", "default")) +
@@ -794,18 +840,19 @@
});
} else {
var allowed = (res && res.allowed_outputs) || [];
- html += '
' +
+ h += '
' +
(allowed.length
? __("可发往输出通道:", "allowed outputs: ") +
allowed.map(function (x) { return '' + escHtml(x) + ""; }).join("")
: __("未划入任何 inputch", "no input channel assigned")) +
"
";
}
- html += "
";
+ h += "
";
});
+ put("owners", h);
- // 通道拓扑(设备能力面:能收什么、能发什么、有哪些工具)
- html += '
' + __("通道拓扑", "Channel topology") + "
";
+ // ---- 段 5:通道拓扑(设备能力面)----
+ h = '
' + __("通道拓扑", "Channel topology") + "
";
var ins = channels.filter(function (c) { return c.direction === "in" || c.direction === "io"; });
var outs = channels.filter(function (c) { return c.direction === "out" || c.direction === "io"; });
function chanHtml(c) {
@@ -816,17 +863,14 @@
escHtml((c.tools || []).length ? (c.tools || []).length + " " + __("个工具", "tools") : (c.description || "").slice(0, 26)) +
"
";
}
- html += '
';
- html += '
' + (ins.length ? ins.map(chanHtml).join("") : '
' + __("无输入通道", "no input channel") + "
") + "
";
- html += '
' + __("内核", "Kernel") + "
";
- html += '
' + (outs.length ? outs.map(chanHtml).join("") : '
' + __("无输出通道", "no output channel") + "
") + "
";
- html += "
";
-
- html += "
";
- el.innerHTML = html;
+ h += '';
+ h += '
' + (ins.length ? ins.map(chanHtml).join("") : '
' + __("无输入通道", "no input channel") + "
") + "
";
+ h += '
' + __("内核", "Kernel") + "
";
+ h += '
' + (outs.length ? outs.map(chanHtml).join("") : '
' + __("无输出通道", "no output channel") + "
") + "
";
+ h += "
";
+ put("topo", h);
}
-
// loadRuntime 拉运行态小快照并就地重绘面板(约 2KB,可秒级轮询)。
async function loadRuntime() {
try {