From d35f233ef3f5432a147f6ed5ea852e8ea0c91aa8 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Mon, 14 Sep 2026 15:33:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8=E9=87=8F=E8=A1=8C=20pace=20?= =?UTF-8?q?=E5=88=97=E7=9C=9F=200=20=E7=AA=97=E5=8F=A3=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=BC=BA=E5=B8=AD=EF=BC=8Cdiff=3D0=20=E4=B8=89=E8=A7=92?= =?UTF-8?q?=E4=BD=8D=E7=94=BB=E7=AD=89=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude 5h 空闲时接口回 utilization 0、resets_at null,算不出 time_pct, 渲染器整列跳过 pace 导致该行进度条比别行长。数据层对 pct=0 且无重置时刻的 窗口记 time_pct=0;基类 _usage_row 只要有百分比就画 pace(缺 time_pct 按 0)。 diff=0 时三角位由留空改为画等宽等号「=」,预留逻辑从宽版下沉到基类,小屏同样齐平。 Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 4 ++-- data.py | 4 +++- renderer.py | 18 ++++++++++++------ renderer_e1002.py | 5 ----- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8308e66..6aa31f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co > **分支 `feature/colorful`**:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为**上中下三段**:A 大时钟+日期 / 天气(定位南京·江宁);B **Claude Usage** 主角(5h/7d 用量进度条 + 时间维度对比 pace,用量吃紧转红预警);C 橘喵今日实时三列。红作**语义强调**(用量预警、涨、定位、天气太阳/闪电),仍**无真灰阶、不抖动**,全图只三种纯色。近30天趋势图当前**隐藏**(`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。 -> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/84**(`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体;24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC/设备遥测 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`/`DEVICE_TTL`,默认 900/60/300/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/Codex 用量为**账户级、形态 A**:本机 `main.py --target push` 采集(Claude 复用 Claude Code 钥匙串登录态调 `/api/oauth/usage`;Codex 经 `codex app-server` RPC)后推送原始快照(`X-Push-Token` 鉴权),服务端内存暂存、**不持任何凭证**、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 大时钟 **F84**(墨迹 16~79)+ 其右一列三行、左缘对齐(F12,行距 22):① 电池图标+电量(充电中绿、≤20% 红)② 温度计+温度 | 蓝色水滴+湿度 ③ 星期/日期(与时钟基线对齐)——设备数据来自 **SenseCraft 设备遥测** `sensecraft_api.py`(2026-09-14 新增;先做过「状态栏横排在时钟上方+时钟缩 F60」被否)/天气;B 左 **Claude Usage 两行(5h / 合并行 7d——整行高度不变、**一个大边框**,边框内上下精确对半两条 7px **实心**填充(紧挨、无空隙,2026-09-14 用户要求去掉白隔):上=总 7d、下=fable 7d,各按自身档位取色;右侧百分比与 pace **只显示两者中较大者**,边框与文字随其档位取色;`E1002Renderer._usage_row_split` 以较大者合成一条交给基类 `_usage_row`(已返回条几何)画边框/文字后在内部重绘两条半高实心。2026-09-14 为与右块对称而合并,先做过「两条各带边框的细条+两行数字」版被否;标签去掉 all 前缀)**、右 **Codex Usage 两行(5h / 7d——ChatGPT 订阅的 Codex 额度池;服务端未收到推送时 mock 标「示例数据」)**。宽版用量行的**空值不显示 "--"**:pct 为 None 按 0 画("00%"、pace "00"、空条、全黑中性色),且 pace 列在 diff=0 时仍预留三角位——各行进度条右端齐平、等宽(2026-09-14;Codex Plus 接口 `secondary_window` 就是 null,原始 `wham/usage` 已核对,5h 行为空是数据如此),紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」|30~80 黑「正常」|≥ `USAGE_WARN` 80 红「危险」;pace ≤ −`PACE_TOL` 10 绿「节余」|±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**:2026-09-14 曾按要求加过 60%~80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。**今日实时的环比/同比「三角 + 数值」涨绿跌红**(2026-09-14 改,与 K 线一致;基类钩子 `_cmp_color`,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。 +> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/84**(`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体;24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC/设备遥测 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`/`DEVICE_TTL`,默认 900/60/300/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/Codex 用量为**账户级、形态 A**:本机 `main.py --target push` 采集(Claude 复用 Claude Code 钥匙串登录态调 `/api/oauth/usage`;Codex 经 `codex app-server` RPC)后推送原始快照(`X-Push-Token` 鉴权),服务端内存暂存、**不持任何凭证**、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 大时钟 **F84**(墨迹 16~79)+ 其右一列三行、左缘对齐(F12,行距 22):① 电池图标+电量(充电中绿、≤20% 红)② 温度计+温度 | 蓝色水滴+湿度 ③ 星期/日期(与时钟基线对齐)——设备数据来自 **SenseCraft 设备遥测** `sensecraft_api.py`(2026-09-14 新增;先做过「状态栏横排在时钟上方+时钟缩 F60」被否)/天气;B 左 **Claude Usage 两行(5h / 合并行 7d——整行高度不变、**一个大边框**,边框内上下精确对半两条 7px **实心**填充(紧挨、无空隙,2026-09-14 用户要求去掉白隔):上=总 7d、下=fable 7d,各按自身档位取色;右侧百分比与 pace **只显示两者中较大者**,边框与文字随其档位取色;`E1002Renderer._usage_row_split` 以较大者合成一条交给基类 `_usage_row`(已返回条几何)画边框/文字后在内部重绘两条半高实心。2026-09-14 为与右块对称而合并,先做过「两条各带边框的细条+两行数字」版被否;标签去掉 all 前缀)**、右 **Codex Usage 两行(5h / 7d——ChatGPT 订阅的 Codex 额度池;服务端未收到推送时 mock 标「示例数据」)**。宽版用量行的**空值不显示 "--"**:pct 为 None 按 0 画("00%"、pace "00"、空条、全黑中性色),且 pace 列在 diff=0 时三角位画一个等宽等号「=」(2026-09-14 用户要求,原为留空、后短暂用过「-」;已下沉到基类 `_pace`,小屏同样)——各行进度条右端齐平、等宽(2026-09-14;Codex Plus 接口 `secondary_window` 就是 null,原始 `wham/usage` 已核对,5h 行为空是数据如此)。**真 0 的窗口也必须有 pace 列**:Claude 5h 空闲时接口回 `utilization: 0.0, resets_at: null`,算不出 time_pct,早先渲染器遇「有 pct 无 time_pct」整列跳过,该行条就比别行长——现 `data.usage_from_raw` 对 pct=0 且无重置时刻的窗口记 time_pct=0(窗口未开始),基类 `_usage_row` 只要有 pct 就画 pace(time_pct 缺失按 0 差值画 "00"),紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」|30~80 黑「正常」|≥ `USAGE_WARN` 80 红「危险」;pace ≤ −`PACE_TOL` 10 绿「节余」|±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**:2026-09-14 曾按要求加过 60%~80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。**今日实时的环比/同比「三角 + 数值」涨绿跌红**(2026-09-14 改,与 K 线一致;基类钩子 `_cmp_color`,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。 ## 命令 @@ -44,7 +44,7 @@ EPD_HOST=epd400a44.local python3 main.py - **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,两路用量经本机采集器);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)`(`_usage(now, raw=)`/`_usage_gpt(now, raw=, allow_local=)` 都基于它,raw=None 时前者经 `claude_usage` 本机采集、后者经 `codex_usage` 本机采集,Codex 采集失败或服务端 `allow_local=False` 时用 `gpt_mock_raw()` 并标 mock)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()`(`TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict:`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`/`device`(`device` = `{battery, charging, temp, humidity}`,由 `_device(now)` 经 `sensecraft_api` 拉取、文件缓存 `.device_cache.json` TTL=`DEVICE_TTL`,服务端用纯拉取 `device_fetch()`/占位 `device_empty()`)。日期时间用真实系统时间;橘喵今日经营由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·江宁)、Claude 用量由 `_usage()` 经 `claude_usage`、Codex 用量由 `_usage_gpt()` 经 `codex_usage`,**Claude 任何异常都回退占位 `"--"`,不回退 mock**(Codex 无数据时回退 mock 并显式标「示例数据」)。改数据保持返回结构不变即可(renderer 依赖其 key:`weather.icon` 为和风图标代码;`mao.cols[]` 为 `{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()` 用 `.get()` 取 `today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--`;`wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`,direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null`);`usage.bars[]` 为 `{k(短标签,小屏用), label(长标签 5h/7d/fable 7d,宽版用), pct(或 None), time_pct?, reset?, scoped}`(宽版把 scoped 行并入紧邻其前的非 scoped 行作同一边框内的下半实心)、`usage.warn`/`usage.ok`/`usage.pace_tol` 为用量与 pace 的三档阈值(小屏只用 warn)、`usage.updated` 为快照采集时刻;`usage_gpt` 与 `usage` 同构、标题「Codex Usage」、只有 5h/7d 两行(快照各窗口可带 `window` 真实秒数,pace 优先按它算),服务端未收到推送时为 mock(`mock: True`,88/22 覆盖红/绿两端)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc`:`_btc()` 经 `btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48),结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`,chg 时 K 取相对 24 根前收盘(24h)、日 K 取相对上一根;缓存 `.btc_cache.json`(TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。 - **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`(TTL=`WEATHER_TTL`,默认 15min)、趋势 `.trend_cache.json`(按日期 key,每天只拉一次)、BTC K 线 `.btc_cache.json`(TTL=`BTC_TTL`,默认 5min);三者拉取失败沿用旧缓存。两路用量各有采集缓存 `.claude_usage_cache.json`/`.codex_usage_cache.json`(`usage_common.cached_snapshot`:距上次成功 0 超前↑ / <0 节余↓);故即使 `utilization` 来自缓存/推送暂存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s(Codex 快照带 `window` 时用真实值)。 + - **用量的 pace(时间维度对比)**:`usage_from_raw()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)`、`pace=pct−time_pct`(>0 超前↑ / <0 节余↓);窗口空闲(pct=0 且 `resets_at` 为 null)记 time_pct=0;故即使 `utilization` 来自缓存/推送暂存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s(Codex 快照带 `window` 时用真实值)。 - **`jm_api.py`** — jm-devops 后端统计接口客户端,**设备密钥授权**(`grant_type=client_secret`,免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` 调 `POST /auth/login` 换 token(进程内缓存、按 `expire_in` 提前刷新),再带 `Authorization: Bearer ` + `clientid` 头调 `/system/statistics/{realtime,recent-days}`;遇 401 清 token 重登一次重试。token 字段为蛇形 `access_token`/`expire_in`。注:历史上曾免鉴权直调、更早曾用账号密码+验证码 OCR 登录(ddddocr 不稳定),现统一为 client_secret 授权——密钥即长期凭证,务必走 HTTPS、优先用 `JM_CLIENT_SECRET` 环境变量、可在后端 `sys_client` 轮换。 - **`weather_api.py`** — 和风天气(QWeather)客户端。用**用户专属 API Host**(`QWEATHER_HOST`)+ `X-QW-Api-Key` 头鉴权;响应 **gzip 压缩**(按 magic number 手动解压)、返回 `code` 为**字符串**。`get_now()` 取实时(`now.text` 中文天气 / `now.temp` / `now.icon` 图标代码),`get_today()` 取 `/3d` 的 `daily[0]` 今日温区。`QWEATHER_HOST`/`QWEATHER_KEY` 未配置时直接抛错→天气区显示 `"--"`。 - **`sensecraft_api.py`** — SenseCraft HMI 设备遥测客户端:`GET {SENSECRAFT_API_BASE}/api/v1/user/device/iot_data/{SENSECRAFT_DEVICE_EUI}`,头 `api-key`(用户在 SenseCraft 后台生成,放 `.env`,**2026-09-14 该 key 曾出现在对话里,建议轮换**),取 `result.battery.level/charging`、`result.sensor.temp/humidity`;设备每 `dataaccess.interval`=300s 上报一次,故 `DEVICE_TTL` 默认 300。直连不走代理、超时 10s、`code!=200` 抛错 → 状态栏 `--`。 diff --git a/data.py b/data.py index a585d35..bfc842b 100644 --- a/data.py +++ b/data.py @@ -334,7 +334,7 @@ GPT_WINDOWS = ( def usage_from_raw(now, raw, windows, title): """原始快照 → 渲染结构(Claude / ChatGPT 通用)。 raw 形如 {five_hour:{utilization, resets_at, window?}, seven_day:{...}, :{...}|null, updated_at?}; - utilization 为已用百分比,resets_at 为 epoch 秒或 ISO,window 为该窗口真实长度秒(缺省用 windows 表的常量);缺哪个窗口哪个显示 "--"。 + utilization 为已用百分比,resets_at 为 epoch 秒或 ISO,window 为该窗口真实长度秒(缺省用 windows 表的常量);缺哪个窗口哪个显示 "--"(宽版按 0 画);utilization 为 0 且 resets_at 为 null 的空闲窗口记 time_pct=0,保证 pace 列不缺席。 time_pct(时间已流逝%)与 pace 按传入的 now **现算**——所以推送端只需推原始快照,服务端渲染时 pace 仍随时钟准确推进。 结构契约(renderer 依赖):{title, warn, ok, pace_tol, updated?, bars:[{k, label, pct(0~100 或 None), time_pct?, reset?, scoped}]}; pct ≥ warn 时该条进度条与百分比转红(宽版另按 ok/warn 分 绿/黑/红 三档,百分比随条色); @@ -349,6 +349,8 @@ def usage_from_raw(now, raw, windows, title): bar = {"k": k, "label": label, "pct": pct, "scoped": scoped} win = int(seg.get("window") or win) tp = _window_time_pct(seg.get("resets_at"), win, now) + if pct is not None and tp is None and pct == 0: + tp = 0 # 窗口空闲(utilization 0 且 resets_at null,5h 窗口重置后常见):窗口尚未开始,已流逝 0%、pace 持平 if pct is not None and tp is not None: bar["time_pct"] = tp rs = _fmt_reset(seg.get("resets_at"), win) diff --git a/renderer.py b/renderer.py index f2886d4..4fdd622 100644 --- a/renderer.py +++ b/renderer.py @@ -162,16 +162,21 @@ class DashboardRenderer: def _pace(self, xr, cy, diff): """时间维度对比(pace = 用量% − 时间%):>0 超前↑红;<0 节余↓黑;=0 持平。 - 数值(FPACE,默认 F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。""" + 数值(FPACE,默认 F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。 + diff=0(持平/无数据)三角位画一个与三角等宽的等号「=」(2026-09-14 用户要求,原为留空,后曾用「-」)——pace 列每行等宽,各行进度条右端齐平。""" up = diff > 0 color = self._pace_color(diff) txt = f"{abs(int(diff)):02d}" tw = self._tw(txt, self.FPACE) self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r") - if diff == 0: - return tw TRI_GAP, TRI = 4 * self.k, 6 * self.k - self.tri(xr - tw - TRI_GAP - TRI / 2, cy, TRI, up=up, fill=color) + cx = xr - tw - TRI_GAP - TRI / 2 + if diff != 0: + self.tri(cx, cy, TRI, up=up, fill=color) + else: + k = self.k # 等号「=」:两道横杠,各宽 6k、厚 k,中间隔 2k,整体以 cy 为中心 + for top in (cy - 2 * k, cy + k): + self.d.rectangle([cx - TRI / 2, top, cx + TRI / 2 - 1, top + k - 1], fill=color) return tw + TRI_GAP + TRI def _usage_row(self, x0, x1, y, bar, warn, label_w=None): @@ -195,8 +200,9 @@ class DashboardRenderer: self._mid(x0, cy, bar["k"], self.F13, bold=lb) # 右侧从右往左:pace(最右) → 百分比 → 进度条 pct_r = x1 - if ok and "time_pct" in bar: - pace_w = self._pace(x1, cy, pct - int(bar["time_pct"])) + if ok: + # 有百分比就必画 pace 列:time_pct 缺失(窗口无重置时刻)按 0 差值画 "00",不能整列省掉——否则该行进度条会变长、右端错位 + pace_w = self._pace(x1, cy, pct - int(bar["time_pct"]) if "time_pct" in bar else 0) pct_r = x1 - pace_w - G2 pct_txt = f"{pct:02d}%" if ok else "--" self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=k, align="r", track=PCT_TRACK) # 百分比 F16(主) diff --git a/renderer_e1002.py b/renderer_e1002.py index 91c2a53..2ad0863 100644 --- a/renderer_e1002.py +++ b/renderer_e1002.py @@ -75,11 +75,6 @@ class E1002Renderer(DashboardRenderer): bar = dict(bar, pct=0, time_pct=0, _null=True) return super()._usage_row(x0, x1, y, bar, warn, label_w=label_w) - def _pace(self, xr, cy, diff): - """宽版:diff=0(持平/无数据)不画三角,但**仍预留三角位**(6 + 间隙 4),使每行 pace 列等宽、进度条右端齐平。""" - w = super()._pace(xr, cy, diff) - return w if diff != 0 else w + 10 * self.k - def _usage_colors(self, ok, pct, warn, bar): """进度条填充、边框、百分比数字三者同色:pct ≤ ok 绿「健康」|其间黑「正常」|pct ≥ warn 红「危险」;无数据(_null)黑。""" if not ok or bar.get("_null"):