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/排版排查的一次性脚本。
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
#!/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)
|