v1.0.10 两件大事:
1. 界面中英双语(英文界面无中文残留)
- 新增 web/i18n.js(约 4200 条词条),零依赖零构建
- 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
realtime/update)纳入翻译并注册切语言重绘钩子
- 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
- 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义
2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
- 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
- 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
- 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
- .dep-item 横向容器直接塞块级子项 → 退回块级
- 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
- 守卫 t-layout.py 扩到 35 条,12 处反向验证全红
3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
uptime 归零后相减得负数 → 恢复比对 boot_id
附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
170 lines
7.4 KiB
Python
170 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""模块健康总览(#5)的静态守卫。
|
||
|
||
守的是「三张表必须对齐」这件事 —— 后端 read:status 只回**稳定枚举**,
|
||
所有展示文案都在前端 i18n 里;一旦后端加了新模块/新状态码而前端没补词条,
|
||
英文界面就会露出 key 原文(如 st.k.xxx),而**没有**这条守卫的话,
|
||
t-en-render 也发现不了(它只看「有没有中文字符」,key 原文是 ASCII)。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
PASS = FAIL = 0
|
||
|
||
|
||
def chk(name, cond, extra=''):
|
||
global PASS, FAIL
|
||
if cond:
|
||
PASS += 1
|
||
print(' ok %s' % name)
|
||
else:
|
||
FAIL += 1
|
||
print(' FAIL %s %s' % (name, ('→ ' + str(extra)) if extra else ''))
|
||
|
||
|
||
def rd(rel):
|
||
return io.open(os.path.join(ROOT, rel), encoding='utf-8').read()
|
||
|
||
|
||
|
||
|
||
HELPER = rd('backend/drouter-helper.py')
|
||
WEBPY = rd('backend/drouter-web.py')
|
||
APP = rd('web/app.js')
|
||
I18N = rd('web/i18n.js')
|
||
FIX = rd('_real_fixtures.js')
|
||
|
||
def st_block(name):
|
||
"""取 i18n.js 里 st: { <name>: { ... } } 这一层的文本。
|
||
|
||
⚠️ 必须按子块取,不能用整文件正则:`system` 既是 k 组的模块名,
|
||
也是 h 组的提示语键。整文件搜索时删掉 k.system,h.system 会顶上,
|
||
判据照样变绿(实测踩过)。
|
||
"""
|
||
i = I18N.find('\n st: {\n')
|
||
if i < 0:
|
||
return ''
|
||
j = I18N.find('\n %s: {\n' % name, i)
|
||
if j < 0:
|
||
return ''
|
||
k = I18N.find('\n },\n', j + 1)
|
||
return I18N[j:k] if k > 0 else ''
|
||
|
||
|
||
ST_K = st_block('k')
|
||
ST_H = st_block('h')
|
||
ST_C = st_block('c')
|
||
ST_M = st_block('m')
|
||
|
||
# ---------- 1. 后端 ----------
|
||
m = re.search(r'\ndef read_status\(_\):\n([\s\S]*?)\n\ndef ', HELPER)
|
||
chk('helper 里有 read_status', bool(m))
|
||
chk('i18n 里 st.k / st.h / st.c / st.m 四个子块都取到了',
|
||
all(len(x) > 200 for x in (ST_K, ST_H, ST_C, ST_M)),
|
||
'len=%s' % [len(x) for x in (ST_K, ST_H, ST_C, ST_M)])
|
||
body = m.group(1) if m else ''
|
||
chk("ACTIONS 注册了 read:status", "'read:status': read_status," in HELPER)
|
||
chk("web 路由注册了 /api/status", "'/api/status': 'read:status'," in WEBPY)
|
||
|
||
# 去掉注释与 docstring 后再查中文:read_status 只该回枚举与数值
|
||
stripped = re.sub(r'""".*?"""', '', body, flags=re.S)
|
||
stripped = re.sub(r'#[^\n]*', '', stripped)
|
||
# log(...) 是**内部日志行**(进系统日志,不下发给前端),中文是允许的;
|
||
# 不排掉的话这条判据会被「健康总览 X 判定失败」这种日志串误伤。
|
||
stripped = re.sub(r"log\((?:[^()]|\([^()]*\))*\)", '', stripped)
|
||
cjk = re.findall(r'[\u4e00-\u9fff]', stripped)
|
||
chk('read_status 不下发任何中文文案', not cjk, ''.join(cjk[:20]))
|
||
|
||
# 轮询安全:必须有结果缓存 + TTL
|
||
chk('有结果缓存(_STATUS_CACHE)', '_STATUS_CACHE' in HELPER)
|
||
tm = re.search(r'_STATUS_TTL\s*=\s*(\d+)', HELPER)
|
||
chk('_STATUS_TTL 是正数', bool(tm) and int(tm.group(1)) > 0,
|
||
tm.group(1) if tm else 'missing')
|
||
# ⚠️ read_metrics 内含 ping 测 RTT,实测 1.2s —— 轮询接口里绝不能出现它
|
||
chk('read_status 不调 read_metrics(含 1.2s 的 ping 探测)',
|
||
'read_metrics(' not in stripped)
|
||
chk('资源采集走零 fork 的 _st_resources', '_st_resources()' in stripped)
|
||
|
||
# ---------- 2. 模块键 → i18n st.k.* / st.h.* ----------
|
||
keys = re.findall(r"_run\('([a-z0-9_]+)'", body)
|
||
chk('read_status 至少覆盖 15 个模块', len(keys) >= 15, '实际 %d' % len(keys))
|
||
miss_k = [k for k in keys if (" %s:" % k) not in I18N or
|
||
("st.k." + k) not in APP]
|
||
bad_k = []
|
||
for k in keys:
|
||
if not re.search(r"\n %s: *\{ zh: '[^']*', en: '[^']*' \}"
|
||
% re.escape(k), ST_K):
|
||
bad_k.append(k)
|
||
chk('每个模块键都有 st.k.<键> 词条', not bad_k, str(bad_k))
|
||
|
||
# st.h.<键>:提示语。允许缺失(前端会留空),但至少覆盖 80%
|
||
hits = [k for k in keys
|
||
if re.search(r"\n %s: *\{ zh: '[^']*', en: '[^']*' \}" % re.escape(k), ST_H)]
|
||
chk('模块提示语 st.h.* 覆盖 >= 80%%', len(hits) >= len(keys) * 0.8,
|
||
'%d/%d' % (len(hits), len(keys)))
|
||
|
||
# ---------- 3. 状态码 → i18n st.c.* ----------
|
||
codes = set()
|
||
codes |= set(re.findall(r"return '[a-z_]+', '([a-z_]+)'", body))
|
||
# ⚠️ 这个形状里**第一个**字符串是等级、第二个才是状态码:
|
||
# lv, code = 'warn', 'noaddr' → 码是 noaddr 而不是 warn。
|
||
codes |= set(re.findall(r"lv, code = '[a-z_]+', '([a-z_]+)'", body))
|
||
codes |= set(re.findall(r"it\['lv'\], it\['c'\] = '[a-z_]+', '([a-z_]+)'", body))
|
||
# ⚠️ 不要再加 `\['lv'\] = '(...)'` 或 `return \('(...)' if ..., '...'\)` 这类
|
||
# 取**第一个**字符串的模式 —— 第一个永远是等级,会把 'warn'/'err'
|
||
# 当成状态码,然后误报「缺 st.c.warn 词条」(实测踩过)。
|
||
codes |= set(re.findall(r"return \('[a-z_]+' if [^\n]*? else '[a-z_]+'\), "
|
||
r"\('([a-z_]+)' if [^\n]*? else '[a-z_]+'\)", body))
|
||
codes |= set(re.findall(r"\['c'\] = '([a-z_]+)'", body))
|
||
# _svc_lv 的返回
|
||
codes |= {'running', 'starting', 'failed', 'stopped', 'disabled', 'unknown'}
|
||
chk('识别到 >= 12 个状态码', len(codes) >= 12, '实际 %d' % len(codes))
|
||
miss_c = sorted(c for c in codes
|
||
if not re.search(r"\n %s: *\{ zh: '[^']*', en: '[^']*' \}"
|
||
% re.escape(c), ST_C))
|
||
chk('每个状态码都有 st.c.<码> 词条', not miss_c, str(miss_c))
|
||
|
||
# ---------- 4. 指标名:真机 fixture 的键必须都能展示 ----------
|
||
mf = re.search(r'const REAL_STATUS = (\{[\s\S]*?\n\});', FIX)
|
||
chk('fixture 里有 REAL_STATUS', bool(mf))
|
||
if mf:
|
||
data = json.loads(mf.group(1))
|
||
real_keys = set()
|
||
for it in data.get('items') or []:
|
||
real_keys |= set((it.get('m') or {}).keys())
|
||
chk('真机 payload 至少 20 个指标键', len(real_keys) >= 20, '实际 %d' % len(real_keys))
|
||
|
||
stm = re.search(r'const ST_METRICS = \[([\s\S]*?)\n\];', APP)
|
||
chk('app.js 有 ST_METRICS 表', bool(stm))
|
||
listed = set(re.findall(r"\['([a-z0-9_]+)',", stm.group(1))) if stm else set()
|
||
hidden = sorted(real_keys - listed)
|
||
chk('真机所有指标键都在 ST_METRICS 里(不会静默不显示)', not hidden, str(hidden))
|
||
miss_m = sorted(k for k in listed
|
||
if not re.search(r"\n %s: *\{ zh: '[^']*', en: '[^']*' \}"
|
||
% re.escape(k), ST_M))
|
||
chk('ST_METRICS 每项都有 st.m.<名> 词条', not miss_m, str(miss_m))
|
||
|
||
# ---------- 5. 前端接线 ----------
|
||
chk('VIEWS 注册了 health', re.search(r'\n health: viewHealth,', APP) is not None)
|
||
chk('导航里有健康总览入口', "{ k: 'health', n: T('st.nav')" in APP)
|
||
chk('HEALTH_TIMER 进 stopPageTimers(切页要停轮询)',
|
||
'DK_TIMER, HEALTH_TIMER]' in APP and 'DK_TIMER = HEALTH_TIMER = null' in APP)
|
||
chk('顶栏状态灯挂进 boot()', 'initHealthLight();' in APP)
|
||
chk('本页状态灯容器存在(index.html)', 'id="page-health"' in rd('web/index.html'))
|
||
chk('本页灯在 go() 里同步', 'paintPageLight(); // 本页模块状态灯' in APP)
|
||
|
||
# 单位不能写中文(会原样漏进英文界面)
|
||
unit_cjk = re.findall(r"\['[a-z0-9_]+', ' ([^']*[\u4e00-\u9fff][^']*)'\]", APP)
|
||
chk('ST_METRICS 的单位不含中文', not unit_cjk, str(unit_cjk))
|
||
|
||
print('\n==== t-status: %d 条判据, %d 失败 ====' % (PASS + FAIL, FAIL))
|
||
if FAIL:
|
||
print('STATUS_GUARD_FAIL')
|
||
sys.exit(1)
|
||
print('STATUS_GUARD_OK')
|