⛅ Sync 2026-11-02 22:20:40

This commit is contained in:
github-actions[bot] committed 2026-11-02 22:20:40 +08:00
1 parent db798bdf5e
commit d09231d556
184 files changed
+19918 -3537

No files matched your search

@@ -1298,3 +1298,406 @@
}
/* lc:end */
/* ---------------------------------------------------------------- 环境横幅(doctor,0.3.3) */
.isp-doctor {
margin: 0;
}
/* fail:红条。与 .is-warn 同一套骨架,只换左边框与标题色——不新增视觉语言。
这里没有关闭按钮,也没有折叠:有 fail 项时横幅必须一直在。 */
.isp-banner.is-bad {
border-left-color: var(--isp-bad);
}
.isp-banner.is-bad .isp-banner-msg strong {
color: var(--isp-bad);
}
.isp-banner-actions {
display: flex;
flex: 0 0 auto;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.isp-env-list {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.isp-env-list[hidden] {
display: none;
}
.isp-env-item {
border-left: 3px solid var(--isp-border-strong);
padding-left: 8px;
}
.isp-env-title {
display: block;
font-weight: 600;
color: var(--isp-text);
}
.isp-env-adv {
display: block;
margin-top: 2px;
color: var(--isp-muted);
font-size: 0.86rem;
line-height: 1.45;
}
/* 全绿:一行,尽量不占版面 */
.isp-env-ok {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
margin: 0 0 16px 0;
padding: 8px 14px;
border: 1px solid var(--isp-border);
border-left: 4px solid var(--isp-ok);
border-radius: var(--isp-radius);
background: var(--isp-card-bg);
color: var(--isp-text);
font-size: 0.9rem;
}
.isp-env-ok .isp-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--isp-ok);
}
.isp-env-sub {
color: var(--isp-muted);
font-size: 0.84rem;
}
.isp-env-wait {
margin: 0 0 16px 0;
color: var(--isp-muted);
font-size: 0.86rem;
}
/* ---------------------------------------------------------------- 首启向导(0.3.3)
沿用 isp-* 那套变量与卡片骨架:向导只新增"步骤条 / 自检行 / 完成态"三块,
不复用体检页那套样式类——它可被构建期剥离,向导不能跟着消失。 */
.wz-steps {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 0 0 16px 0;
}
.wz-step {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 10px 12px;
text-align: left;
background: var(--isp-card-bg);
border: 1px solid var(--isp-border);
border-radius: var(--isp-radius);
color: var(--isp-muted);
cursor: pointer;
}
.wz-step.is-current {
border-color: var(--isp-primary);
box-shadow: inset 0 0 0 1px var(--isp-primary);
color: var(--isp-text);
}
.wz-step.is-done {
border-left: 4px solid var(--isp-ok);
color: var(--isp-text);
}
.wz-step[disabled] {
opacity: 0.55;
cursor: not-allowed;
}
.wz-step-no {
font-size: 0.72rem;
letter-spacing: 0.08em;
}
.wz-step-title {
font-size: 0.95rem;
font-weight: 600;
}
.wz-step-hint {
font-size: 0.76rem;
}
.wz-body {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 14px;
}
.wz-card {
padding: 14px 16px;
}
.wz-card h3 {
margin: 0 0 10px 0;
}
.wz-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 14px 0 10px 0;
}
.wz-summary-head {
color: var(--isp-muted);
font-size: 0.88rem;
}
.wz-doctor {
display: flex;
flex-direction: column;
gap: 12px;
}
.wz-checks {
display: flex;
flex-direction: column;
gap: 10px;
}
.wz-check {
padding: 12px 14px;
border: 1px solid var(--isp-border);
border-left: 4px solid var(--isp-border-strong);
border-radius: var(--isp-radius);
background: var(--isp-card-bg);
}
.wz-check.is-ok {
border-left-color: var(--isp-ok);
}
.wz-check.is-warn {
border-left-color: var(--isp-warn);
}
.wz-check.is-bad {
border-left-color: var(--isp-bad);
}
.wz-check-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.wz-check-title {
font-weight: 600;
color: var(--isp-text);
}
.wz-check-conf {
margin-left: auto;
color: var(--isp-muted);
font-size: 0.8rem;
}
.wz-find {
margin-top: 6px;
}
.wz-find-line {
color: var(--isp-muted);
font-size: 0.86rem;
line-height: 1.5;
}
.wz-advice {
margin-top: 6px;
color: var(--isp-text);
font-size: 0.88rem;
line-height: 1.5;
}
.wz-kv {
display: flex;
gap: 10px;
padding: 3px 0;
font-size: 0.88rem;
}
.wz-kv-key {
flex: 0 0 96px;
color: var(--isp-muted);
}
.wz-kv-val {
flex: 1 1 auto;
color: var(--isp-text);
word-break: break-all;
}
.wz-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-top: 12px;
}
.wz-hint {
margin: 8px 0 0 0;
color: var(--isp-muted);
font-size: 0.84rem;
line-height: 1.5;
}
.wz-meta {
margin-top: 8px;
color: var(--isp-muted);
font-size: 0.84rem;
}
.wz-note {
min-height: 1.2em;
margin: 10px 0 6px 0;
color: var(--isp-primary);
font-size: 0.88rem;
}
.wz-note.is-bad {
color: var(--isp-bad);
}
.wz-note.is-ok {
color: var(--isp-ok);
}
.wz-loading {
color: var(--isp-muted);
font-size: 0.88rem;
}
.wz-block {
margin-top: 10px;
padding: 10px 12px;
border-radius: var(--isp-radius);
background: var(--isp-card-bg-2);
color: var(--isp-text);
font-size: 0.88rem;
line-height: 1.5;
}
.wz-preview {
margin-top: 12px;
}
.wz-preview-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 8px;
color: var(--isp-muted);
font-size: 0.88rem;
}
.wz-dry {
max-height: 320px;
}
.isp-out.is-bad {
border-color: var(--isp-bad);
}
.wz-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 12px;
}
.wz-ack {
display: flex;
align-items: center;
gap: 8px;
color: var(--isp-text);
font-size: 0.88rem;
}
.wz-list {
margin: 8px 0 0 0;
padding-left: 20px;
color: var(--isp-text);
font-size: 0.9rem;
line-height: 1.55;
}
.wz-footer {
display: flex;
align-items: center;
gap: 10px;
margin: 14px 0 6px 0;
}
.wz-spacer {
flex: 1 1 auto;
}
.wz-done {
display: flex;
gap: 14px;
align-items: flex-start;
}
.wz-done-mark {
flex: 0 0 auto;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--isp-ok);
color: #fff;
font-size: 1.4rem;
font-weight: 700;
}
.wz-done-text strong {
display: block;
font-size: 1.05rem;
color: var(--isp-text);
}
.wz-done-banner strong {
color: var(--isp-ok);
}
@media (max-width: 820px) {
.wz-steps {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wz-cards {
grid-template-columns: 1fr;
}
}
@@ -22,6 +22,20 @@
var CMD = '/usr/sbin/isp-split';
var REFRESH_SEC = 10;
/*
* 环境横幅(0.3.3):页面顶部一条 doctor 结论。
*
* 为什么带缓存:`doctor` 不是零成本——它要读 nft 规则表、ubus、/proc,在
* 小闪存机器上要几百毫秒到几秒。而状态页每 10 秒刷新一次,跟着刷等于拿
* 路由器当体检机用。所以结果按 10 分钟缓存:内存一份(页面内切来切去不重跑)
* + localStorage 一份(刷新页面也不重跑)。用户点「重新体检」才强制重跑。
*
* 颜色规矩(W2):fail 红且不能关(TUN 劫持 / DNS 53 这类问题会静默吃掉分流,
* 关掉就等于藏了个定时炸弹);warn 橙、可折叠;全绿只留一行,不占地方。
*/
var DOCTOR_TTL_MS = 600 * 1000;
var DOCTOR_CACHE_KEY = 'isp-split.doctor.v1';
/* ---------------------------------------------------------------- 图标(内联 SVG,无外链) */
var ICONS = {
@@ -246,6 +260,187 @@ return view.extend({
});
},
/* ---------------- 环境横幅(doctor) ----------------
doctor 的退出码有语义(0 全绿 / 1 有保留 / 2 会静默失效),所以这里不看
退出码,只看 stdout 里有没有一份能解析的 JSON。 */
doctorCacheGet: function() {
try {
var raw = window.localStorage.getItem(DOCTOR_CACHE_KEY);
var o = raw ? JSON.parse(raw) : null;
if (o && o.t && o.d && (Date.now() - o.t) < DOCTOR_TTL_MS)
return { data: o.d, at: +o.t };
} catch (e) { /* 隐私模式 / 配额满:退化成只在内存里缓存 */ }
return null;
},
doctorCachePut: function(data) {
try {
window.localStorage.setItem(DOCTOR_CACHE_KEY,
JSON.stringify({ t: Date.now(), d: data }));
} catch (e) { /* 同上:缓存写不进去不影响功能 */ }
},
refreshDoctor: function(force) {
var self = this;
if (this._doctorInflight)
return this._doctorInflight;
if (!force) {
var c = this.doctorCacheGet();
if (c) {
this.doctor = c.data;
this.doctorAt = c.at;
this.paintDoctor();
return Promise.resolve();
}
}
this.doctorErr = null;
this._doctorInflight = fs.exec(CMD, ['doctor', '--json']).then(function(res) {
var d = parseJson(res);
if (!d)
throw new Error((((res && res.stdout) || '') + ((res && res.stderr) || '')).trim() ||
('退出码 ' + ((res && res.code) != null ? res.code : '?')));
self.doctor = d;
self.doctorAt = Date.now();
self.doctorCachePut(d);
}).catch(function(err) {
self.doctorErr = String(err);
}).then(function() {
self._doctorInflight = null;
self.paintDoctor();
});
return this._doctorInflight;
},
paintDoctor: function() {
var box = document.getElementById('isp-doctor');
if (!box)
return;
while (box.firstChild)
box.removeChild(box.firstChild);
var self = this;
if (this.doctorErr) {
box.appendChild(E('div', { 'class': 'isp-banner is-warn' }, [
E('div', { 'class': 'isp-banner-msg' }, [
E('strong', {}, [ _('环境体检不可用') ]),
E('div', {}, [ this.doctorErr ]),
E('div', { 'class': 'isp-banner-hint' }, [
_('不影响下方的运行状态;多半是 rpcd 权限或 isp-split 版本不匹配。')
])
]),
E('button', {
'class': 'cbi-button isp-banner-btn',
'click': function() { self.refreshDoctor(true); }
}, [ _('重试') ])
]));
return;
}
if (!this.doctor) {
box.appendChild(E('div', { 'class': 'isp-env-wait' }, [ _('正在做环境体检(doctor)…') ]));
return;
}
var d = this.doctor;
var checks = d.checks || [];
var sum = d.summary || {};
var fails = checks.filter(function(c) { return c.status === 'fail'; });
var warns = checks.filter(function(c) { return c.status === 'warn'; });
var ageMin = this.doctorAt ? Math.max(0, Math.round((Date.now() - this.doctorAt) / 60000)) : 0;
var cacheHint = _('(结果缓存 ') + ageMin + _(' 分钟,10 分钟内不重跑探针)');
var retryBtn = E('button', {
'class': 'cbi-button isp-banner-btn',
'title': _('立即重跑 doctor(会跳过 10 分钟缓存)'),
'click': function() {
retryBtn.disabled = true;
self.refreshDoctor(true);
}
}, [ _('重新体检') ]);
/* fail:红条,不能关,建议直接摊开 */
if (fails.length) {
var list = E('div', { 'class': 'isp-env-list' });
fails.forEach(function(c) {
list.appendChild(E('div', { 'class': 'isp-env-item' }, [
E('span', { 'class': 'isp-env-title' }, [ c.title || c.id ]),
E('span', { 'class': 'isp-env-adv' }, [ c.advice || '' ])
]));
});
box.appendChild(E('div', { 'class': 'isp-banner is-bad' }, [
E('div', { 'class': 'isp-banner-msg' }, [
E('strong', {}, [
_('环境体检:') + fails.length + _(' 项会让分流静默失效')
]),
E('div', {}, [ sum.headline || _('按下面的建议处理后再看状态指标。') ]),
list,
E('div', { 'class': 'isp-banner-hint' }, [
_('这条横幅在有 fail 项时不能关闭。') + cacheHint
])
]),
retryBtn
]));
return;
}
/* warn:橙条,可折叠 */
if (warns.length) {
var detail = E('div', { 'class': 'isp-env-list', 'hidden': '' });
warns.forEach(function(c) {
detail.appendChild(E('div', { 'class': 'isp-env-item' }, [
E('span', { 'class': 'isp-env-title' }, [ c.title || c.id ]),
E('span', { 'class': 'isp-env-adv' }, [ c.advice || '' ])
]));
});
var toggle = E('button', {
'class': 'cbi-button isp-banner-btn',
'click': function() {
if (detail.hasAttribute('hidden')) {
detail.removeAttribute('hidden');
toggle.textContent = _('收起建议');
} else {
detail.setAttribute('hidden', '');
toggle.textContent = _('查看建议');
}
}
}, [ _('查看建议') ]);
box.appendChild(E('div', { 'class': 'isp-banner is-warn' }, [
E('div', { 'class': 'isp-banner-msg' }, [
E('strong', {}, [ _('环境体检:') + warns.length + _(' 项有保留') ]),
E('div', {}, [ sum.headline || '' ]),
detail,
E('div', { 'class': 'isp-banner-hint' }, [ cacheHint ])
]),
E('div', { 'class': 'isp-banner-actions' }, [ toggle, retryBtn ])
]));
return;
}
/* 全绿:一行,不抢版面 */
box.appendChild(E('div', { 'class': 'isp-env-ok' }, [
E('span', { 'class': 'isp-dot' }),
E('span', {}, [ _('环境体检正常(') + checks.length + _(' 项)') ]),
E('span', { 'class': 'isp-env-sub' }, [
sum.worst && sum.worst !== 'ok'
? _('最差项:') + sum.worst + _(' · ') + (sum.headline || '')
: cacheHint
]),
E('button', {
'class': 'cbi-button isp-banner-btn',
'click': function() { self.refreshDoctor(true); }
}, [ _('重新体检') ])
]));
},
setOut: function(text) {
var out = document.getElementById('isp-out');
if (out) {
@@ -691,6 +886,10 @@ return view.extend({
this.paused = false;
this.busy = false;
this._inflight = null;
this.doctor = null;
this.doctorAt = 0;
this.doctorErr = null;
this._doctorInflight = null;
var pauseLabel = E('span', {}, [_('暂停刷新')]);
var pauseBtn = E('button', {
@@ -754,6 +953,7 @@ return view.extend({
E('div', { 'class': 'isp-toolbar' }, [ statePill, pauseBtn ])
]),
E('div', { 'id': 'isp-doctor', 'class': 'isp-doctor' }),
E('div', { 'id': 'isp-load-error', 'class': 'isp-err', 'style': 'display:none' }),
E('div', { 'id': 'isp-cards', 'class': 'isp-cards' }),
@@ -782,6 +982,8 @@ return view.extend({
]);
this.refresh();
/* 环境体检单独走自己的 10 分钟缓存:失败/成功都不影响下面的状态刷新 */
this.refreshDoctor(false);
/* 自带定时器替代 LuCI 的 poll(见文件头注释):暂停即停,页面被换走即收摊。 */
this.timer = window.setInterval(function() {
File diff suppressed because it is too large. Load diff