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/排版排查的一次性脚本。
180 lines
7.5 KiB
JavaScript
180 lines
7.5 KiB
JavaScript
/* ============================ 网络明细(邻居 / 路由 / 规则)============================
|
||
概览页的「网络明细」卡片组。这三张表是排障时最常看的:
|
||
邻居表回答「这台设备是谁」,路由表回答「流量往哪走」,规则表回答「策略怎么定」。
|
||
|
||
⚠️ 三条显示上的诚实性约束(都来自内核语义的坑):
|
||
① 邻居表里的 **STALE** 不是故障 —— 它表示「这个 MAC 曾经用过,
|
||
现在静默」。界面上必须弱化显示,否则用户看到一堆 STALE 会以为网络坏了。
|
||
真正要标红的是 **FAILED**(ARP/ND 解析失败)。
|
||
② 路由表只取 **main 表**。loopback 的 local 表(`::1`、`fe80::1`)
|
||
有十几条,全是噪音,会把真正要看的 default 路由淹掉。
|
||
③ IPv6 规则表里优先级 0 / 32766 是**内核自动建的**,用户没配过。
|
||
标出来,否则用户会去「删除」它。
|
||
------------------------------------------------------------------------ */
|
||
(function () {
|
||
'use strict';
|
||
if (window.__drouterNetDetail) return;
|
||
window.__drouterNetDetail = true;
|
||
|
||
const $ = s => document.querySelector(s);
|
||
const esc = window.__drouterEsc || (x => {
|
||
if (x == null) return '';
|
||
if (typeof x === 'object') { try { x = JSON.stringify(x); } catch (e) { /* 循环引用 */ } }
|
||
return String(x).replace(/[&<>"']/g,
|
||
c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
});
|
||
|
||
let TIMER = null;
|
||
|
||
const EMPTY = '<p class="desc" style="margin:6px 0 0">' + t('nd.empty') + '</p>';
|
||
|
||
/* ---------- 邻居表 ---------- */
|
||
function neighTable(list, isV6) {
|
||
if (!list || !list.length) return EMPTY;
|
||
// STALE 排最后:活跃的在前,用户不用往下翻
|
||
const sorted = list.slice().sort((a, b) => {
|
||
const rank = x => (x.failed ? 0 : (x.permanent ? 1 : (x.stale ? 3 : 2)));
|
||
return rank(a) - rank(b) || String(a.dst).localeCompare(String(b.dst));
|
||
});
|
||
const rows = sorted.map(x => {
|
||
// 状态标签:只有 FAILED 标红,STALE 灰显(它是正常的)
|
||
let tag = '';
|
||
if (x.failed) tag = '<span class="tag warn">' + t('nd.failed') + '</span>';
|
||
else if (x.permanent) tag = '<span class="tag gray">' + t('nd.perm') + '</span>';
|
||
else if (x.stale) tag = '<span class="tag gray">' + t('nd.stale') + '</span>';
|
||
else tag = '<span class="tag ok">' + t('nd.active') + '</span>';
|
||
return `<tr>
|
||
<td class="mono">${esc(x.dst)}</td>
|
||
<td class="mono">${esc(x.mac || '—')}</td>
|
||
<td class="mono">${esc(x.dev || '—')}</td>
|
||
<td>${tag}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
const stale = list.filter(x => x.stale).length;
|
||
const failed = list.filter(x => x.failed).length;
|
||
return `<table class="tbl">
|
||
<thead><tr><th>${isV6 ? t('nd.ipv6Addr') : t('nd.ipv4Addr')}</th>
|
||
<th>${t('nd.mac')}</th><th>${t('common.interface')}</th><th>${t('common.state')}</th></tr></thead>
|
||
<tbody>${rows}</tbody></table>
|
||
<p class="desc" style="margin:7px 0 0">
|
||
${t('nd.countTip', [list.length])}${stale ? t('nd.staleTip', [stale]) : ''}
|
||
</p>`;
|
||
}
|
||
|
||
/* ---------- 路由表 ---------- */
|
||
function routeTable(list, isV6) {
|
||
if (!list || !list.length) return EMPTY;
|
||
const rows = list.map(x => {
|
||
// default 路由排在最前 —— 它最重要
|
||
const isDef = x.dst === 'default' || x.dst === '0.0.0.0/0';
|
||
return `<tr${isDef ? ' class="row-def"' : ''}>
|
||
<td class="mono">${esc(x.dev || '—')}</td>
|
||
<td class="mono">${isDef ? '<b>' + t('nd.defRoute') + '</b>' : esc(x.dst)}</td>
|
||
<td class="mono">${esc(x.gw || '—')}</td>
|
||
<td class="mono">${esc(x.src || '—')}</td>
|
||
<td class="mono">${x.metric === '' ? '—' : esc(x.metric)}</td>
|
||
<td>${esc(x.table || 'main')}</td>
|
||
<td>${esc(x.proto || '—')}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
return `<table class="tbl">
|
||
<thead><tr><th>${t('common.device')}</th><th>${t('nd.dst')}</th><th>${t('nd.gw')}</th><th>${t('nd.srcAddr')}</th>
|
||
<th>${t('nd.metric')}</th><th>${t('nd.table')}</th><th>${t('nd.proto')}</th></tr></thead>
|
||
<tbody>${rows}</tbody></table>
|
||
<p class="desc" style="margin:7px 0 0">${t('nd.mainTableOnly')}</p>`;
|
||
}
|
||
|
||
/* ---------- IPv6 规则表 ---------- */
|
||
function ruleTable(list) {
|
||
if (!list || !list.length) return EMPTY;
|
||
const rows = list.map(x => `<tr>
|
||
<td class="mono">${esc(x.prio)}</td>
|
||
<td class="mono">${esc(x.src || 'all')}</td>
|
||
<td class="mono">${esc(x.table || '—')}</td>
|
||
<td>${x.builtin
|
||
? '<span class="tag gray">' + t('nd.builtin') + '</span>'
|
||
: '<span class="tag info">' + t('nd.userPol') + '</span>'}</td>
|
||
</tr>`).join('');
|
||
return `<table class="tbl">
|
||
<thead><tr><th>${t('nd.prio')}</th><th>${t('nd.srcAddr')}</th><th>${t('nd.table')}</th><th>${t('nd.source')}</th></tr></thead>
|
||
<tbody>${rows}</tbody></table>
|
||
<p class="desc" style="margin:7px 0 0">${t('nd.ruleBuiltinTip')}</p>`;
|
||
}
|
||
|
||
function card(title, note, body) {
|
||
return `<div class="card up-card">
|
||
<h3>${esc(title)}</h3>
|
||
<p class="desc" style="margin:0 0 4px">${note}</p>
|
||
${body}
|
||
</div>`;
|
||
}
|
||
|
||
async function render() {
|
||
const host = $('#netdetail-box');
|
||
if (!host) return;
|
||
let d = null;
|
||
try {
|
||
const r = await api('/api/netdetail');
|
||
if (r && r.ok) d = r.data;
|
||
} catch (e) { /* 下面统一处理 */ }
|
||
|
||
if (!d) {
|
||
host.innerHTML = card(t('网络明细'), t('邻居表 / 路由表 / 规则表'),
|
||
`<div class="eth-box eth-off">
|
||
<span class="tag warn">${t('读取失败')}</span>
|
||
<span class="eth-reason">${t('后端接口无响应,请检查')} drouter-helper ${t('是否在运行')}</span>
|
||
</div>`);
|
||
return;
|
||
}
|
||
|
||
const n4 = (d.neigh4 || []).length;
|
||
const n6 = (d.neigh6 || []).length;
|
||
const r4 = (d.routes4 || []).length;
|
||
const r6 = (d.routes6 || []).length;
|
||
const ru = (d.rules6 || []).length;
|
||
|
||
host.innerHTML =
|
||
card(t('nd.neigh4'),
|
||
t('nd.neighTip'),
|
||
neighTable(d.neigh4, false)) +
|
||
card(t('nd.neigh6'),
|
||
t('nd.neigh6Tip'),
|
||
neighTable(d.neigh6, true)) +
|
||
card(t('nd.route4'),
|
||
t('nd.route4Tip'),
|
||
routeTable(d.routes4, false)) +
|
||
card(t('nd.route6'),
|
||
t('nd.route6Tip'),
|
||
routeTable(d.routes6, true)) +
|
||
card(t('nd.rule6'),
|
||
t('nd.rule6Tip'),
|
||
ruleTable(d.rules6)) +
|
||
`<div class="up-row" style="margin-top:0">
|
||
<span class="tag gray">${t('nd.neigh4')} ${n4}</span>
|
||
<span class="tag gray">${t('nd.neigh6')} ${n6}</span>
|
||
<span class="tag gray">${t('nd.route4')} ${r4}</span>
|
||
<span class="tag gray">${t('nd.route6')} ${r6}</span>
|
||
<span class="tag gray">${t('nd.rule6')} ${ru}</span>
|
||
</div>`;
|
||
}
|
||
|
||
/* 15 秒刷新:ARP/ND 表变化以分钟计,路由表更慢。
|
||
比上游的 10 秒慢一点 —— 邻居表在局域网繁忙时条目多,渲染成本更高。 */
|
||
function start() {
|
||
if (TIMER) clearInterval(TIMER);
|
||
TIMER = setInterval(() => {
|
||
if (!$('#netdetail-box')) { stop(); return; }
|
||
render();
|
||
}, 15000);
|
||
}
|
||
function stop() { if (TIMER) { clearInterval(TIMER); TIMER = null; } }
|
||
window.__drouterNetDetailStop = stop;
|
||
window.drouterRenderNetDetail = () => { render(); start(); };
|
||
if (window.i18n) window.i18n.register('netdetail',
|
||
() => { render(); start(); });
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => render());
|
||
}
|
||
})();
|