diff --git a/README.md b/README.md
index ecb5410..34a3227 100644
--- a/README.md
+++ b/README.md
@@ -42,3 +42,7 @@ v1.1.0 已与 Console v2.1.0 部署,真实树莓派在线并开始记录历史
当前生产实时指标统一 4 秒读取,实测及版本 v1.3.2 见 [调优记录](DEPLOYMENT-v1.3.2-2026-10-15.md)。
当前生产 v1.3.3 的指标用词、状态与缺失百分比统一见 [部署记录](DEPLOYMENT-v1.3.3-2026-10-15.md)。
+
+## Console UI 语义统一
+
+本次界面需要 Console v2.7.30 的公共语义模块:在线、运行中使用绿色,健康正常使用蓝色,警告/故障/未知分别使用黄/红/灰色;状态采用14px/20px纯文字。完整规则见相邻 Console 项目的 `docs/UI_SEMANTICS.md`。
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6cc5772..73ba8f8 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,7 @@
-# pigway-console-adapter-raspberry-pi v1.6.7
+# pigway-console-adapter-raspberry-pi v1.6.8
-统一首页卡片布局、管理入口及图标间距。详见 docs/HOME-CARD-ORDER-2026-10-17.md。
+合并主要告警与 OLED/RGB 展示,在线状态右对齐,保留系统配置插件管理;统一现有状态与折叠展示。
-验证:线上 1440/820/390px 卡片顺序、NAS 温度整合、管理链接、无横向溢出与 JS 错误。
+验证:适配器单元测试及相关桌面、平板、手机浏览器检查通过。
+
+发布包可供安装;发布不自动升级生产已安装适配器。树莓派合并展示已单独热更新,Ubuntu/NAS 隐藏空告警块尚未部署。
diff --git a/SHA256SUMS b/SHA256SUMS
index 1637f38..20da690 100644
--- a/SHA256SUMS
+++ b/SHA256SUMS
@@ -1,17 +1,31 @@
3cfd361f7663a9a9c93b507d17bb1c7e98003d61316826f0a4ba4a93594f72d4 .gitignore
-1f960195978886b318953a6dbc7f13a551d9bb2bab44aa88b1599c69c8a9ced5 README.md
+8ef77455daa560a7f25a1d8a4684c9e8ce9232745d844b0e3703c7f57830c85f DEPLOYMENT-v1.2.0-2026-10-14.md
+176127d6a7c5abf61112ea019ccc8f62819d1b94c5084ae51b99cbe0369cc22f DEPLOYMENT-v1.2.1-2026-10-14.md
+d4938f4fb971fb665e377ba966b523250401138cf7e1186cb57da55fbf904f3b DEPLOYMENT-v1.2.2-2026-10-14.md
+ec04c97e0271455a41328b8d68e8e4a5d0e3ef5782ddb8fc62a08b7bfff2ef9d DEPLOYMENT-v1.3.0-2026-10-15.md
+136db6c02d7181085f18c54bf1ca2b0bc56e3aa43e3a71a0e59926fc7fa7b18f DEPLOYMENT-v1.3.2-2026-10-15.md
+d0870015c81d16af20cf38b775fa95b5ac1619cc5144fcda5007cb2f832e0e33 DEPLOYMENT-v1.3.3-2026-10-15.md
+552b66fe8946ea96b12633fcf705c71b14d32679bd57ddd39fd5c4cc8910f1b2 README.md
+c7432d981ad0a0bc66cdc6e1915620df60d9e31be4da67ad19dd32ceca92d884 RELEASE_NOTES.md
+a669ccb22bf3aafec6fa558fd84108dca013cb8dc2988206b7b5fe58a1eb2035 STORAGE-DISCOVERY.md
+0cf93f3b3d3219ab8229b59e98cbd82e42647a461231f7c767ea3d120f8e0910 docs/AUDIT-FIXES-2026-10-16.md
+1eec71f194bfdd51b6ae5a9063a0760ccf24b46474901defdf19503a680b8fcd docs/HOME-CARD-ORDER-2026-10-17.md
+b47aafb725706a03837aceabd1ac59b302ead066bf12bf947326a98cb38963ef docs/OVERVIEW-DISCLOSURES-2026-10-16.md
+f0df601aafe839cfe06c069471071023158beced81c5d0b802fa0bb4ed3528bc docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md
+d3528d8fd4eed197942e2c9a72d62e6c9ff6b3ab00f4be98c337b6e306d6e177 docs/STATUS-ALIGNMENT-2026-10-16.md
d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py
-e5c04001dbd8ed8bba614f7b341237fc6487a9a8fe2f28ccb6f59278725b0a69 plugin/ui/config.js
+7e985bc8ce58c9d83b6e61c5a47e123856f57281c23a389750471f988c13699e plugin/manifest.json
+d6a342605426357da7d2e65daa435e99d7234549cffa52e1a9229ef5bc36f33c plugin/ui/config.js
c98f34119436e3020ef20b325f91617943aefe662119cf76738e0226c2e91cbb plugin/ui/device.js
89b9d7994e7477e18c3f43e6f0975e04d5106056ba581b2964db0f39183d6f8a plugin/ui/hardware.js
af631694fcd7da21b77b554e6ee1bf2eaa93bd46a0b8b543e367f032d3236ecb plugin/ui/index.js
+b1d50c4ba896905b3c3ee8808072dfb41c04486b9364a5f0f49377059e280a78 plugin/ui/overview.js
422e1f8323d29a4be757f39e8b60addd7764c436a1302673809e0c547fce5ff4 plugin/ui/schema.js
-5d967b5d4e62eed7022bc04fe29e4ed670f2e288cf20adae0c25233f94a29079 plugin/ui/services.js
-56e67d2839cbb865133e3b5f796a07f36595c0f6c30480f8e2fac79e7d51e03f plugin/ui/styles.css
+1bee0f9e5656a52bc97d9fafffbf5157dae062888f618046ad09470dc2685f3c plugin/ui/services.js
+6806df689f62de56f7b61f8b3cf3a33c494a09931a14f51d9d71c26d2c1f61ee plugin/ui/storage.js
+061f5011b78cedcd0c69a32aa5106889d38f6863f4c613ae326cc9d16f90e2dd plugin/ui/styles.css
0b79b9c5b5f23727164e05b127caca72255416309166ba200ccbd15a9667473c plugin/ui/translations.js
-0c37e4604a30c13923b45316d21c841164c48e8b632c3a3f8febd778b434c2f8 plugin/worker.py
-9ffd557bb01ccda24970af1cfc5ec395721d5b1e613bfc013feb27d074582b7e tests/test_metrics.py
-c7c5733264f0f9b00fbd4d13d5891b477d0e1f477e766db29da23efe6626e610 plugin/ui/overview.js
-5b1be269706fb8d0f269c99941d36017f1d451f62960443c7d5d14a30cda708a plugin/manifest.json
-1eec71f194bfdd51b6ae5a9063a0760ccf24b46474901defdf19503a680b8fcd docs/HOME-CARD-ORDER-2026-10-17.md
-ee80ad795c189f136bfa41ac235cf8d2951e8e362f7d12b3e47825982a9a9fe9 RELEASE_NOTES.md
+d5b22366a2c7b375e6f3e35cefb9f95da0ea3b8390b3ec4d1000492f00128f6e plugin/worker.py
+22c913e75736b78c54d90a60c7a2c8f739467dc7f8ed3f46b71be28e4bc3dec5 tests/browser_alert_semantics.cjs
+40c038aa7689c127c0d90ddf390674f534a2bdee8fd3c344d09f8fb6d4bb3964 tests/test_metrics.py
+40d10555aef08f8132f576976b7205b3e1c16d330196a11c6cc1c5cbc167c670 tests/test_storage_metrics.py
diff --git a/docs/AUDIT-FIXES-2026-10-16.md b/docs/AUDIT-FIXES-2026-10-16.md
new file mode 100644
index 0000000..36df0ce
--- /dev/null
+++ b/docs/AUDIT-FIXES-2026-10-16.md
@@ -0,0 +1,5 @@
+# 2026-10-16 主要告警展示(1.6.2)
+
+概览把 Agent owner 的标题从“显示状态”改为“主要告警”,明确告警按严重程度排序、实际屏幕见硬件插件。避免未连接硬件或 OLED 轮播时把 Agent 告警摘要误认为屏幕实况。
+
+原四项 Python 测试通过;tests/browser_alert_semantics.cjs 验证中英文及桌面、平板、手机布局,使用真实组件及 CSS、隔离硬件和网络依赖。运行需 PLAYWRIGHT_MODULE 指向可用 Playwright;PIGWAY_WEB_ROOT 可指定控制台 web 目录。已安装至生产控制台 2.7.23,API 确认版本 1.6.2,设备在线与显示偏好保持一致;备份:`/volume1/homes/way/pigway-console-log-zh-backup-1792125044`。
diff --git a/docs/OVERVIEW-DISCLOSURES-2026-10-16.md b/docs/OVERVIEW-DISCLOSURES-2026-10-16.md
new file mode 100644
index 0000000..12c8fb7
--- /dev/null
+++ b/docs/OVERVIEW-DISCLOSURES-2026-10-16.md
@@ -0,0 +1,5 @@
+# 概览折叠区块 1.6.3
+
+“服务与进程监控”或“已读取范围内的告警”采用默认收起的 details/summary,沿用 SMART 展开样式。data-overview-details 配合 Console 2.7.24 保留每台设备在自动刷新中的展开状态。内容和监控行为保持不变。
+
+已部署至生产;中英文、桌面/平板/手机验收通过。部署回执及验证详见控制台 docs/OVERVIEW-DISCLOSURES-2026-10-16.md。
diff --git a/docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md b/docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md
new file mode 100644
index 0000000..9bdec88
--- /dev/null
+++ b/docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md
@@ -0,0 +1,3 @@
+# 概览汇总标签
+
+折叠标题增加正常/异常数量/未确认汇总,暂无监控不视为正常。共享 Console 2.7.25 overview-health.js 判定,过期数据不显示绿色正常。已部署,中英文桌面/平板/手机及各状态模拟验收通过。完整回执见控制台 docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md。
diff --git a/docs/STATUS-ALIGNMENT-2026-10-16.md b/docs/STATUS-ALIGNMENT-2026-10-16.md
new file mode 100644
index 0000000..25445c7
--- /dev/null
+++ b/docs/STATUS-ALIGNMENT-2026-10-16.md
@@ -0,0 +1,3 @@
+# 概览状态纯文字展示
+
+汇总调用改为 summaryStatus,配合 Console 2.7.26 的统一纯文字与对齐规则。已部署;中英文、深浅主题、桌面/平板/手机及长文本实际展开测量均通过。判断逻辑不变。详见控制台 docs/STATUS-ALIGNMENT-2026-10-16.md。
diff --git a/plugin/manifest.json b/plugin/manifest.json
index 666e23a..d417da4 100644
--- a/plugin/manifest.json
+++ b/plugin/manifest.json
@@ -1,7 +1,7 @@
{
"id": "pigway-agent",
"name": "PIGWay Raspberry Pi Agent",
- "version": "1.6.7",
+ "version": "1.6.8",
"protocol_version": 1,
"entrypoint": "worker.py",
"runtime": "python3",
diff --git a/plugin/ui/config.js b/plugin/ui/config.js
index dc5949c..5565553 100644
--- a/plugin/ui/config.js
+++ b/plugin/ui/config.js
@@ -1,3 +1,4 @@
+import { runtimeStatus } from "/assets/js/ui-semantics.js";
import { state as appState } from "/assets/js/state.js";
import { addStorageSettings } from "./storage.js";
import { renderDevice } from "./device.js";
@@ -60,7 +61,7 @@ export function renderProcessList() {
"processList",
appState.processCache.map(
(p) =>
- `
${esc(p.name)} ${tr(p.up ? "正常" : "停止")}
${esc(p.target)}
`,
+ `${esc(p.name)} ${runtimeStatus(p.up, appState.LANG)}
${esc(p.target)}
`,
),
pluginText("尚未监控任何普通进程", "No process monitors configured"),
);
diff --git a/plugin/ui/overview.js b/plugin/ui/overview.js
index 16646fc..5a72e46 100644
--- a/plugin/ui/overview.js
+++ b/plugin/ui/overview.js
@@ -1,15 +1,17 @@
+import { runtimeStatus } from "/assets/js/ui-semantics.js";
+import { monitorHealth, summaryStatus, dataIsCurrent } from "/assets/js/overview-health.js";
import { state as appState } from "/assets/js/state.js";
import { storageCards } from "./storage.js";
import { esc } from "/assets/js/shared.js";
import { tr } from "/assets/js/i18n.js";
import { bytes, formatUptime, meter } from "/assets/js/overview.js";
-import { fanMetric, pluginSummary } from "./hardware.js";
-import { ownerName } from "./schema.js";
+import { fanMetric } from "./hardware.js";
+import { ownerName, rgbName } from "./schema.js";
export function agentHostCard(entry) {
const h = entry.host;
if (!entry.online)
- return `${esc(h.name || h.id)}
ID ${esc(h.id)}${esc(h.url || tr("本机"))}
${tr("离线")}${esc(entry.error || tr("连接失败"))}
`;
+ return `${esc(h.name || h.id)}
ID ${esc(h.id)}${esc(h.url || tr("本机"))}
${tr("离线")}${esc(entry.error || tr("连接失败"))}
`;
const s = entry.status,
y = s.system || {},
n = s.network || {},
@@ -21,7 +23,17 @@ export function agentHostCard(entry) {
memTotal = y.memory_total_bytes,
diskUsed = y.disk_used_bytes,
diskTotal = y.disk_total_bytes;
+ const hardware = Object.values(s.plugins || {}).filter(p => p.capabilities?.display || p.capabilities?.rgb);
+ const current = dataIsCurrent(entry);
+ const ready = p => current && p.online === true && p.status?.available !== false;
+ const oled = hardware.find(p => p.capabilities?.display && ready(p) && p.status?.oled_page);
+ const rgb = hardware.find(p => p.capabilities?.rgb && ready(p) && p.status?.rgb_mode);
+ const displayText = [oled ? `OLED ${ownerName(oled.status.oled_page)}` : ownerName(d.owner),
+ rgb ? `RGB ${rgbName(rgb.status.rgb_mode)}` : ""].filter(Boolean).join(" · ");
+ const hardwareState = !hardware.length ? "unlinked" : !current ? "unknown" : hardware.some(p => p.online === false) ? "offline" : hardware.every(ready) ? "online" : "unknown";
+ const hardwareLabel = { online: tr("在线"), offline: tr("离线"), unknown: appState.LANG === "zh" ? "未确认" : "Unconfirmed", unlinked: appState.LANG === "zh" ? "未接入" : "Not connected" }[hardwareState];
+ const hardwareNames = hardware.map(p => p.name).filter(Boolean).join(" · ");
const percent = n => Number.isFinite(n) ? `${n.toFixed(1)}%` : "--";
const progress = n => Number.isFinite(n) ? meter(n) : "";
- return `${esc(m.name || h.name || m.id)}
${esc(formatUptime(s.uptime_seconds))}ID ${esc(m.id || h.id)}${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}${esc(v.manufacturer || "")} ${esc(v.model || "--")}${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}${esc(v.os || "--")}${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}
${tr("在线")}${tr("处理器负载")}
${percent(y.cpu_percent)}
${progress(y.cpu_percent)}
${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}
${tr("内存占用")}
${percent(y.memory_percent)}
${progress(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}
${tr("主网络")}
↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s
${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.link === true ? (appState.LANG === "zh" ? "已连接" : "Connected") : n.link === false ? (appState.LANG === "zh" ? "已断开" : "Disconnected") : (appState.LANG === "zh" ? "状态未确认" : "Unconfirmed")}
${Array.isArray(y.storage_inventory) ? storageCards(y,entry.snapshot?.resources) : `
${tr("磁盘占用")}
${percent(y.disk_percent)}
${progress(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}
`}
${tr("显示状态")}
${esc(ownerName(d.owner))}
${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}
${tr("当前告警")}
${(d.alerts || []).length}
${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}
${pluginSummary(s.plugins || {}, h.id)}${tr("服务与进程监控")}
${(s.watches || []).map((w) => `
${esc(w.name)} · ${esc(w.target)}${tr(w.up ? "正常" : "停止")}
`).join("") || `
${tr("暂无数据")}
`}
`;
+ return `${esc(m.name || h.name || m.id)}
${esc(formatUptime(s.uptime_seconds))}ID ${esc(m.id || h.id)}${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}${esc(v.manufacturer || "")} ${esc(v.model || "--")}${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}${esc(v.os || "--")}${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}
${tr("在线")}${tr("处理器负载")}
${percent(y.cpu_percent)}
${progress(y.cpu_percent)}
${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}
${tr("内存占用")}
${percent(y.memory_percent)}
${progress(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}
${tr("主网络")}
↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s
${esc(n.interface || "--")} · ${n.kind === "WIF" ? `${esc(n.metric)} dBm · ` : n.link_speed_mbps ? `${esc(n.link_speed_mbps)} Mbps · ` : ""}${esc(n.ip_label)} ${esc(n.ip)} · ${n.link === true ? (appState.LANG === "zh" ? "已连接" : "Connected") : n.link === false ? (appState.LANG === "zh" ? "已断开" : "Disconnected") : (appState.LANG === "zh" ? "状态未确认" : "Unconfirmed")}
${Array.isArray(y.storage_inventory) ? storageCards(y,entry.snapshot?.resources) : `
${tr("磁盘占用")}
${percent(y.disk_percent)}
${progress(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}
`}
${appState.LANG === "zh" ? "主要告警" : "Primary alert"}${esc(hardwareLabel)}
${esc(displayText)}
${hardwareNames ? `
${esc(hardwareNames)}
` : ""}
${(d.alerts || []).length || !dataIsCurrent(entry) || !Array.isArray(d.alerts) ? `
${tr("当前告警")}
${(d.alerts || []).length}
${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}
` : ""}
${tr("服务与进程监控")}${summaryStatus(monitorHealth(s.watches, dataIsCurrent(entry)), appState.LANG)}
${(s.watches || []).map((w) => `
${esc(w.name)} · ${esc(w.target)}${runtimeStatus(w.up, appState.LANG)}
`).join("") || `
${tr("暂无数据")}
`}
`;
}
diff --git a/plugin/ui/services.js b/plugin/ui/services.js
index 61bbb37..5106f58 100644
--- a/plugin/ui/services.js
+++ b/plugin/ui/services.js
@@ -1,5 +1,5 @@
import { state as appState } from "/assets/js/state.js";
-import { $, esc } from "/assets/js/shared.js";
+import { pluginText as text, $, esc } from "/assets/js/shared.js";
import { get, rows } from "/assets/js/navigation.js";
import { loadConfig } from "/assets/js/settings.js";
@@ -31,7 +31,7 @@ export function renderServiceLists() {
"systemServices",
filtered.map(
(s) =>
- `${esc(s.unit)}
${esc(s.description || "暂无服务说明")} · ${esc(s.active === "active" ? "正在运行" : "未运行")}${s.monitored ? (s.paused ? "已暂停" : "已监控") : "未监控"}
`,
+ `${esc(s.unit)}
${esc(s.description || "暂无服务说明")} · ${esc(s.active === "active" ? text("运行中", "Running") : s.active === "inactive" || s.active === "failed" ? text("已停止", "Stopped") : text("未确认", "Unconfirmed"))}${s.monitored ? (s.paused ? "已暂停" : "已监控") : "未监控"}
`,
),
"没有匹配的服务",
);
@@ -40,7 +40,7 @@ export function renderServiceLists() {
"monitoredServices",
monitored.map(
(s) =>
- `${esc(s.monitor_name)} ${s.paused ? "监控已暂停" : s.active === "active" ? "运行中" : "服务已停止"}
${esc(s.unit)}
备注:${esc(s.note || "未填写")}
`,
+ `${esc(s.monitor_name)} ${s.paused ? text("监控已暂停", "Monitor paused") : s.active === "active" ? text("运行中", "Running") : s.active === "inactive" || s.active === "failed" ? text("已停止", "Stopped") : text("未确认", "Unconfirmed")}
${esc(s.unit)}
备注:${esc(s.note || "未填写")}
`,
),
"尚未监控任何服务",
);
diff --git a/plugin/ui/styles.css b/plugin/ui/styles.css
index 8b9a631..e74aedf 100644
--- a/plugin/ui/styles.css
+++ b/plugin/ui/styles.css
@@ -52,3 +52,7 @@
grid-row: 1 / span 3;
}
}
+
+/* Hardware display status belongs to the primary-alert metric. */
+.pi-alert-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.pi-alert-heading .semantic-status { flex-shrink: 0; }
diff --git a/tests/browser_alert_semantics.cjs b/tests/browser_alert_semantics.cjs
new file mode 100644
index 0000000..154c3ab
--- /dev/null
+++ b/tests/browser_alert_semantics.cjs
@@ -0,0 +1,37 @@
+const {chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');
+const fs=require('fs');const path=require('path');
+const adapter=path.resolve(__dirname,'..');
+const web=process.env.PIGWAY_WEB_ROOT || path.resolve(adapter,'../pigway-web-manager/web');
+const output=adapter+'/.data/audit-validation';fs.mkdirSync(output,{recursive:true});
+(async()=>{const browser=await chromium.launch({headless:true});const page=await browser.newPage();let errors=[];page.on('pageerror',e=>errors.push(e.message));
+await page.route('http://audit.local/**',async route=>{const u=new URL(route.request().url());let body,type='text/javascript';
+if(u.pathname==='/'){type='text/html';body=``;}
+else if(u.pathname==='/assets/js/state.js')body='export const state={LANG:"zh"};';
+else if(u.pathname==='/assets/js/i18n.js')body='export const tr=x=>x;';
+else if(u.pathname==='/assets/js/overview.js'){let source=fs.readFileSync(web+'/js/overview.js','utf8');body='import {state as appState} from "/assets/js/state.js";\n'+source.slice(source.indexOf('export function bytes'),source.indexOf('export const connectionLabels')); }
+else if(u.pathname==='/plugin/hardware.js')body='export const fanMetric=()=>"";export const pluginSummary=()=>"";';
+else {let file=u.pathname.startsWith('/plugin/')?adapter+'/plugin/ui/'+u.pathname.slice(8):web+'/'+u.pathname.replace('/assets/','');if(!fs.existsSync(file))throw Error(file);body=fs.readFileSync(file);if(file.endsWith('.css'))type='text/css';}
+await route.fulfill({status:200,contentType:type,body});});
+await page.goto('http://audit.local/');await page.waitForFunction(()=>typeof window.render==='function');
+for(const [name,width,height] of [['desktop',1440,1000],['tablet',768,1024],['mobile',390,844]]){await page.setViewportSize({width,height});for(const lang of ['zh','en']){await page.evaluate(lang=>render(lang),lang);await page.waitForTimeout(100);const text=await page.locator('body').innerText();if(!text.includes(lang==='zh'?'主要告警':'Primary alert'))throw Error('Missing label');if(text.includes('显示状态'))throw Error('Old label');if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth))throw Error('Overflow '+name);await page.screenshot({path:output+'/'+name+'-'+lang+'.png',fullPage:true});}}
+const hardware={hat:{name:'Yahboom RGB Cooling HAT',online:true,capabilities:{display:true,rgb:true},status:{available:true,oled_page:'NORMAL_HOME',rgb_mode:'OFF'}}};
+for(const width of [1440,768,390]){
+ await page.setViewportSize({width,height:1000});
+ await page.evaluate(p=>render('zh',p),hardware);
+ const primary=page.locator('[data-refresh-group="display"]');
+ if((await primary.locator('.value').innerText())!=='OLED 正常主页 · RGB 全灭')throw Error('Merged OLED/RGB text');
+ if((await primary.locator('.pi-alert-heading .semantic-status').innerText())!=='在线')throw Error('Missing hardware online status');
+ if(await page.locator('[data-manage-plugin-host]').count())throw Error('Overview must not expose plugin management');
+ if(await page.locator('.group.action-row').count())throw Error('Standalone hardware row remains');
+ if(!(await primary.innerText()).includes('Yahboom RGB Cooling HAT'))throw Error('Hardware name missing');
+ if(!(await primary.locator('.pi-alert-heading').evaluate(n=>Math.abs(n.lastElementChild.getBoundingClientRect().right-n.getBoundingClientRect().right)<1)))throw Error('Hardware status is not right aligned');
+ if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth))throw Error('Merged display overflow');
+ await page.screenshot({path:output+'/merged-'+width+'.png',fullPage:true});
+}
+hardware.hat.online=false;
+await page.evaluate(p=>render('zh',p),hardware);
+if((await page.locator('.pi-alert-heading .semantic-status').innerText())!=='离线')throw Error('Offline hardware shown online');
+if((await page.locator('[data-refresh-group="display"] .value').innerText()).includes('RGB 全灭'))throw Error('Stale RGB shown as current');
+await page.evaluate(()=>render('zh',{}));
+if((await page.locator('.pi-alert-heading .semantic-status').innerText())!=='未接入')throw Error('Absent hardware shown online');
+if(errors.length)throw Error(errors.join('\n'));console.log('PASS: merged hardware display; online/offline/absent state; plugin management removed from overview; desktop/tablet/mobile; Chinese/English; no overflow or browser errors');await browser.close();})().catch(e=>{console.error(e);process.exit(1)});