From c1459aef450426f8241993b60de2f102176f816b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A1=E5=8F=A3=E6=B5=AA=E4=BA=BA?= Date: Sat, 17 Oct 2026 23:31:38 +0800 Subject: [PATCH] Flatten monitoring cards and align frame subtitle with Console --- DEPLOYMENT.md | 6 ++++++ README.md | 6 +++--- plugin/manifest.json | 2 +- plugin/ui/index.js | 23 +++++++++++++++++++++-- pyproject.toml | 2 +- tests/ui.cjs | 8 +++++++- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 79a8b7e..b36d8bc 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -19,3 +19,9 @@ 历史页补充 7 个指标的中英文译名;基于生产原镜像仅加入这一文件改动,运行镜像为 `pigway-device-console-manager:v2.8.2-frame-flow-i18n1`,原镜像保留。Console 容器已重建,其他服务未重启。数据库、旧插件、原 Compose 和 history.js 备份于 `/srv/pigway/apps/pigway-device-console/backups/frame-i18n-20261017-150652`。 新包 SHA-256:`455b1ec5fb5d43aeb042528da0d13ebf1b5b214a2f56234038c4639cb0cac83c`。6 项 Python 测试通过;生产中英文与桌面/平板/手机共 6 次页面检查通过,无横向溢出及页面脚本错误;四台登记设备在线。 + +## v1.0.2 平铺卡片 + +2026-10-17(Asia/Shanghai):删除中间资源分组框,指标直接平铺;相框名称使用 Console 现有 `host-title` / `device-details` 结构显示在 Frame Flow 下方,与 Docker 的 Server 副标题一致。轮播任务合并为“轮播任务 / 启用”,最近运行成功时右上角显示蓝色“正常”,失败和未确认保留对应状态。 + +生产旧插件备份:`/srv/pigway/apps/pigway-device-console/backups/frame-flat-20261017-153104`;仅重启 Console 容器以加载插件版本。中英文与三尺寸生产浏览器检查共 6 次通过,确认无嵌套指标卡、任务状态颜色使用 Console 的 `--status-normal`、无横向溢出及脚本错误。四台设备在线。 diff --git a/README.md b/README.md index a8b5936..0bd3356 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # PIGWay Console Adapter — Frame Flow -独立 ZIP 插件 `pigway-frame-flow` v1.0.1,采用现有 Console adapter protocol v1,仅依赖 Python 标准库。源码在独立仓库 `pigway-console-adapter-frame-flow` 维护,无需修改 Console 核心或部署额外服务。 +独立 ZIP 插件 `pigway-frame-flow` v1.0.2,采用现有 Console adapter protocol v1,仅依赖 Python 标准库。源码在独立仓库 `pigway-console-adapter-frame-flow` 维护,无需修改 Console 核心或部署额外服务。 ## 安装 -1. 在 Console「机器管理」上传 `dist/pigway-frame-flow-1.0.1.zip`,启用插件。 +1. 在 Console「机器管理」上传 `dist/pigway-frame-flow-1.0.2.zip`,启用插件。 2. 添加设备,选择 PIGWay Frame Flow,填写 Console 能访问的 Frame Flow HTTP(S) 根地址(不含 `/api/v1`)和 **Frame Flow API 令牌**,不是 KoKonna Key 或网页登录密码。 3. 若使用受限 API 客户端,授予 `devices:read`、`jobs:read`;默认 `FRAME_FLOW_TOKEN` 也可使用。令牌保存于 Console 敏感字段。跨不可信网络使用 HTTPS。 4. 查看通用资源看板、告警与历史;默认每 60 秒采集一次。卡片和告警随 Console 中英文切换,资源显示 API 上报名称或 Console 中设置的显示名称。 @@ -24,7 +24,7 @@ python3 -m venv .venv .venv/bin/python -m pip install pytest ruff .venv/bin/python -m pytest -q mkdir -p dist -.venv/bin/python package.py plugin dist/pigway-frame-flow-1.0.1.zip +.venv/bin/python package.py plugin dist/pigway-frame-flow-1.0.2.zip ``` 开发时可将 `PIGWAY_ADAPTER_PATH` 指向本目录 `plugin` 的绝对路径。ZIP 安装默认停用;生产安装与设备登记需在 Console 中执行。本地验证覆盖模拟接口、RPC 和隔离安装;生产部署记录见 [DEPLOYMENT.md](DEPLOYMENT.md)。 diff --git a/plugin/manifest.json b/plugin/manifest.json index cb1db1b..2c9d28a 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -1,7 +1,7 @@ { "id": "pigway-frame-flow", "name": "PIGWay Frame Flow", - "version": "1.0.1", + "version": "1.0.2", "protocol_version": 1, "runtime": "python3", "entrypoint": "worker.py", diff --git a/plugin/ui/index.js b/plugin/ui/index.js index edd7dfb..9abd319 100644 --- a/plugin/ui/index.js +++ b/plugin/ui/index.js @@ -1,7 +1,6 @@ import { state } from '/assets/js/state.js'; import { esc, pluginText as text } from '/assets/js/shared.js'; -const typeName = type => ({service: text('服务', 'Service'), device: text('画框', 'Frame'), job: text('任务', 'Job')}[type] || type); const metricValue = metric => { if (metric.unit === 'percent') return `${metric.value}%`; if (metric.unit === 'count') return `${metric.value} ${text('个', 'items')}`; @@ -26,5 +25,25 @@ export function hostCard(entry) { const resource = separator < 0 ? '' : `${kind.startsWith('JOB_') ? 'job' : 'device'}:${alert.id.slice(separator + 1)}`; return `
${esc(label(alert))}${esc(resource ? name(resource) : '')}
`; }).join(''); - return `

${esc(entry.host.name || 'Frame Flow')}

${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')}
${!current ? `

${text('以下为上次采集数据', 'Showing the last collected data')}

` : ''}${resources.map(resource => `

${esc(resource.name || resource.id)} · ${esc(typeName(resource.type))}

${(snapshot.metrics || []).filter(m => m.resource_id === resource.id).map(metric => `
${esc(label(metric))}
${esc(metricValue(metric))}
`).join('')}
`).join('')}

${text('当前告警', 'Current alerts')}

${alertRows || text('无告警', 'No alerts')}
`; + const metrics = snapshot.metrics || []; + const value = (resource, key) => metrics.find(m => m.resource_id === resource.id && m.key === key)?.value; + const card = (title, content, status = '') => `
${esc(title)}
${status}
${esc(content)}
`; + const status = (semantic, title) => `${esc(title)}`; + const cards = resources.flatMap(resource => { + if (resource.type === 'job') { + const enabled = value(resource, 'job_enabled') === 1; + const result = value(resource, 'last_run_success'); + const unknown = (snapshot.alerts || []).some(a => a.id === `JOB_UNKNOWN:${resource.id.slice(4)}`); + const kind = !current || unknown || result === undefined ? 'unknown' : result === 1 ? 'normal' : 'error'; + const stateLabel = kind === 'normal' ? text('正常', 'Normal') : kind === 'error' ? text('失败', 'Failed') : text('待确认', 'Unconfirmed'); + const title = /轮播/.test(resource.name || '') ? text('轮播任务', 'Slideshow task') : resource.name || text('任务', 'Job'); + return [card(title, enabled ? text('启用', 'Enabled') : text('停用', 'Disabled'), status(kind, stateLabel))]; + } + return metrics.filter(m => m.resource_id === resource.id).map(metric => { + const title = label(metric); + return card(title, metricValue(metric)); + }); + }).join(''); + const frames = resources.filter(resource => resource.type === 'device'); + return `

${esc(entry.host.name || 'Frame Flow')}

${frames.map(frame => `${esc(frame.name || text('画框', 'Frame'))}`).join('')}
${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')}
${!current ? `

${text('以下为上次采集数据', 'Showing the last collected data')}

` : ''}
${cards}
${alertRows ? `

${text('当前告警', 'Current alerts')}

${alertRows}
` : ''}
`; } diff --git a/pyproject.toml b/pyproject.toml index 4f7d01c..68c95a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pigway-console-adapter-frame-flow" -version = "1.0.1" +version = "1.0.2" requires-python = ">=3.12" dependencies = [] diff --git a/tests/ui.cjs b/tests/ui.cjs index 2516da0..3430405 100644 --- a/tests/ui.cjs +++ b/tests/ui.cjs @@ -6,6 +6,8 @@ try { const source=fs.readFileSync(path.join(__dirname,'../plugin/ui/index.js'),'utf8').replace(/^import .*;\n/gm,'').replace('export function hostCard','function hostCard'); const labels={service_ready:{zh:'服务就绪',en:'Service ready'},device_online:{zh:'画框在线',en:'Frame online'},battery_percent:{zh:'电量',en:'Battery'}}; const entry={host:{name:'Frame Flow'},dataCurrent:true,snapshot:{resources:[{id:'service',name:'Frame Flow',type:'service'},{id:'device:living-room',name:'渡口浪人的相框',type:'device'}],metrics:[{resource_id:'service',key:'service_ready',value:1,unit:'boolean',label:labels.service_ready},{resource_id:'device:living-room',key:'device_online',value:1,unit:'boolean',label:labels.device_online},{resource_id:'device:living-room',key:'battery_percent',value:28,unit:'percent',label:labels.battery_percent}],alerts:[]}}; +entry.snapshot.resources.push({id:'job:slideshow',name:'家庭照片轮播',type:'job'}); +entry.snapshot.metrics.push({resource_id:'job:slideshow',key:'job_enabled',value:1,unit:'boolean'},{resource_id:'job:slideshow',key:'last_run_success',value:1,unit:'boolean'}); for(const lang of ['zh','en'])for(const width of [1440,820,390]) { const page=await browser.newPage({viewport:{width,height:1000}}); await page.goto('http://192.168.89.19/'); @@ -14,12 +16,16 @@ const result=await page.evaluate(({source,entry,lang})=>{ const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const html=eval(source+'\nhostCard(entry)'); document.body.innerHTML=`
${html}
`; - return {text:document.body.innerText,overflow:document.documentElement.scrollWidth>innerWidth}; + return {text:document.body.innerText,overflow:document.documentElement.scrollWidth>innerWidth,subtitle:document.querySelector(".host-head .device-details")?.textContent,nested:document.querySelectorAll(".group .metric, .metric .metric").length,status:document.querySelector(".metric [data-semantic]")?.textContent}; },{source,entry,lang}); assert(result.text.includes('渡口浪人的相框')); assert(!result.text.includes('living-room')); assert(result.text.includes(lang==='zh'?'服务就绪':'Service ready')); assert.equal(result.overflow,false); +assert.equal(result.nested,0); +assert.equal(result.subtitle,'渡口浪人的相框'); +assert.equal(result.status,lang==='zh'?'正常':'Normal'); +assert(result.text.includes(lang==='zh'?'轮播任务':'Slideshow task')); await page.close(); } console.log('6 browser checks passed');