Files
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
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/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

117 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""t-layout 判据的反向验证:逐个把修复点改坏,确认对应判据变红。
一次只注入一处 —— 一次注入多处时中间某处静默跳过会给出假绿(这条踩过)。
改的是工作区真实文件,跑完立刻还原(finally)。
"""
import io
import os
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CSS = os.path.join(ROOT, 'web', 'app.css')
APP = os.path.join(ROOT, 'web', 'app.js')
PY = sys.executable
MUTS = [
('app.css', 'th,td 去掉 keep-all',
'vertical-align:top;word-break:keep-all;overflow-wrap:break-word',
'vertical-align:top;overflow-wrap:break-word',
'th,td 用 word-break:keep-all'),
('app.css', 'th 去掉 nowrap',
'background:#fafbfe;position:sticky;top:0;\n white-space:nowrap}',
'background:#fafbfe;position:sticky;top:0}',
'th 用 white-space:nowrap'),
('app.css', '.tag 改回 nowrap',
'white-space:normal;word-break:keep-all;overflow-wrap:break-word}',
'white-space:nowrap;word-break:normal}',
'.tag 用 word-break:keep-all'),
('app.css', '.row>label 去掉 keep-all',
'.row>label{word-break:keep-all}', '.row>label{word-break:normal}',
'.row>label 用 word-break:keep-all'),
('app.css', '#al-rules>.dep-item 改回 flex',
'#al-rules>.dep-item{display:block}', '#al-rules>.dep-item{display:flex}',
'#al-rules>.dep-item 是块级'),
('app.css', '.dk-trow>* 去掉 min-width:0',
'.dk-trow>*{min-width:0;overflow-wrap:break-word}',
'.dk-trow>*{overflow-wrap:break-word}',
'.dk-trow 单元格可断行'),
('app.css', '.ul-trow>* 去掉 min-width:0',
'.ul-trow>*{min-width:0;overflow-wrap:break-word}',
'.ul-trow>*{overflow-wrap:break-word}',
'.ul-trow 单元格可断行'),
('app.css', '.dep-item 去掉 flex-wrap',
'gap:11px;flex-wrap:wrap;padding:11px 13px', 'gap:11px;padding:11px 13px',
'.dep-item 允许换行'),
('app.css', '.dep-body 改回裸 flex:1',
'.dep-item .dep-body{flex:1 1 200px;min-width:0}',
'.dep-item .dep-body{flex:1;min-width:0}',
'.dep-item .dep-body 的 flex-basis 有像素下限'),
('app.js', 'app.js 加回内联 flex:1',
'class="dep-body" style="flex:1 1 200px"', 'class="dep-body" style="flex:1"',
'内联 style="flex:1"'),
('app.css', '.switch 去掉 flex-wrap',
'gap:8px;flex-wrap:wrap;cursor:pointer', 'gap:8px;cursor:pointer',
'.switch 允许换行'),
('app.css', '.switch b 去掉不收缩',
'.switch b{flex:0 0 auto}', '.switch b{flex:0 1 auto}',
'.switch b 不收缩'),
]
FILES = {'app.css': CSS, 'app.js': APP}
def run():
p = subprocess.run([PY, os.path.join(ROOT, '_dev', 't-layout.py')],
capture_output=True, text=True, encoding='utf-8',
errors='replace')
return p.returncode, (p.stdout or '') + (p.stderr or '')
orig = {k: io.open(v, encoding='utf-8').read() for k, v in FILES.items()}
bak = {k: v + '.rvbak' for k, v in FILES.items()}
for k, v in FILES.items():
shutil.copyfile(v, bak[k])
rc, out = run()
print('基线: rc=%s %s' % (rc, 'LAYOUT_OK' if 'LAYOUT_OK' in out else '异常!'))
if rc != 0:
print('⚠️ 基线不干净,先停下')
for k, v in FILES.items():
shutil.copyfile(bak[k], v)
sys.exit(1)
bad = 0
try:
for fn, name, old, new, expect in MUTS:
src = orig[fn]
if src.count(old) < 1:
print(' [SKIP] %-26s 替换没命中(锚点变了?)' % name)
bad += 1
continue
io.open(FILES[fn], 'w', encoding='utf-8', newline='\n').write(
src.replace(old, new))
rc, out = run()
red = rc != 0 and expect in out
print(' [%s] %-26s 期望变红 → rc=%s %s'
% ('OK' if red else 'BAD', name, rc,
'(%s 命中)' % expect if red else '⚠️ 没变红'))
if not red:
bad += 1
for line in out.splitlines():
if 'FAIL' in line:
print(' ' + line.strip())
io.open(FILES[fn], 'w', encoding='utf-8', newline='\n').write(src)
finally:
for k, v in FILES.items():
io.open(v, 'w', encoding='utf-8', newline='\n').write(orig[k])
os.remove(bak[k])
rc, out = run()
print('还原后: rc=%s %s' % (rc, 'LAYOUT_OK' if 'LAYOUT_OK' in out else '异常!'))
print('\n反向验证: %d 处变异, %d 处未按预期变红' % (len(MUTS), bad))
sys.exit(1 if bad else 0)