Compare commits

...
7 Commits
26 changed files with 247 additions and 150 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Verify
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
# Fork code may run on GitHub-hosted runners, never the trusted Gitea runner.
if: github.server_url == 'https://github.com' || github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.13'
- run: python -m unittest discover -s tests -v
- run: git diff --check
+2
View File
@@ -3,3 +3,5 @@ __pycache__/
dist/
.DS_Store
.data/
.pytest_cache/
.ruff_cache/
+9
View File
@@ -0,0 +1,9 @@
# 贡献指南 / Contributing
安装和运行步骤见项目首页;使用 Python 3.13 或 3.14,修改保持小而集中。运行 `python3 -m unittest discover -s tests -v`,Shell 脚本使用 `bash -n`,提交前运行 `git diff --check`。
测试只能使用临时目录、模拟服务和设备桩;不得访问真实设备、执行生产写入或提交密钥、令牌、数据库、照片及部署回执。真实设备验证由维护者另行执行。
请通过仓库 Pull Request 提交,说明问题、行为变化和验证结果。行为修复应覆盖对应回归风险。新增代码按本项目 MIT 许可证提供,保留第三方许可及来源。
See README.md for installation. Keep changes focused, test with mocks, and submit a pull request describing the behavior and checks. Never include credentials or production data. Contributions use the MIT license; preserve third-party notices.
-5
View File
@@ -1,5 +0,0 @@
# 树莓派接入插件 v1.2.0
2026-10-14 已部署。主网络卡片主文字改为“有线网络 · 链路速率”;接口/IP/状态与上下行速率独立两行,与群晖 v1.4.0 一致。Wi-Fi 信号与告警语义保留。Agent v3.8.0 提供 rx_bytes_per_second/tx_bytes_per_second,映射到标准历史指标;旧 Agent 缺少数值时不伪造速率,非数值、布尔和非有限值不写入历史。两项指标回归测试通过,核心全页三尺寸、中英文主题浏览器回归通过;真实生产快照额外验证两处网络卡片三尺寸与两语言,桌面截图已查看。
生产实际插件 1.2.0、Agent 3.8.0,两设备在线,树莓派上下行采集与标准历史指标通过认证 API 核验,临时会话删除。停服一致性备份 `/volume1/homes/way/pigway-console-backup-network-align-20261014/data.tar.gz`,目录 0700;旧插件在数据卷 adapter-backups/network-align-20261014。Console 镜像保持原版。
-5
View File
@@ -1,5 +0,0 @@
# 网络卡片 v1.2.1
2026-10-14 已部署。网络主文字改为下载/上传速率,删除 ETH/有线网络文字;下方保留 eth0、1000 Mbps、IP 和状态,不重复显示速率。两处网络卡片使用一致布局。模拟及真实设备快照三尺寸、中英文浏览器检查通过,无溢出,桌面截图已查看。生产认证 API 确认两设备在线、树莓派实时流量与历史标准指标有效。Agent 保持 v3.8.0,未再次升级。
停服一致性数据备份 /volume1/homes/way/pigway-console-backup-network-rates-layout-20261014/data.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/network-rates-layout-20261014。Console 镜像及 NAS 每日存储采集保持。
-5
View File
@@ -1,5 +0,0 @@
# 树莓派接入插件 v1.2.2
2026-10-14 网络卡片改为按 network.link 显示已连接/已断开,字段缺失显示状态未确认,与 NAS 的接口连接语义一致。页面不再把 network.internet 用作接口状态文案。Agent 的既有互联网探测和告警逻辑不修改。两项后端测试及全页三尺寸中英文主题浏览器验证通过。部署使用停服一致性数据备份 /volume1/homes/way/pigway-console-backup-synology-vpi-link-v1.2.2-20261014/data.tar.gz,目录 0700;旧树莓派插件保存在数据卷 adapter-backups/vpi-link-v1.2.2/pigway-agent。
生产安装并启用 1.2.2,实际 UI 已使用 network.link 且不存在“互联网正常”文案;认证 API 两设备在线,实时流量和历史指标保留。验证会话删除。Tea ZIP 与 SHA256 附件下载校验一致。
-5
View File
@@ -1,5 +0,0 @@
# pigway-console-adapter-raspberry-pi v1.3.0
2026-10-15 已与 Console v2.4.0 部署。声明每秒缓存读取并透传 Agent 的 system/network 真实采样时间。树莓派网络仍每 5 秒采样,历史去重验证通过。
生产两设备在线。认证 API 连续观测确认树莓派缓存每秒、网络 5 秒、NAS 实时约 10 秒;最近三个网络 10 秒历史桶各 2 个样本,没有每秒重复入库。Console 保留原名称、证书、存储缓存和一年历史策略。部署详情见 Console [生产记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.4.0-2026-10-15.md)。
-7
View File
@@ -1,7 +0,0 @@
# pigway-console-adapter-raspberry-pi v1.3.2 实测调优
2026-10-15 生产调优结论:统一 4 秒读取 CPU、内存、温度及网络速率。曾试 2 秒,但这台 DSM 的 utilization 实际采样时间通常每 4 秒更新,连续细粒度观察 CPU 时间为 7853/7857/7861/7865 等相差 4 秒的样本;不能把两次相同样本当成更快采样。只读接口测量 System.info 约 0.05–0.07 秒、Utilization.get 约 1.03–1.09 秒,请求耗时与真实样本周期不同。
最终 40 秒连续观测:两设备各 10 个独立实时样本、10 个网络样本,中位间隔均 4 秒;无离线或缺失采集组,缓存 API 最大耗时 0.032 秒(这是 Console 缓存读取,不是 DSM 接口耗时)。Pi 本地 CPU 等继续 1 秒,网络配置已从 5 秒改为 2 秒;Console 每 4 秒读取,覆盖更快缓存。NAS 网络配置 60 秒、存储 24 小时保持。
插件生产版本 Pi 1.3.2、NAS 1.6.1,Console 仍 2.4.0、Agent 仍 3.9.0。3 项 Pi 回归和 14 项 NAS 回归通过。停服一致性数据备份 /volume1/homes/way/pigway-console-backup-balanced-4s-20261015/data.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/balanced-4s-20261015。Pi 原配置备份 /home/way/pigway-timing-backup-20261015.conf,权限 0600。仅保留 Console 原 latest 镜像,没有新增镜像。
-7
View File
@@ -1,7 +0,0 @@
# v1.3.3 指标语义统一
2026-10-15 已部署。运行状态与历史趋势统一处理器负载、内存占用、磁盘占用及英文名称;主网络英文统一 Primary network,无告警与状态未确认提示一致。缺失百分比显示 --,不显示孤立百分号或假零值进度条。温度、内存可用量/存储剩余量按数据来源保留,不改用户资源名称。
全页、历史五范围和三尺寸中英文主题浏览器验证通过;真实设备公开快照在本地渲染两处卡片,三尺寸两语言名称检查和缺失读数检查通过。生产认证 API 核对 Console 2.4.1、Pi 插件 1.3.3、NAS 插件 1.6.2,两设备在线、原名称保持,4 秒声明不变,验证会话删除。生产实际 UI 和历史名称代码核对通过。
停服一致性备份 /volume1/homes/way/pigway-console-backup-v2.4.1-20261015/{program,data}.tar.gz,目录 0700;旧插件在数据卷 adapter-backups/semantics-v2.4.1。实际镜像 sha256:127d80530abc728add1eb947e304387f7f941f8fa570386d951c6f9ae6eabc0e,仅保留 latest 标签,候选标签与旧镜像清理。没有主动刷新磁盘。Tea 三个 Release 附件下载校验一致。
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 PIGWay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22 -34
View File
@@ -1,44 +1,32 @@
# PIGWay Raspberry Pi Console Plugin
# PIGWay Raspberry Pi Console Adapter
磁盘呈现统一版本已上线,需要 Console v2.7.0:默认按设备返回顺序,同组支持上移/下移、显示名称及隐藏,所有显示偏好保存到 Console,首页与配置页共用。Agent 的名称和隐藏定义继续作为设备默认值保留。扫描仍只由配置页按钮触发。生产验证见相邻 Console 的 docs/STORAGE-PRESENTATION-v2.7.0-2026-10-15.md。
[简体中文](README.zh-CN.md) | English
当前 v1.6.0 支持统一磁盘显示设置与手动扫描,需要 Console v2.7.0、Agent v3.11.1;新磁盘仅手动发现,已知磁盘容量继续刷新。见 [生产验证](STORAGE-DISCOVERY.md)。
An independently maintained Device Console adapter for the PIGWay Raspberry Pi Agent. It installs no extra process on the Pi and retains adapter ID `pigway-agent` for existing devices.
独立维护的树莓派接入插件:连接现有 PIGWay Raspberry Pi Agent,不在树莓派安装新进程。设备 API、状态转换、配置字段、服务/进程监控、硬件界面及其翻译均由本仓库维护。
## Requirements and installation
依赖 Python 标准库;接入 Console JSON-RPC v1 与可信前端扩展 `console-ui-v1`。插件 ID 保持 `pigway-agent`,已有设备可直接继续使用。
The storage display and manual discovery integration requires Console v2.7.0 and Agent v3.11.1 or later. The shared UI semantics require Console v2.7.30. Runtime uses the Python standard library, JSON-RPC v1 and `console-ui-v1`.
打包:
Build the ZIP below, upload it under Machine management and enable it. Register the Agent API URL and device token. Use HTTPS or a trusted tunnel across untrusted networks. For local development, set `PIGWAY_ADAPTER_PATH` to this repository's absolute `plugin` directory. Production Docker installations keep installed plugins in the persistent Console data volume.
## Features and boundaries
- Host status, hardware integration, services/processes, network metrics and Console-managed history.
- Disk discovery runs only from Storage settings → Scan disks. Known disk capacity continues to refresh; display order, names and visibility use the shared Console settings.
- The adapter retains existing API and service identifiers, including `pigway-pi-control`; a repository rename does not change those interfaces. Other Linux platforms are outside its support claim.
Install only trusted plugins: frontend code shares the page JavaScript environment, and the backend process is not a security sandbox. The ZIP contains the manifest, worker, UI and LICENSE, not Console internals or Git metadata.
See [storage behavior](docs/STORAGE.md), [release notes](RELEASE_NOTES.md), and the [Console UI semantics](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/UI_SEMANTICS.md). Dated deployment records describe historical validation, not a fresh online check.
## Build
Current adapter version: **1.6.11**.
```sh
mkdir -p dist
python3 package.py plugin dist/pigway-agent-1.1.0.zip
python3 package.py plugin dist/pigway-agent-1.6.11.zip
```
在控制中心的机器管理页面安装 ZIP,再启用。开发时可用 `PIGWAY_ADAPTER_PATH=/absolute/path/pigway-console-adapter-raspberry-pi/plugin` 加载源码;Docker 推荐使用持久化数据卷中的安装包。
包根目录只有 manifest、worker 与 ui;打包工具、文档和 Git 元数据不进入插件包。前端扩展通过 Console 的共享状态、请求、翻译与基础显示函数接入,不注入额外安装步骤。只应安装可信插件:前端模块与主页面共享 JavaScript 执行环境,加载失败、同步异常及 Promise 拒绝可降级,但不能隔离死循环或恶意代码;后端独立进程的隔离也不是安全沙箱。
正式仓库:https://tea.pigway.com/way/pigway-console-adapter-raspberry-pi 。首次独立版本为 v1.0.0,需要 Console v2.0.0。与 Console 的集成回归使用后者 tests 下的模拟 Agent,不操作真实设备。
## Naming and compatibility
This project targets Raspberry Pi. Other Linux platforms are not covered by its support claim and may have separately maintained adapters. Repository naming does not change the existing `pigway-agent` adapter ID or `pigway-pi-control` service, configuration and API identifiers.
## v1.1.0 历史指标
将现有 Agent API 的内存/磁盘字节数、开机时长与链路速度补充到标准 metrics,Console v2.1.0 可直接统一记录历史。不需要更新或新增树莓派进程。现有设备端没有上传下载流量计数,因此本版没有实际流量曲线;链路速度仅代表协商速率。
v1.1.0 已与 Console v2.1.0 部署,真实树莓派在线并开始记录历史;设备端 Agent 保持原版本。[部署验收记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.1.0-2026-10-11.md)。
生产已升级至 v1.2.0,网络卡片与上下行历史指标见 [部署记录](DEPLOYMENT-v1.2.0-2026-10-14.md)。
当前网络卡片以实时上下行速率为主文字,详见 [v1.2.1 部署记录](DEPLOYMENT-v1.2.1-2026-10-14.md)。
网络连接文案统一为接口链路状态,见 [v1.2.2](DEPLOYMENT-v1.2.2-2026-10-14.md)。
生产 v1.3.0 与按设备周期采集的核验见 [部署记录](DEPLOYMENT-v1.3.0-2026-10-15.md)。
当前生产实时指标统一 4 秒读取,实测及版本 v1.3.2 见 [调优记录](DEPLOYMENT-v1.3.2-2026-10-15.md)。
当前生产 v1.3.3 的指标用词、状态与缺失百分比统一见 [部署记录](DEPLOYMENT-v1.3.3-2026-10-15.md)。
[Documentation](docs/README.md)
+30
View File
@@ -0,0 +1,30 @@
# PIGWay Raspberry Pi Console Plugin
简体中文 | [English](README.md)
磁盘呈现统一版本已上线,需要 Console v2.7.0:默认按设备返回顺序,同组支持上移/下移、显示名称及隐藏,所有显示偏好保存到 Console,首页与配置页共用。Agent 的名称和隐藏定义继续作为设备默认值保留。扫描仍只由配置页按钮触发。
当前 v1.6.11 支持统一磁盘显示设置与手动扫描,需要 Console v2.7.0、Agent v3.11.1;新磁盘仅手动发现,已知磁盘容量继续刷新。见 [磁盘发现说明](docs/STORAGE.md)。
独立维护的树莓派接入插件:连接现有 PIGWay Raspberry Pi Agent,不在树莓派安装新进程。设备 API、状态转换、配置字段、服务/进程监控、硬件界面及其翻译均由本仓库维护。
依赖 Python 标准库;接入 Console JSON-RPC v1 与可信前端扩展 `console-ui-v1`。插件 ID 保持 `pigway-agent`,已有设备可直接继续使用。
打包:
```sh
mkdir -p dist
python3 package.py plugin dist/pigway-agent-1.6.11.zip
```
在控制中心的机器管理页面安装 ZIP,再启用。开发时可用 `PIGWAY_ADAPTER_PATH=/absolute/path/pigway-console-adapter-raspberry-pi/plugin` 加载源码;Docker 推荐使用持久化数据卷中的安装包。
包根目录包含 manifest、worker、ui 与 LICENSE;打包工具、文档和 Git 元数据不进入插件包。前端扩展通过 Console 的共享状态、请求、翻译与基础显示函数接入,不注入额外安装步骤。只应安装可信插件:前端模块与主页面共享 JavaScript 执行环境,加载失败、同步异常及 Promise 拒绝可降级,但不能隔离死循环或恶意代码;后端独立进程的隔离也不是安全沙箱。
正式仓库:https://tea.pigway.com/way/pigway-console-adapter-raspberry-pi 。首次独立版本为 v1.0.0,需要 Console v2.0.0。与 Console 的集成回归使用后者 tests 下的模拟 Agent,不操作真实设备。
## 命名与兼容性
本项目面向 Raspberry Pi,不承诺支持其他 Linux 平台。仓库名称不改变现有 `pigway-agent` 适配器 ID,以及 `pigway-pi-control` 服务、配置和 API 标识。
[文档目录](docs/README.md)
+3 -3
View File
@@ -1,5 +1,5 @@
# pigway-console-adapter-raspberry-pi v1.6.7
# pigway-console-adapter-raspberry-pi v1.6.11
统一首页卡片布局、管理入口及图标间距。详见 docs/HOME-CARD-ORDER-2026-10-17.md。
统一开源许可证、贡献和安全反馈入口;完善当前版本的文档导航、源码分发和校验和。插件 ZIP 携带许可证。运行功能边界保持项目首页所述。
验证:线上 1440/820/390px 卡片顺序、NAS 温度整合、管理链接、无横向溢出与 JS 错误。
本轮 4 项 Python 测试通过。源码语法、文档入口、打包和校验和检查通过;生产运行状态以实际部署验收为准。
+7
View File
@@ -0,0 +1,7 @@
# 安全反馈 / Security
仅支持当前发布版本。请勿在公开 Issue 中发布密钥、令牌、个人数据或可直接利用的完整漏洞细节。
请通过仓库维护者 [way](https://tea.pigway.com/way) 的私有联系渠道报告,包含受影响版本、复现条件、影响和最小化示例。若尚未建立私有渠道,先在 Issue 请求私下联系,不附敏感细节。
Only the current release is supported. Contact the maintainer privately before disclosing exploit details; include the version, impact and a minimal reproduction. Do not attach credentials or private data to public issues.
+21 -11
View File
@@ -1,17 +1,27 @@
3cfd361f7663a9a9c93b507d17bb1c7e98003d61316826f0a4ba4a93594f72d4 .gitignore
1f960195978886b318953a6dbc7f13a551d9bb2bab44aa88b1599c69c8a9ced5 README.md
1db9d786fdc6c2ae84b7d317e981b23e303a5bc0b72704d43558b29373d97ade .github/workflows/verify.yml
5b30f5e166b59ce8e59437ebdd753a417e0c6c6776b19bb073832aa2ccc72faa .gitignore
79f34290cfa1a6f0928162909848e6597d46183d0de6b213bf52b12546f5fc33 CONTRIBUTING.md
6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc LICENSE
54a252d3512c4b4120103ac600364907d014a10c215f61afd281381bbf89c0c4 README.md
4f3992613b137b83994941d740cb986e3176b78f319bdc2a4e1cc94b0d32b829 README.zh-CN.md
8951d1ad1fdaed9312d4367f5bb4d7adfdab5af4c2dad9231796598e05c60f1e RELEASE_NOTES.md
6cead6c1378cd840f655adc3d467e2cd6fd3ab368d7200a3afbfe85d9ed159be SECURITY.md
1f4df7c43b0f6d138ac40c130511646d6ee9f7f1d9010505479acc7fee2f0e63 docs/README.md
45b7bfb3d8f414e28d50c1f160aee2af8e4150dcd24b815569d48765b1eb413f docs/STORAGE.md
d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py
e5c04001dbd8ed8bba614f7b341237fc6487a9a8fe2f28ccb6f59278725b0a69 plugin/ui/config.js
6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc plugin/LICENSE
17893ebf3fa8f8e0e3b875c1ea214aa006cbd06dcf9b20678029f184599df5d9 plugin/manifest.json
d6a342605426357da7d2e65daa435e99d7234549cffa52e1a9229ef5bc36f33c plugin/ui/config.js
c98f34119436e3020ef20b325f91617943aefe662119cf76738e0226c2e91cbb plugin/ui/device.js
89b9d7994e7477e18c3f43e6f0975e04d5106056ba581b2964db0f39183d6f8a plugin/ui/hardware.js
af631694fcd7da21b77b554e6ee1bf2eaa93bd46a0b8b543e367f032d3236ecb plugin/ui/index.js
67f4d59e288f5390400c52af6a509e940b3ed17f8f520d6bf8bcf7bd345f9336 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
494c90f6d154edc64aad3169653cc08c7f5fd4a6613341b4c0ff10dd7e0e1cb5 tests/browser_alert_semantics.cjs
40c038aa7689c127c0d90ddf390674f534a2bdee8fd3c344d09f8fb6d4bb3964 tests/test_metrics.py
40d10555aef08f8132f576976b7205b3e1c16d330196a11c6cc1c5cbc167c670 tests/test_storage_metrics.py
-52
View File
@@ -1,52 +0,0 @@
# 磁盘手动扫描(2026-10-15)
后续显示层已升级为 Console v2.7.0 / Ubuntu 插件 v1.5.0 / Raspberry Pi 插件 v1.6.0 / Synology 插件 v1.9.0:默认原样使用设备返回顺序,取消 NAS 特殊倒序和优先级。显示名称、同组排序、隐藏统一保存到 Console,首页与配置共用,支持恢复默认顺序。设备已有名称/隐藏定义保留为默认上报值;本篇后文“名称保存在 Agent”等描述为当时的设备定义行为,不代表最新 Console 显示偏好的存储位置。最新验收见相邻 pigway-web-manager/docs/STORAGE-PRESENTATION-v2.7.0-2026-10-15.md;所有实际扫描行为保持手动。
## 已上线状态
Ubuntu Agent v1.3.1 / Console 插件 v1.4.1,Raspberry Pi Agent v3.11.1 / 插件 v1.5.1,Synology 插件 v1.8.1 已上线。两台 Agent 各识别两个实际分区,监控/API 服务 active,循环错误 0。
三种设备在线;中英文及 1440/820/390 三种屏宽的生产界面通过。设备名称、顺序和隐藏设置保留。Console 重启、页面加载及浏览器验收前后,NAS storage_observed_at 未改变,本次没有额外扫描 NAS 硬盘。尚未插入新物理磁盘,也未进行休眠/唤醒实测。
用户最终确认“三种设备都手动发现新增磁盘”。此前自动发现与 NAS 每 5 分钟扫描的候选方案已替换。
## 使用
Ubuntu、树莓派:Console → 系统配置 → 选择设备 → 磁盘设置 → 扫描磁盘。扫描后重新打开该页查看新磁盘,概览随正常轮询更新。名称和隐藏设置保存到 Agent,隐藏不停止历史采集。
NAS:与其他设备一样在配置 → 磁盘设置中点击“扫描磁盘”,首页只展示数据。磁盘清单、容量、温度、健康状态仅在手动扫描时更新;页面保留采集时间。CPU、内存和网络等快速采集继续工作。已有卷别名仍由 Console 保存。
v1.4.1 / v1.5.1 / v1.8.1 统一配置页扫描入口,保存按钮上方保留 20px 间距,扫描与保存反馈分别显示。按用户最终要求,NAS 每个机箱内硬盘采用 DSM 返回顺序反转;存储空间同样反转,主空间和 SSD 空间优先。生产三种屏宽、中英文已验证入口、间距、倒序及优先级;NAS 存储采集时间未改变。前两次发布因验收脚本固定计入网络卡片位置而自动回退,修正为直接验证存储卡片后通过。
扫描有 60 秒冷却,NAS 在途请求合并。页面加载、普通刷新、定时轮询和重启不会重新枚举磁盘。部署时执行过一次明确的初始扫描以保存现有 Agent 磁盘清单。
## 实现与边界
认证 PUT /api/v1/storage/scan 调用 lsblk,读取宿主机挂载点,将块设备清单保存到对应配置旁的 .storage.json(0600)。监控服务只读取清单;已知磁盘挂载和容量每 30 秒更新,新增磁盘必须手动扫描。根分区原有指标和告警保留兼容。
UUID 优先作为标识,其次为设备序列号和分区编号,再退回路径并标记 stable_identity=false。展示可用叶设备,去除重复挂载及 loop/swap 等伪设备。未挂载设备不伪造使用率。配置过的条目在后续扫描未发现时保留为断开;扫描失败保留原清单。
不执行挂载、格式化、分区或 SMART 指令。本次没有增加每磁盘断开/容量告警。
Agent 手工配置使用 status.system.storage_inventory[].id 的 32 位十六进制 key:
```ini
[storage_names]
0123456789abcdef0123456789abcdef = 媒体盘
[storage_visible]
0123456789abcdef0123456789abcdef = true
```
NAS 普通 collect 不再调度存储任务,只有 device.refresh(group=storage) 触发慢查询。重启复用上次摘要;过旧健康数据标为未确认,不确认旧告警恢复。只展示 DSM hdd_info / vol_info 上报的硬盘、扩展柜和卷,未验证任意 USB 外接盘是否均由接口提供。
## 验证和备份
两种 Agent 覆盖挂载点转义、bind 去重、伪设备、未挂载状态、UUID 稳定性、百分号名称、隐藏设置、手动发现和重启后禁止自动枚举。两种插件的中英文/三屏宽测试覆盖 0% 使用率、名称转义、隐藏恢复和显式扫描;打开设置不会扫描。NAS 15 项 Python 回归及六种浏览器布局通过。
生产备份:
- Ubuntu:/var/backups/pigway-ubuntu-agent-storage-1792043168
- Raspberry Pi:/var/backups/pigway-pi-control-storage-1792043209
- Console 容器:/data/storage-release-backups/1792043974(旧插件与 SQLite 备份)
ops/deploy-storage.py 检查 SHA-256 清单、备份三个程序文件与配置,仅重启两个项目服务;验证失败恢复原文件。Console 更新验证三个插件版本、全部设备在线、显示设置保留、NAS 未额外扫描,并执行生产浏览器检查。
-7
View File
@@ -1,7 +0,0 @@
# 首页设备卡片统一约定
所有主机(包括 Server、Gateway、NAS 及后续新增设备)使用顺序:处理器负载 → 内存占用 → 主网络 → 存储分区,再显示设备专属信息。
NAS 系统温度放在处理器负载进度条下方,不单独占卡片;保留系统温度的原始含义。管理跳转图标紧跟设备名称,布局间距 2px,新标签页打开。Docker 管理入口使用 Server 的 DPanel 88 端口。
本次发布为增量更新,保留线上已有功能与配置。发布附件仅包含本次更新的运行时文件,不是可直接导入 Console 的完整适配器安装 ZIP。
+7
View File
@@ -0,0 +1,7 @@
# 文档目录
安装、连接及日常使用从[中文首页](../README.zh-CN.md)或[英文首页](../README.md)开始。本目录收录仍适用的功能、配置与开发说明;版本变化见[版本记录](../RELEASE_NOTES.md)。
- [磁盘发现、刷新与显示](STORAGE.md)
[贡献指南](../CONTRIBUTING.md) · [安全反馈](../SECURITY.md) · [许可证](../LICENSE)
+19
View File
@@ -0,0 +1,19 @@
# 磁盘发现与显示
## 使用
在 Console → 系统配置 → 选择设备 → 磁盘设置中点击“扫描磁盘”,显式发现新增磁盘。打开页面、普通刷新和重启不执行新增磁盘发现。扫描失败保留已有清单;不执行挂载、格式化或分区操作。
已知磁盘容量每周刷新,CPU、内存与网络按自身周期采集。摘要显示实际采集时间;缺失指标不补零,采集失败不据此确认告警恢复。
显示名称、同组顺序与隐藏偏好统一保存到 Console,首页与配置页共用;Agent 已有名称和隐藏定义作为默认值。隐藏只影响显示,不停止采集或删除历史。
## 适配器边界
适配器连接现有 Agent API,不在设备安装额外进程。扫描请求通过既有认证接口转交 Agent;Console 管理展示偏好和通用历史,不替代 Agent 的磁盘身份和持久化清单。
磁盘显示集成需要 Console v2.7.0 或更新的兼容实现;具体 Agent 与适配器版本要求见项目首页。
## 相关说明
[中文首页](../README.zh-CN.md) · [文档目录](README.md)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 PIGWay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "pigway-agent",
"name": "PIGWay Raspberry Pi Agent",
"version": "1.6.7",
"version": "1.6.11",
"protocol_version": 1,
"entrypoint": "worker.py",
"runtime": "python3",
+2 -1
View File
@@ -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) =>
`<div class="row"><span><b>${esc(p.name)}</b> <span class="badge ${p.up ? "good" : "bad"}">${tr(p.up ? "正常" : "停止")}</span><br><span class="muted">${esc(p.target)}</span></span><span class="row-actions"><button data-edit-process="${esc(p.name)}">${tr("编辑")}</button><button class="danger" data-delete-process="${esc(p.name)}">${tr("删除")}</button></span></div>`,
`<div class="row"><span><b>${esc(p.name)}</b> ${runtimeStatus(p.up, appState.LANG)}<br><span class="muted">${esc(p.target)}</span></span><span class="row-actions"><button data-edit-process="${esc(p.name)}">${tr("编辑")}</button><button class="danger" data-delete-process="${esc(p.name)}">${tr("删除")}</button></span></div>`,
),
pluginText("尚未监控任何普通进程", "No process monitors configured"),
);
+16 -4
View File
@@ -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 `<article class="panel host-panel offline-card"><div class="host-head"><div><h2>${esc(h.name || h.id)}</h2><div class="device-details"><span>ID ${esc(h.id)}</span><span>${esc(h.url || tr("本机"))}</span></div></div><span class="badge bad">${tr("离线")}</span></div><div class="error">${esc(entry.error || tr("连接失败"))}</div></article>`;
return `<article class="panel host-panel offline-card"><div class="host-head"><div><h2>${esc(h.name || h.id)}</h2><div class="device-details"><span>ID ${esc(h.id)}</span><span>${esc(h.url || tr("本机"))}</span></div></div><span class="badge semantic-status" data-semantic="error">${tr("离线")}</span></div><div class="error">${esc(entry.error || tr("连接失败"))}</div></article>`;
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 `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge good">${tr("在线")}</span></div><div class="host-grid"><div class="metric" data-refresh-group="system"><div class="muted">${tr("处理器负载")}</div><div class="value">${percent(y.cpu_percent)}</div>${progress(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric" data-refresh-group="system"><div class="muted">${tr("内存占用")}</div><div class="value">${percent(y.memory_percent)}</div>${progress(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric" data-refresh-group="network"><div class="muted">${tr("主网络")}</div><div class="value">↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s</div><div class="muted">${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")}</div></div>${Array.isArray(y.storage_inventory) ? storageCards(y,entry.snapshot?.resources) : `<div class="metric" data-refresh-group="storage"><div class="muted">${tr("磁盘占用")}</div><div class="value">${percent(y.disk_percent)}</div>${progress(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div>`}<div class="metric" data-refresh-group="display"><div class="muted">${tr("显示状态")}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${appState.LANG === "zh" ? "告警状态独立于硬件插件" : "Alert state is independent of hardware plugins"}</div></div><div class="metric" data-refresh-group="alerts"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div></div>${pluginSummary(s.plugins || {}, h.id)}<div class="group"><h3>${tr("服务与进程监控")}</h3><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up ? "good" : "bad"}">${tr(w.up ? "正常" : "停止")}</span></div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></article>`;
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2><span class="uptime-badge">${esc(formatUptime(s.uptime_seconds))}</span></div><div class="device-details"><span>ID ${esc(m.id || h.id)}</span><span>${appState.LANG === "zh" ? "主机名" : "Hostname"} ${esc(m.hostname || v.hostname || "--")}</span><span>${esc(v.manufacturer || "")} ${esc(v.model || "--")}</span><span>${appState.LANG === "zh" ? "序列号" : "Serial"} ${esc(v.serial || "--")}</span><span>${appState.LANG === "zh" ? "硬件版本" : "Revision"} ${esc(v.revision || "--")}</span><span>${esc(v.os || "--")}</span><span>${appState.LANG === "zh" ? "内核" : "Kernel"} ${esc(v.kernel || "--")}</span><span>${esc(v.architecture || "--")} · CPU ${esc(v.cpu_count || "--")}</span><span>${appState.LANG === "zh" ? "程序版本" : "Agent"} ${esc(s.version || "--")}</span></div></div><span class="badge semantic-status" data-semantic="online">${tr("在线")}</span></div><div class="host-grid"><div class="metric" data-refresh-group="system"><div class="muted">${tr("处理器负载")}</div><div class="value">${percent(y.cpu_percent)}</div>${progress(y.cpu_percent)}<div class="muted">${appState.LANG === "zh" ? "频率" : "Frequency"} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins || {})}</div></div><div class="metric" data-refresh-group="system"><div class="muted">${tr("内存占用")}</div><div class="value">${percent(y.memory_percent)}</div>${progress(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${appState.LANG === "zh" ? "可用" : "Available"} ${bytes(y.memory_available_bytes)}</div></div><div class="metric" data-refresh-group="network"><div class="muted">${tr("主网络")}</div><div class="value">↓ ${bytes(n.rx_bytes_per_second)}/s · ↑ ${bytes(n.tx_bytes_per_second)}/s</div><div class="muted">${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")}</div></div>${Array.isArray(y.storage_inventory) ? storageCards(y,entry.snapshot?.resources) : `<div class="metric" data-refresh-group="storage"><div class="muted">${tr("磁盘占用")}</div><div class="value">${percent(y.disk_percent)}</div>${progress(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${appState.LANG === "zh" ? "剩余" : "Free"} ${bytes(y.disk_free_bytes)}</div></div>`}<div class="metric" data-refresh-group="display"><div class="pi-alert-heading"><span class="muted">${appState.LANG === "zh" ? "主要告警" : "Primary alert"}</span>${hardwareState === "online" ? "" : `<span class="semantic-status" data-semantic="${hardwareState === 'offline' ? 'error' : 'unknown'}">${esc(hardwareLabel)}</span>`}</div><div class="value">${esc(displayText)}</div>${hardwareNames ? `<div class="muted">${esc(hardwareNames)}</div>` : ""}</div>${(d.alerts || []).length || !dataIsCurrent(entry) || !Array.isArray(d.alerts) ? `<div class="metric" data-refresh-group="alerts"><div class="muted">${tr("当前告警")}</div><div class="value">${(d.alerts || []).length}</div><div class="muted">${(d.alerts || []).map((a) => esc(ownerName(a.id))).join(" · ") || tr("无告警")}</div></div>` : ""}</div><details class="group" data-overview-details="services"><summary class="smart-summary"><strong class="smart-summary-title">${tr("服务与进程监控")}</strong>${summaryStatus(monitorHealth(s.watches, dataIsCurrent(entry)), appState.LANG)}</summary><div style="margin-top:12px"><div class="rows">${(s.watches || []).map((w) => `<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span>${runtimeStatus(w.up, appState.LANG)}</div>`).join("") || `<div class="muted">${tr("暂无数据")}</div>`}</div></div></details></article>`;
}
+3 -3
View File
@@ -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) =>
`<div class="row"><span><b>${esc(s.unit)}</b><br><span class="muted">${esc(s.description || "暂无服务说明")} · ${esc(s.active === "active" ? "正在运行" : "未运行")}</span></span><span class="row-actions"><span class="badge ${s.monitored && !s.paused ? "good" : ""}">${s.monitored ? (s.paused ? "已暂停" : "已监控") : "未监控"}</span><button data-select-service="${esc(s.unit)}">${s.monitored ? "编辑监控" : "加入监控"}</button></span></div>`,
`<div class="row"><span><b>${esc(s.unit)}</b><br><span class="muted">${esc(s.description || "暂无服务说明")} · ${esc(s.active === "active" ? text("运行中", "Running") : s.active === "inactive" || s.active === "failed" ? text("已停止", "Stopped") : text("未确认", "Unconfirmed"))}</span></span><span class="row-actions"><span class="badge semantic-status" data-semantic="${s.monitored && !s.paused ? "enabled" : "neutral"}">${s.monitored ? (s.paused ? "已暂停" : "已监控") : "未监控"}</span><button data-select-service="${esc(s.unit)}">${s.monitored ? "编辑监控" : "加入监控"}</button></span></div>`,
),
"没有匹配的服务",
);
@@ -40,7 +40,7 @@ export function renderServiceLists() {
"monitoredServices",
monitored.map(
(s) =>
`<div class="row"><span><b>${esc(s.monitor_name)}</b> <span class="badge ${s.paused ? "" : s.active === "active" ? "good" : "bad"}">${s.paused ? "监控已暂停" : s.active === "active" ? "运行中" : "服务已停止"}</span><br><span class="muted">${esc(s.unit)}</span><br><span class="muted">备注:${esc(s.note || "未填写")}</span></span><span class="row-actions"><button data-monitor-action="${s.paused ? "resume" : "pause"}" data-unit="${esc(s.unit)}">${s.paused ? "恢复监控" : "暂停监控"}</button><button data-service-action="${s.active === "active" ? "stop" : "start"}" data-unit="${esc(s.unit)}" ${s.protected && s.active === "active" ? 'disabled title="API 服务不能停止自身"' : ""}>${s.active === "active" ? "停止服务" : "启动服务"}</button><button data-select-service="${esc(s.unit)}">编辑</button><button class="danger" data-delete-monitor="${esc(s.unit)}">删除监控</button></span></div>`,
`<div class="row"><span><b>${esc(s.monitor_name)}</b> <span class="badge semantic-status" data-semantic="${s.paused ? "neutral" : s.active === "active" ? "running" : s.active === "inactive" || s.active === "failed" ? "error" : "unknown"}">${s.paused ? text("监控已暂停", "Monitor paused") : s.active === "active" ? text("运行中", "Running") : s.active === "inactive" || s.active === "failed" ? text("已停止", "Stopped") : text("未确认", "Unconfirmed")}</span><br><span class="muted">${esc(s.unit)}</span><br><span class="muted">备注:${esc(s.note || "未填写")}</span></span><span class="row-actions"><button data-monitor-action="${s.paused ? "resume" : "pause"}" data-unit="${esc(s.unit)}">${s.paused ? "恢复监控" : "暂停监控"}</button><button data-service-action="${s.active === "active" ? "stop" : "start"}" data-unit="${esc(s.unit)}" ${s.protected && s.active === "active" ? 'disabled title="API 服务不能停止自身"' : ""}>${s.active === "active" ? "停止服务" : "启动服务"}</button><button data-select-service="${esc(s.unit)}">编辑</button><button class="danger" data-delete-monitor="${esc(s.unit)}">删除监控</button></span></div>`,
),
"尚未监控任何服务",
);
+4
View File
@@ -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; }
+37
View File
@@ -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-device-console/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=`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="/assets/css/base.css"><link rel="stylesheet" href="/assets/css/layout.css"><link rel="stylesheet" href="/assets/css/controls.css"><link rel="stylesheet" href="/plugin/styles.css"><main id="cards"></main><script type="module">import {agentHostCard} from '/plugin/overview.js';import {state} from '/assets/js/state.js';window.render=(lang,plugins={})=>{state.LANG=lang;document.querySelector('#cards').innerHTML=agentHostCard({host:{id:'gateway',name:'Gateway'},online:true,status:{version:'3.11.2',machine:{id:'gateway',name:'家庭网关 100%',hostname:'gateway'},device:{model:'Raspberry Pi 4',os:'Debian 13',architecture:'aarch64',cpu_count:4},system:{cpu_percent:12,memory_percent:25,temperature_c:42,cpu_freq_mhz:1500,memory_used_bytes:1000000,memory_total_bytes:4000000,memory_available_bytes:3000000,storage_inventory:[]},network:{interface:'eth0',kind:'ETH',link_speed_mbps:1000,link:true,ip_label:'IP4',ip:'192.168.1.2',rx_bytes_per_second:10,tx_bytes_per_second:20},display:{owner:'DISK_CRITICAL',alerts:[{id:'DISK_CRITICAL'}]},plugins,watches:[]}})};render('zh');</script>`;}
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').count())throw Error('Healthy hardware must not show redundant 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 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('.pi-alert-heading').evaluate(n=>Math.abs(n.lastElementChild.getBoundingClientRect().right-n.getBoundingClientRect().right)<1)))throw Error('Hardware issue status is not right aligned');
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)});