Compare commits

...
10 Commits
Author SHA1 Message Date
way bd3cbe14ab fix: resolve visual spacing and responsive layout issues
Verify / test (push) Successful in 6s
2026-10-20 21:04:03 +08:00
way b8bab83f08 feat: add Gateway management access URL in system settings
Verify / test (push) Successful in 6s
2026-10-20 20:55:28 +08:00
way ffb213f279 release: v1.6.12 official PIGWay branding
Verify / test (push) Successful in 6s
2026-10-19 19:22:29 +08:00
way 782ada6d76 feat: integrate official PIGWay v1.1 brand assets
Verify / test (push) Successful in 6s
2026-10-19 19:03:03 +08:00
way 78158a3112 ci: use verified official actions on the shared Server runner
Verify / test (push) Successful in 4s
2026-10-18 22:18:54 +08:00
way 799315f6c9 release: v1.6.11 open-source delivery and audit fixes
Verify / test (push) Failing after 13m45s
2026-10-18 18:45:03 +08:00
way 65f89bffb3 docs: organize project guides and consolidate historical records 2026-10-18 17:59:23 +08:00
way efdff94626 docs: align repository names and bilingual entry points; remove obsolete reports 2026-10-18 17:53:43 +08:00
way ddcf53504f release: publish v1.6.10 2026-10-18 14:43:04 +08:00
way e9d7cc536f fix: show hardware status only when attention is needed 2026-10-18 13:32:51 +08:00
32 changed files with 256 additions and 179 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: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: https://github.com/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/ dist/
.DS_Store .DS_Store
.data/ .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.
+24 -38
View File
@@ -1,48 +1,34 @@
# PIGWay Raspberry Pi Console Plugin <p><img src="docs/brand/pigway-logo.png" alt="PIGWay" width="96" height="96"></p>
磁盘呈现统一版本已上线,需要 Console v2.7.0:默认按设备返回顺序,同组支持上移/下移、显示名称及隐藏,所有显示偏好保存到 Console,首页与配置页共用。Agent 的名称和隐藏定义继续作为设备默认值保留。扫描仍只由配置页按钮触发。生产验证见相邻 Console 的 docs/STORAGE-PRESENTATION-v2.7.0-2026-10-15.md。 # PIGWay Raspberry Pi Console Adapter
当前 v1.6.0 支持统一磁盘显示设置与手动扫描,需要 Console v2.7.0、Agent v3.11.1;新磁盘仅手动发现,已知磁盘容量继续刷新。见 [生产验证](STORAGE-DISCOVERY.md)。 [简体中文](README.zh-CN.md) | English
独立维护的树莓派接入插件:连接现有 PIGWay Raspberry Pi Agent,不在树莓派安装新进程。设备 API、状态转换、配置字段、服务/进程监控、硬件界面及其翻译均由本仓库维护。 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.
依赖 Python 标准库;接入 Console JSON-RPC v1 与可信前端扩展 `console-ui-v1`。插件 ID 保持 `pigway-agent`,已有设备可直接继续使用。 ## Requirements and installation
打包: 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.14**.
```sh ```sh
mkdir -p dist mkdir -p dist
python3 package.py plugin dist/pigway-agent-1.1.0.zip python3 package.py plugin dist/pigway-agent-1.6.14.zip
``` ```
在控制中心的机器管理页面安装 ZIP,再启用。开发时可用 `PIGWAY_ADAPTER_PATH=/absolute/path/pigway-console-adapter-raspberry-pi/plugin` 加载源码;Docker 推荐使用持久化数据卷中的安装包。 [Documentation](docs/README.md)
包根目录只有 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)。
## Console UI 语义统一
本次界面需要 Console v2.7.30 的公共语义模块:在线、运行中使用绿色,健康正常使用蓝色,警告/故障/未知分别使用黄/红/灰色;状态采用14px/20px纯文字。完整规则见相邻 Console 项目的 `docs/UI_SEMANTICS.md`。
+32
View File
@@ -0,0 +1,32 @@
<p><img src="docs/brand/pigway-logo.png" alt="PIGWay" width="96" height="96"></p>
# PIGWay Raspberry Pi Console Plugin
简体中文 | [English](README.md)
磁盘呈现统一版本已上线,需要 Console v2.7.0:默认按设备返回顺序,同组支持上移/下移、显示名称及隐藏,所有显示偏好保存到 Console,首页与配置页共用。Agent 的名称和隐藏定义继续作为设备默认值保留。扫描仍只由配置页按钮触发。
当前 v1.6.14 支持统一磁盘显示设置与手动扫描,需要 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.14.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)
+2 -4
View File
@@ -1,5 +1,3 @@
# PIGWay Raspberry Pi Adapter v1.6.9 # pigway-console-adapter-raspberry-pi v1.6.14
- 主要告警标题右侧的硬件可用状态从绿色“在线”改为蓝色“正常”,复用 Console normal 语义。 修复系统配置中 Gateway 访问地址标签与输入框紧贴的问题,复用现有 fields 布局,统一字段宽度及上下间距。
- 保留离线、未确认、未接入提示以及 OLED/RGB 内容。
- 桌面、平板、手机及在线、离线、未接入状态浏览器检查通过。
+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.
+19 -19
View File
@@ -1,31 +1,31 @@
3cfd361f7663a9a9c93b507d17bb1c7e98003d61316826f0a4ba4a93594f72d4 .gitignore 6635c6d8b53076d7fd9fe2c3382469420ada532ce010970644e912b877495151 .github/workflows/verify.yml
8ef77455daa560a7f25a1d8a4684c9e8ce9232745d844b0e3703c7f57830c85f DEPLOYMENT-v1.2.0-2026-10-14.md 5b30f5e166b59ce8e59437ebdd753a417e0c6c6776b19bb073832aa2ccc72faa .gitignore
176127d6a7c5abf61112ea019ccc8f62819d1b94c5084ae51b99cbe0369cc22f DEPLOYMENT-v1.2.1-2026-10-14.md 79f34290cfa1a6f0928162909848e6597d46183d0de6b213bf52b12546f5fc33 CONTRIBUTING.md
d4938f4fb971fb665e377ba966b523250401138cf7e1186cb57da55fbf904f3b DEPLOYMENT-v1.2.2-2026-10-14.md 6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc LICENSE
ec04c97e0271455a41328b8d68e8e4a5d0e3ef5782ddb8fc62a08b7bfff2ef9d DEPLOYMENT-v1.3.0-2026-10-15.md c3666d31c679a6f269a98d6b86ca4311b5186012162472051b2a5a29298be88b README.md
136db6c02d7181085f18c54bf1ca2b0bc56e3aa43e3a71a0e59926fc7fa7b18f DEPLOYMENT-v1.3.2-2026-10-15.md a2a7dec9efbd8cba4d9c50e0116080ddddcc2fd506a45a1637ab27257edc10ce README.zh-CN.md
d0870015c81d16af20cf38b775fa95b5ac1619cc5144fcda5007cb2f832e0e33 DEPLOYMENT-v1.3.3-2026-10-15.md bd32ad65de3dfcd937b357e0364fa756345d54790be1213629e13bf40af21116 RELEASE_NOTES.md
552b66fe8946ea96b12633fcf705c71b14d32679bd57ddd39fd5c4cc8910f1b2 README.md 6cead6c1378cd840f655adc3d467e2cd6fd3ab368d7200a3afbfe85d9ed159be SECURITY.md
377762a3427522893090b4bd7ef2d950758692c3b012d9b5b808549dda13cf2b RELEASE_NOTES.md 1f4df7c43b0f6d138ac40c130511646d6ee9f7f1d9010505479acc7fee2f0e63 docs/README.md
a669ccb22bf3aafec6fa558fd84108dca013cb8dc2988206b7b5fe58a1eb2035 STORAGE-DISCOVERY.md 45b7bfb3d8f414e28d50c1f160aee2af8e4150dcd24b815569d48765b1eb413f docs/STORAGE.md
0cf93f3b3d3219ab8229b59e98cbd82e42647a461231f7c767ea3d120f8e0910 docs/AUDIT-FIXES-2026-10-16.md f8ef7749c58d4396781bbc21371048f6561e791efbd489570224dc51173bffff docs/brand/README.md
1eec71f194bfdd51b6ae5a9063a0760ccf24b46474901defdf19503a680b8fcd docs/HOME-CARD-ORDER-2026-10-17.md f709156217c798f6b6230dae1f341373e0814a67ab90e881401da577f52ec3d3 docs/brand/pigway-avatar.png
b47aafb725706a03837aceabd1ac59b302ead066bf12bf947326a98cb38963ef docs/OVERVIEW-DISCLOSURES-2026-10-16.md da6734d8a23305f6254c0936dadff4ec34977b4c934f5067186423c8cfce3fc7 docs/brand/pigway-logo.png
f0df601aafe839cfe06c069471071023158beced81c5d0b802fa0bb4ed3528bc docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md 0fa494a3fbd456c754bbea3f7f050be139e32e3b42b09a1ed953094087437210 docs/brand/pigway-logo.svg
d3528d8fd4eed197942e2c9a72d62e6c9ff6b3ab00f4be98c337b6e306d6e177 docs/STATUS-ALIGNMENT-2026-10-16.md
d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py d081c599d821149853368b1a8637c57a16995344406ff5ec9d7619b66e0479b3 package.py
fb2eab0e50e1f0363e89a5df284f609f9a7f911725a81c09d17ea839b78f37ff plugin/manifest.json 6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc plugin/LICENSE
d6a342605426357da7d2e65daa435e99d7234549cffa52e1a9229ef5bc36f33c plugin/ui/config.js a65aaffe53b4dcfa0a6681fa679a30a097de201035e60fb2fa8ab4bdc2df354b plugin/manifest.json
74d3e10521a5359bdb248d1b5ee43b4601d6a05a24c3c3426810ae6354f41aa8 plugin/ui/config.js
c98f34119436e3020ef20b325f91617943aefe662119cf76738e0226c2e91cbb plugin/ui/device.js c98f34119436e3020ef20b325f91617943aefe662119cf76738e0226c2e91cbb plugin/ui/device.js
89b9d7994e7477e18c3f43e6f0975e04d5106056ba581b2964db0f39183d6f8a plugin/ui/hardware.js 89b9d7994e7477e18c3f43e6f0975e04d5106056ba581b2964db0f39183d6f8a plugin/ui/hardware.js
af631694fcd7da21b77b554e6ee1bf2eaa93bd46a0b8b543e367f032d3236ecb plugin/ui/index.js af631694fcd7da21b77b554e6ee1bf2eaa93bd46a0b8b543e367f032d3236ecb plugin/ui/index.js
9c1a488c99a61f78c3a6abaea9ce84e53fc6b73f36dc92c7365136231dc70058 plugin/ui/overview.js 914087ba4441832ac51e268542dc6d27a30371fc9cb2250cd84233dc8d00f6d9 plugin/ui/overview.js
422e1f8323d29a4be757f39e8b60addd7764c436a1302673809e0c547fce5ff4 plugin/ui/schema.js 422e1f8323d29a4be757f39e8b60addd7764c436a1302673809e0c547fce5ff4 plugin/ui/schema.js
1bee0f9e5656a52bc97d9fafffbf5157dae062888f618046ad09470dc2685f3c plugin/ui/services.js 1bee0f9e5656a52bc97d9fafffbf5157dae062888f618046ad09470dc2685f3c plugin/ui/services.js
6806df689f62de56f7b61f8b3cf3a33c494a09931a14f51d9d71c26d2c1f61ee plugin/ui/storage.js 6806df689f62de56f7b61f8b3cf3a33c494a09931a14f51d9d71c26d2c1f61ee plugin/ui/storage.js
061f5011b78cedcd0c69a32aa5106889d38f6863f4c613ae326cc9d16f90e2dd plugin/ui/styles.css 061f5011b78cedcd0c69a32aa5106889d38f6863f4c613ae326cc9d16f90e2dd plugin/ui/styles.css
0b79b9c5b5f23727164e05b127caca72255416309166ba200ccbd15a9667473c plugin/ui/translations.js 0b79b9c5b5f23727164e05b127caca72255416309166ba200ccbd15a9667473c plugin/ui/translations.js
d5b22366a2c7b375e6f3e35cefb9f95da0ea3b8390b3ec4d1000492f00128f6e plugin/worker.py d5b22366a2c7b375e6f3e35cefb9f95da0ea3b8390b3ec4d1000492f00128f6e plugin/worker.py
4efdd82aac730361cde262fc55677e965588259064e68bb1b01831d1ba5a157d tests/browser_alert_semantics.cjs a6c66ba9df4758ac7362016efaec37ac17b1235244885bf270cea418371dcaad tests/browser_alert_semantics.cjs
40c038aa7689c127c0d90ddf390674f534a2bdee8fd3c344d09f8fb6d4bb3964 tests/test_metrics.py 40c038aa7689c127c0d90ddf390674f534a2bdee8fd3c344d09f8fb6d4bb3964 tests/test_metrics.py
40d10555aef08f8132f576976b7205b3e1c16d330196a11c6cc1c5cbc167c670 tests/test_storage_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 未额外扫描,并执行生产浏览器检查。
-5
View File
@@ -1,5 +0,0 @@
# 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`。
-7
View File
@@ -1,7 +0,0 @@
# 首页设备卡片统一约定
所有主机(包括 Server、Gateway、NAS 及后续新增设备)使用顺序:处理器负载 → 内存占用 → 主网络 → 存储分区,再显示设备专属信息。
NAS 系统温度放在处理器负载进度条下方,不单独占卡片;保留系统温度的原始含义。管理跳转图标紧跟设备名称,布局间距 2px,新标签页打开。Docker 管理入口使用 Server 的 DPanel 88 端口。
本次发布为增量更新,保留线上已有功能与配置。发布附件仅包含本次更新的运行时文件,不是可直接导入 Console 的完整适配器安装 ZIP。
-5
View File
@@ -1,5 +0,0 @@
# 概览折叠区块 1.6.3
“服务与进程监控”或“已读取范围内的告警”采用默认收起的 details/summary,沿用 SMART 展开样式。data-overview-details 配合 Console 2.7.24 保留每台设备在自动刷新中的展开状态。内容和监控行为保持不变。
已部署至生产;中英文、桌面/平板/手机验收通过。部署回执及验证详见控制台 docs/OVERVIEW-DISCLOSURES-2026-10-16.md。
@@ -1,3 +0,0 @@
# 概览汇总标签
折叠标题增加正常/异常数量/未确认汇总,暂无监控不视为正常。共享 Console 2.7.25 overview-health.js 判定,过期数据不显示绿色正常。已部署,中英文桌面/平板/手机及各状态模拟验收通过。完整回执见控制台 docs/OVERVIEW-SUMMARY-BADGES-2026-10-16.md。
+7
View File
@@ -0,0 +1,7 @@
# 文档目录
安装、连接及日常使用从[中文首页](../README.zh-CN.md)或[英文首页](../README.md)开始。本目录收录仍适用的功能、配置与开发说明;版本变化见[版本记录](../RELEASE_NOTES.md)。
- [磁盘发现、刷新与显示](STORAGE.md)
[贡献指南](../CONTRIBUTING.md) · [安全反馈](../SECURITY.md) · [许可证](../LICENSE)
-3
View File
@@ -1,3 +0,0 @@
# 概览状态纯文字展示
汇总调用改为 summaryStatus,配合 Console 2.7.26 的统一纯文字与对齐规则。已部署;中英文、深浅主题、桌面/平板/手机及长文本实际展开测量均通过。判断逻辑不变。详见控制台 docs/STATUS-ALIGNMENT-2026-10-16.md。
+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)
+15
View File
@@ -0,0 +1,15 @@
# PIGWAY 品牌资源 / Brand assets
来源:PIGWAY Brand Production v1.1 正式交付包。图形、颜色与比例保持原文件,不使用重绘、滤镜或替代字标。
- `pigway-logo.svg`:正式 Web 图形标。
- `pigway-logo.png`:正式 256px 透明图形标,用于项目介绍页。
- `pigway-avatar.png`:正式 512px 平台图标,用于 Tea 项目头像。
本项目的产品名称保持独立。品牌物料版本不跟随软件版本变化;网页小图标使用同一正式包的 Favicon 和 Apple Touch Icon。
## SHA-256
- `0fa494a3fbd456c754bbea3f7f050be139e32e3b42b09a1ed953094087437210` `pigway-logo.svg`
- `da6734d8a23305f6254c0936dadff4ec34977b4c934f5067186423c8cfce3fc7` `pigway-logo.png`
- `f709156217c798f6b6230dae1f341373e0814a67ab90e881401da577f52ec3d3` `pigway-avatar.png`
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

+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.
+10 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "pigway-agent", "id": "pigway-agent",
"name": "PIGWay Raspberry Pi Agent", "name": "PIGWay Raspberry Pi Agent",
"version": "1.6.9", "version": "1.6.14",
"protocol_version": 1, "protocol_version": 1,
"entrypoint": "worker.py", "entrypoint": "worker.py",
"runtime": "python3", "runtime": "python3",
@@ -33,6 +33,15 @@
"type": "string", "type": "string",
"required": true, "required": true,
"secret": true "secret": true
},
"management_url": {
"label": {
"zh": "访问地址",
"en": "Access URL"
},
"type": "string",
"required": false,
"format": "http-origin"
} }
}, },
"ui": { "ui": {
+29 -1
View File
@@ -311,7 +311,7 @@ export function renderConfig() {
); );
$("mainSavebar").classList.toggle( $("mainSavebar").classList.toggle(
"hidden", "hidden",
["bulk_sync", "plugins"].includes(b.dataset.configSection), ["bulk_sync", "plugins", "access"].includes(b.dataset.configSection),
); );
if (b.dataset.configSection === "services") loadServices(); if (b.dataset.configSection === "services") loadServices();
if (b.dataset.configSection === "processes") loadProcesses(); if (b.dataset.configSection === "processes") loadProcesses();
@@ -348,6 +348,7 @@ export function renderConfig() {
bindBulkSync(); bindBulkSync();
updateConditional(); updateConditional();
addStorageSettings(); addStorageSettings();
installAccessSettings();
} }
export function markChanged() { export function markChanged() {
let n = 0; let n = 0;
@@ -365,3 +366,30 @@ export function markChanged() {
: tr("尚未修改"); : tr("尚未修改");
$("saveState").className = n ? "" : "muted"; $("saveState").className = n ? "" : "muted";
} }
function installAccessSettings() {
const host = appState.fleetHosts.find(h => h.id === appState.currentConfigHost);
if (!host) return;
$("configMenu").insertAdjacentHTML("beforeend", `<button data-config-section="access">${pluginText("访问地址", "Access URL")}</button>`);
$("configRows").insertAdjacentHTML("beforeend", `<section class="config-page" data-config-page="access"><h2>${pluginText("访问地址", "Access URL")}</h2><p class="muted">${pluginText("设置 Gateway 管理页面地址,首页显示链接按钮;留空隐藏入口。", "Set the Gateway management URL to show a link on the overview. Leave blank to hide it.")}</p><div class="fields"><div class="field"><label>${pluginText("访问地址", "Access URL")}<input id="gatewayAccessUrl" type="url" value="${esc(host.connection?.management_url || "")}" placeholder="https://gateway.example.com"></label></div></div><div class="action-row"><button id="saveGatewayAccess">${pluginText("保存访问地址", "Save access URL")}</button><span id="gatewayAccessMessage" role="status"></span></div></section>`);
$("saveGatewayAccess").onclick = async () => {
const button = $("saveGatewayAccess"), message = $("gatewayAccessMessage");
const value = $("gatewayAccessUrl").value.trim();
if (value) {
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw Error();
} catch {
message.textContent = pluginText("请使用不含路径或账号密码的 HTTP(S) 根地址", "Use an HTTP(S) origin without paths or credentials");
message.className = "error"; return;
}
}
button.disabled = true;
try {
const result = await get("/api/v1/hosts", {method: "PUT", headers: {"Content-Type": "application/json"}, body: JSON.stringify({operation: "save", id: host.id, adapter_id: host.adapter_id, connection: {management_url: value}})});
appState.fleetHosts = result.hosts;
message.textContent = pluginText("已保存", "Saved"); message.className = "success";
} catch (error) { message.textContent = error.message; message.className = "error"; }
finally { button.disabled = false; }
};
}
+11 -2
View File
@@ -8,10 +8,19 @@ import { bytes, formatUptime, meter } from "/assets/js/overview.js";
import { fanMetric } from "./hardware.js"; import { fanMetric } from "./hardware.js";
import { ownerName, rgbName } from "./schema.js"; import { ownerName, rgbName } from "./schema.js";
function managementLink(host) {
try {
const url = new URL(host.connection?.management_url);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return "";
const label = appState.LANG === "zh" ? "打开 Gateway 管理界面" : "Open Gateway management";
return `<a class="docker-icon" style="margin-left: -7px" href="${esc(url.href)}" target="_blank" rel="noopener noreferrer" title="${esc(label)}" aria-label="${esc(label)}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-10 10M11 4H4v16h16v-7"/></svg></a>`;
} catch { return ""; }
}
export function agentHostCard(entry) { export function agentHostCard(entry) {
const h = entry.host; const h = entry.host;
if (!entry.online) 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 semantic-status" data-semantic="error">${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><div class="host-title"><h2>${esc(h.name || h.id)}</h2>${managementLink(h)}</div><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, const s = entry.status,
y = s.system || {}, y = s.system || {},
n = s.network || {}, n = s.network || {},
@@ -35,5 +44,5 @@ export function agentHostCard(entry) {
const hardwareNames = hardware.map(p => p.name).filter(Boolean).join(" · "); const hardwareNames = hardware.map(p => p.name).filter(Boolean).join(" · ");
const percent = n => Number.isFinite(n) ? `${n.toFixed(1)}%` : "--"; const percent = n => Number.isFinite(n) ? `${n.toFixed(1)}%` : "--";
const progress = n => Number.isFinite(n) ? meter(n) : ""; 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 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><span class="semantic-status" data-semantic="${hardwareState === 'online' ? 'normal' : 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>`; return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(m.name || h.name || m.id)}</h2>${managementLink(h)}<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>`;
} }
+5 -5
View File
@@ -1,11 +1,11 @@
const {chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright'); const {chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const fs=require('fs');const path=require('path'); const fs=require('fs');const path=require('path');
const adapter=path.resolve(__dirname,'..'); const adapter=path.resolve(__dirname,'..');
const web=process.env.PIGWAY_WEB_ROOT || path.resolve(adapter,'../pigway-web-manager/web'); 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}); 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)); (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'; 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>`;} 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',connection:{management_url:'http://192.168.89.14'}},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/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/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==='/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')); }
@@ -13,6 +13,7 @@ else if(u.pathname==='/plugin/hardware.js')body='export const fanMetric=()=>"";e
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';} 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 route.fulfill({status:200,contentType:type,body});});
await page.goto('http://audit.local/');await page.waitForFunction(()=>typeof window.render==='function'); await page.goto('http://audit.local/');await page.waitForFunction(()=>typeof window.render==='function');
await page.locator('a[href="http://192.168.89.14/"]').waitFor();
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});}} 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'}}}; 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]){ for(const width of [1440,768,390]){
@@ -20,18 +21,17 @@ for(const width of [1440,768,390]){
await page.evaluate(p=>render('zh',p),hardware); await page.evaluate(p=>render('zh',p),hardware);
const primary=page.locator('[data-refresh-group="display"]'); 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('.value').innerText())!=='OLED 正常主页 · RGB 全灭')throw Error('Merged OLED/RGB text');
if((await primary.locator('.pi-alert-heading .semantic-status').innerText())!=='正常')throw Error('Missing hardware normal status'); if(await primary.locator('.pi-alert-heading .semantic-status').count())throw Error('Healthy hardware must not show redundant status');
if((await primary.locator('.pi-alert-heading .semantic-status').getAttribute('data-semantic'))!=='normal')throw Error('Hardware status must use normal blue');
if(await page.locator('[data-manage-plugin-host]').count())throw Error('Overview must not expose plugin management'); 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 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.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'); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth))throw Error('Merged display overflow');
await page.screenshot({path:output+'/merged-'+width+'.png',fullPage:true}); await page.screenshot({path:output+'/merged-'+width+'.png',fullPage:true});
} }
hardware.hat.online=false; hardware.hat.online=false;
await page.evaluate(p=>render('zh',p),hardware); 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 .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'); if((await page.locator('[data-refresh-group="display"] .value').innerText()).includes('RGB 全灭'))throw Error('Stale RGB shown as current');
await page.evaluate(()=>render('zh',{})); await page.evaluate(()=>render('zh',{}));
if((await page.locator('.pi-alert-heading .semantic-status').innerText())!=='未接入')throw Error('Absent hardware shown online'); if((await page.locator('.pi-alert-heading .semantic-status').innerText())!=='未接入')throw Error('Absent hardware shown online');