mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-03 07:59:09 +08:00
572 lines
20 KiB
JavaScript
572 lines
20 KiB
JavaScript
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2023-2025 ImmortalWrt.org
|
|
*/
|
|
|
|
'use strict';
|
|
'require dom';
|
|
'require homeproxy as hp';
|
|
'require poll';
|
|
'require rpc';
|
|
'require uci';
|
|
'require ui';
|
|
'require view';
|
|
|
|
const css = '\
|
|
.diag-section { margin-bottom: 1.5em; }\
|
|
.diag-row { display:flex; align-items:baseline; margin:.35em 0; gap:.6em; }\
|
|
.diag-label { min-width:11em; color:#666; font-size:.9em; flex-shrink:0; }\
|
|
.diag-ok { color: #2a2; font-weight: bold; }\
|
|
.diag-fail { color: #c33; font-weight: bold; }\
|
|
.diag-warn { color: #c80; font-weight: bold; }\
|
|
.diag-gray { color: #888; }\
|
|
.diag-pre { font-family:monospace; font-size:.82em; background:#f5f5f5; border:1px solid #ddd;\
|
|
padding:.5em .8em; white-space:pre-wrap; word-break:break-all; border-radius:3px;\
|
|
margin:.3em 0 0; max-height:14em; overflow-y:auto; }\
|
|
.diag-input { padding:.25em .4em; border:1px solid #ccc; border-radius:3px;\
|
|
font-size:.9em; width:14em; }\
|
|
.diag-btn { margin-right:.4em; }\
|
|
.diag-port-line { font-family:monospace; font-size:.82em; margin:.1em 0; }\
|
|
#diag-report-area { width:100%; height:22em; font-family:monospace; font-size:.8em;\
|
|
white-space:pre; overflow:auto; background:#f5f5f5; border:1px solid #ddd; padding:.5em; }\
|
|
';
|
|
|
|
/* ── RPC declarations ─────────────────────────────────────────────────── */
|
|
|
|
const callCoreCheck = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_core_check',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callConfigCheck = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_config_check',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callDnsRu = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_dns_ru',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callNftables = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_nftables',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
|
|
const callReport = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_report',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callServiceRestart = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'diag_service_restart',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callConnCheck = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'connection_check',
|
|
params: ['site'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callIpInfo = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'clash_ip_info',
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const callActiveNode = rpc.declare({
|
|
object: 'luci.homeproxy',
|
|
method: 'clash_active_node',
|
|
params: ['tag'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
/* Resolve a sing-box outbound tag (cfg-<section>-out) to its UCI label. */
|
|
function resolveTag(tag) {
|
|
const m = tag && tag.match(/^cfg-(.+)-out$/);
|
|
if (m) {
|
|
const label = uci.get('homeproxy', m[1], 'label');
|
|
if (label) return label;
|
|
}
|
|
/* Fixed aggregate tags → name the configured node behind them. */
|
|
const specials = { 'main-out': 'main_node', 'main-udp-out': 'main_udp_node' };
|
|
if (specials[tag]) {
|
|
const v = uci.get('homeproxy', 'config', specials[tag]);
|
|
if (v === 'byedpi-out') return 'ByeDPI';
|
|
if (v === 'zapret-out') return 'Zapret';
|
|
if (v && !['urltest', 'same', 'nil', ''].includes(v)) {
|
|
const label = uci.get('homeproxy', v, 'label');
|
|
if (label) return label;
|
|
}
|
|
}
|
|
return tag;
|
|
}
|
|
|
|
/* ── DOM helpers ──────────────────────────────────────────────────────── */
|
|
|
|
function statusBadge(ok, text) {
|
|
/* ucode serialises booleans as integers 0/1 — use loose checks so both
|
|
JS booleans and integers work. null/undefined → "uncertain" (orange ?). */
|
|
const isOk = !!ok;
|
|
const isWarn = ok == null; /* catches null AND undefined */
|
|
const icon = isOk ? '✓ ' : isWarn ? '? ' : '✗ ';
|
|
const cls = isOk ? 'diag-ok' : isWarn ? 'diag-warn' : 'diag-fail';
|
|
const label = text || (isOk ? _('OK') : isWarn ? '?' : _('FAIL'));
|
|
return E('strong', { 'class': cls }, icon + label);
|
|
}
|
|
|
|
function row(label, value) {
|
|
return E('div', { 'class': 'diag-row' }, [
|
|
E('span', { 'class': 'diag-label' }, label),
|
|
E('span', {}, value)
|
|
]);
|
|
}
|
|
|
|
function pre(text) {
|
|
return E('div', { 'class': 'diag-pre' }, text || _('(empty)'));
|
|
}
|
|
|
|
function sectionCard(title, id, rows) {
|
|
return E('div', { 'class': 'cbi-section diag-section' }, [
|
|
E('h3', {}, title),
|
|
E('div', { 'id': id }, rows)
|
|
]);
|
|
}
|
|
|
|
function spinner(el, label) {
|
|
dom.content(el, E('em', { 'class': 'diag-gray' }, label || _('Testing…')));
|
|
}
|
|
|
|
/* ── Section builders ─────────────────────────────────────────────────── */
|
|
|
|
function buildConnectivitySection(view, coreType) {
|
|
const items = []; /* manual checks; the section's run() fires them together */
|
|
|
|
function siteRow(label, site) {
|
|
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
|
|
function run() {
|
|
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
|
|
return L.resolveDefault(callConnCheck(site), {}).then(function(ret) {
|
|
dom.content(resultEl, statusBadge(!!ret.result, ret.result ? _('passed') : _('failed')));
|
|
});
|
|
}
|
|
items.push(run);
|
|
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
|
|
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
|
|
}
|
|
|
|
/* Exit IP / geo — only hiddify-core's Clash API reports it. */
|
|
function ipRow(label, type) {
|
|
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
|
|
function run() {
|
|
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
|
|
return L.resolveDefault(callIpInfo(), {}).then(function(ret) {
|
|
if (ret.error) { dom.content(resultEl, E('span', { 'class': 'diag-fail' }, ret.error)); return; }
|
|
const entry = ret[type];
|
|
if (!entry || !entry.ip) { dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No data'))); return; }
|
|
const meta = [entry.country, entry.org].filter(Boolean).join(', ');
|
|
const delay = (entry.delay && entry.delay !== 65535) ? ' — ' + entry.delay + ' ms' : '';
|
|
const node = (type === 'proxy' && entry.node) ? resolveTag(entry.node) + ': ' : '';
|
|
dom.content(resultEl, E('strong', { 'class': 'diag-ok' },
|
|
node + entry.ip + (meta ? ' (' + meta + ')' : '') + delay));
|
|
});
|
|
}
|
|
items.push(run);
|
|
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
|
|
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
|
|
}
|
|
|
|
/* Active node — live status (which node sing-box has selected), auto-updating;
|
|
* sing-box can't report exit IP, so this stands in for the IP rows. */
|
|
/* Live active-node row. `tag` selects which outbound group to resolve:
|
|
undefined → the main (TCP) node via GLOBAL; 'main-udp-out' → the dedicated UDP node. */
|
|
function activeNodeRow(labelText, tag) {
|
|
const resultEl = E('span', { 'class': 'diag-gray' }, '—');
|
|
poll.add(L.bind(function() {
|
|
return L.resolveDefault(callActiveNode(tag), {}).then(function(ret) {
|
|
if (ret && !ret.error && ret.node) {
|
|
const type = ret.type ? ' (' + ret.type + ')' : '';
|
|
/* Same 4-colour scheme as the status/client pages: 65535 ms is the
|
|
URLTest timeout sentinel (confirmed dead → red); >=3000 ms is
|
|
working-but-slow (orange); a real low latency is green; no delay
|
|
is unmeasured (gray, no number). */
|
|
let cls, dStr = '';
|
|
if (ret.delay === 65535) { cls = 'diag-fail'; dStr = ' — ' + _('timeout'); }
|
|
else if (ret.delay >= 3000) { cls = 'diag-warn'; dStr = ' — ' + ret.delay + ' ms'; }
|
|
else if (ret.delay) { cls = 'diag-ok'; dStr = ' — ' + ret.delay + ' ms'; }
|
|
else cls = 'diag-gray';
|
|
dom.content(resultEl, E('strong', { 'class': cls }, resolveTag(ret.node) + type + dStr));
|
|
} else {
|
|
dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No active node')));
|
|
}
|
|
});
|
|
}));
|
|
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, labelText), resultEl ]);
|
|
}
|
|
|
|
const rows = [
|
|
siteRow(_('Baidu'), 'baidu'),
|
|
siteRow(_('Google'), 'google'),
|
|
siteRow(_('YouTube'), 'youtube'),
|
|
siteRow(_('Yandex'), 'yandex'),
|
|
siteRow(_('Speedtest'), 'speedtest')
|
|
];
|
|
|
|
/* hiddify → exit-IP rows; sing-box → live Active Node (it can't report exit IP). */
|
|
if (coreType === 'hiddify') {
|
|
rows.push(ipRow(_('Direct IP'), 'direct'));
|
|
rows.push(ipRow(_('Proxy IP'), 'proxy'));
|
|
} else if (coreType === 'singbox') {
|
|
rows.push(activeNodeRow(_('Active Node')));
|
|
}
|
|
|
|
/* Dedicated UDP node — only the modes that expose "Main UDP node" in the UI set it
|
|
(Global + the bypass_cn/bypass_ir reverse modes; proxy_banned_ru/custom don't, and
|
|
'redirect' proxy mode has no UDP path). Show which node currently carries UDP. */
|
|
const udpNode = uci.get('homeproxy', 'config', 'main_udp_node');
|
|
const proxyMode = uci.get('homeproxy', 'config', 'proxy_mode');
|
|
if (udpNode && !['nil', 'same', ''].includes(udpNode) && proxyMode !== 'redirect')
|
|
rows.push(activeNodeRow(_('Active UDP Node'), 'main-udp-out'));
|
|
|
|
return {
|
|
el: sectionCard(_('Connectivity'), 'diag-connectivity', rows),
|
|
run: function() { return Promise.all(items.map(function(fn) { return fn(); })); }
|
|
};
|
|
}
|
|
|
|
function buildCoreSection(view) {
|
|
const resultsEl = E('div', {});
|
|
const restartMsgEl = E('span', { 'class': 'diag-gray', 'style': 'font-size:.9em' });
|
|
|
|
function run() {
|
|
spinner(resultsEl, _('Checking core…'));
|
|
return L.resolveDefault(callCoreCheck(), {}).then(function(ret) {
|
|
if (!ret || ret.error) {
|
|
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
|
|
return;
|
|
}
|
|
|
|
const portLines = (ret.listen_ports && ret.listen_ports.length)
|
|
? ret.listen_ports.map(function(l) { return E('div', { 'class': 'diag-port-line' }, l); })
|
|
: [ E('em', { 'class': 'diag-gray' }, _('none detected')) ];
|
|
|
|
dom.content(resultsEl, [
|
|
row(_('Hiddify-core'), statusBadge(ret.hiddify_installed, ret.hiddify_installed ? _('installed') : _('not found'))),
|
|
row(_('sing-box'), statusBadge(ret.singbox_installed, ret.singbox_installed ? _('installed') : _('not found'))),
|
|
row(_('ByeDPI'), statusBadge(ret.byedpi_installed, ret.byedpi_installed ? _('installed') : _('not found'))),
|
|
row(_('Zapret 2'), statusBadge(ret.zapret_installed, ret.zapret_installed ? _('installed') : _('not found'))),
|
|
ret.binary ? row(_('Active binary'), E('code', {}, ret.binary)) : null,
|
|
ret.version ? row(_('Version'), E('span', { 'class': 'diag-pre', 'style': 'max-height:5em' }, ret.version)) : null,
|
|
row(_('Running'), statusBadge(ret.running, ret.running ? _('yes') + (ret.pid ? ' (PID ' + ret.pid + ')' : '') : _('no'))),
|
|
ret.byedpi_installed ? row(_('ByeDPI running'), statusBadge(ret.byedpi_running, ret.byedpi_running ? _('yes') + (ret.byedpi_pid ? ' (PID ' + ret.byedpi_pid + ')' : '') : _('no'))) : null,
|
|
ret.zapret_installed ? row(_('Zapret 2 running'), statusBadge(ret.zapret_running, ret.zapret_running ? _('yes') + (ret.zapret_pid ? ' (PID ' + ret.zapret_pid + ')' : '') : _('no'))) : null,
|
|
row(_('Listening ports'), E('div', {}, portLines))
|
|
].filter(Boolean));
|
|
});
|
|
}
|
|
|
|
function restart() {
|
|
dom.content(restartMsgEl, E('em', { 'class': 'diag-gray' }, _('Restarting… (may take ~5 s)')));
|
|
return L.resolveDefault(callServiceRestart(), {}).then(function(ret) {
|
|
if (ret && ret.result) {
|
|
dom.content(restartMsgEl, statusBadge(true, _('Service restarted')));
|
|
/* Refresh core status after restart */
|
|
return run();
|
|
} else {
|
|
dom.content(restartMsgEl, statusBadge(false,
|
|
_('Restart failed') + (ret && ret.exit_code != null ? ' (exit ' + ret.exit_code + ')' : '')));
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
el: sectionCard(_('Core & System'), 'diag-core', [
|
|
E('div', { 'class': 'diag-row' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run)
|
|
}, _('Check')),
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-negative diag-btn',
|
|
'click': ui.createHandlerFn(view, restart)
|
|
}, _('Restart Service')),
|
|
restartMsgEl
|
|
]),
|
|
resultsEl
|
|
]),
|
|
run: run
|
|
};
|
|
}
|
|
|
|
function buildConfigSection(view) {
|
|
const resultsEl = E('div', {});
|
|
|
|
function run() {
|
|
spinner(resultsEl, _('Checking config…'));
|
|
return L.resolveDefault(callConfigCheck(), {}).then(function(ret) {
|
|
if (!ret || ret.error) {
|
|
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
|
|
return;
|
|
}
|
|
|
|
dom.content(resultsEl, [
|
|
row(_('Valid'), statusBadge(ret.valid)),
|
|
ret.size_bytes != null ? row(_('Size'), (ret.size_bytes / 1024).toFixed(1) + ' KB') : null,
|
|
ret.stats ? row(_('Inbounds'), String(ret.stats.inbounds)) : null,
|
|
ret.stats ? row(_('Outbounds'), String(ret.stats.outbounds)) : null,
|
|
ret.stats ? row(_('Rules'), String(ret.stats.rules)) : null,
|
|
ret.stats ? row(_('DNS servers'), String(ret.stats.dns_servers)) : null,
|
|
ret.check_output ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, _('Check output')),
|
|
pre(ret.check_output)
|
|
]) : null
|
|
].filter(Boolean));
|
|
});
|
|
}
|
|
|
|
return {
|
|
el: sectionCard(_('Configuration'), 'diag-config', [
|
|
E('div', { 'class': 'diag-row' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run)
|
|
}, _('Check'))
|
|
]),
|
|
resultsEl
|
|
]),
|
|
run: run
|
|
};
|
|
}
|
|
|
|
function buildDnsSection(view) {
|
|
const resultsEl = E('div', {});
|
|
|
|
const cardEl = sectionCard(_('DNS Tests'), 'diag-dns', [
|
|
E('div', { 'class': 'diag-row' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run)
|
|
}, _('Test'))
|
|
]),
|
|
resultsEl
|
|
]);
|
|
cardEl.style.display = 'none';
|
|
|
|
function run() {
|
|
spinner(resultsEl, _('Testing DNS…'));
|
|
return L.resolveDefault(callDnsRu(), {}).then(function(ret) {
|
|
if (ret.skip) {
|
|
cardEl.style.display = 'none';
|
|
return;
|
|
}
|
|
cardEl.style.display = '';
|
|
if (ret.error) {
|
|
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
|
|
return;
|
|
}
|
|
|
|
const regionDomain = ret.region_domain || '?';
|
|
dom.content(resultsEl, [
|
|
E('div', { 'style': 'font-weight:bold; margin:.4em 0 .2em' },
|
|
(ret.region_label ? ret.region_label + ' ' : '') + _('DNS (direct)')),
|
|
row(_('Server'), E('code', {}, ret.region_server || '?')),
|
|
row(regionDomain, statusBadge(ret.region_ok,
|
|
ret.region_ok ? _('Resolved') : _('No answer — check server address'))),
|
|
ret.region_output ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, 'nslookup ' + regionDomain),
|
|
pre(ret.region_output)
|
|
]) : null,
|
|
|
|
E('div', { 'style': 'font-weight:bold; margin:.8em 0 .2em' }, _('Secure DNS')),
|
|
ret.secure_server ? row(_('Server'), E('code', {}, ret.secure_server)) : null,
|
|
row(_('Bootstrap'), E('code', {}, ret.bootstrap || '?')),
|
|
row('andrevi.ch', statusBadge(ret.secure_ok,
|
|
ret.secure_ok ? _('Resolved via proxy') : _('No answer — check proxy and proxy list'))),
|
|
ret.secure_output ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, 'nslookup andrevi.ch'),
|
|
pre(ret.secure_output)
|
|
]) : null
|
|
].filter(Boolean));
|
|
});
|
|
}
|
|
|
|
return { el: cardEl, run: run };
|
|
}
|
|
|
|
function buildNftSection(view) {
|
|
const resultsEl = E('div', {});
|
|
|
|
function run() {
|
|
spinner(resultsEl, _('Reading nftables…'));
|
|
return L.resolveDefault(callNftables(), {}).then(function(ret) {
|
|
if (ret.error) {
|
|
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
|
|
return;
|
|
}
|
|
|
|
dom.content(resultsEl, [
|
|
row(_('Re:HomeProxy chains'), statusBadge(ret.nft_present,
|
|
ret.nft_present ? _('found') : _('not found — firewall rules may be missing'))),
|
|
ret.nft_rules ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, _('Matched lines in fw4')),
|
|
pre(ret.nft_rules)
|
|
]) : null,
|
|
ret.udp_tproxy ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, _('UDP TPROXY counters (UDP into HomeProxy)')),
|
|
pre(ret.udp_tproxy)
|
|
]) : null,
|
|
ret.uci_firewall ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, _('UCI firewall settings')),
|
|
pre(ret.uci_firewall)
|
|
]) : null,
|
|
ret.zapret_enabled ? row(_('Zapret (nfqws2)'), statusBadge(ret.zapret_running,
|
|
ret.zapret_running ? _('running') : _('enabled, but nfqws2 not running'))) : null,
|
|
(ret.zapret_enabled && ret.zapret_queue) ? E('div', {}, [
|
|
E('div', { 'class': 'diag-label' }, _('Zapret NFQUEUE counters (mark 110 → queue)')),
|
|
pre(ret.zapret_queue)
|
|
]) : null
|
|
].filter(Boolean));
|
|
});
|
|
}
|
|
|
|
return {
|
|
el: sectionCard(_('Network Intercept'), 'diag-nft', [
|
|
E('div', { 'class': 'diag-row' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run)
|
|
}, _('Check'))
|
|
]),
|
|
resultsEl
|
|
]),
|
|
run: run
|
|
};
|
|
}
|
|
|
|
|
|
function buildReportSection(view) {
|
|
const textArea = E('textarea', {
|
|
'id': 'diag-report-area',
|
|
'readonly': true,
|
|
'placeholder': _('Click Generate to collect diagnostics…')
|
|
});
|
|
|
|
function run() {
|
|
textArea.value = _('Collecting diagnostics — this may take a few seconds…');
|
|
return L.resolveDefault(callReport(), {}).then(function(ret) {
|
|
textArea.value = ret.report || _('(empty)');
|
|
});
|
|
}
|
|
|
|
function copy() {
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
}
|
|
|
|
function download() {
|
|
const text = textArea.value;
|
|
if (!text || text === _('Click Generate to collect diagnostics…')) return;
|
|
const now = new Date();
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
const stamp = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) +
|
|
'_' + pad(now.getHours()) + '-' + pad(now.getMinutes()) + '-' + pad(now.getSeconds());
|
|
const blob = new Blob([text], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = E('a', { href: url, download: 'homeproxy-diagnostics-' + stamp + '.txt' });
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
return {
|
|
el: sectionCard(_('Diagnostics Report'), 'diag-report', [
|
|
E('div', { 'class': 'diag-row' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action diag-btn',
|
|
'click': ui.createHandlerFn(view, run)
|
|
}, _('Generate')),
|
|
E('button', {
|
|
'class': 'btn cbi-button diag-btn',
|
|
'click': ui.createHandlerFn(view, copy)
|
|
}, _('Copy')),
|
|
E('button', {
|
|
'class': 'btn cbi-button diag-btn',
|
|
'click': ui.createHandlerFn(view, download)
|
|
}, _('Download')),
|
|
E('em', { 'class': 'diag-gray', 'style': 'font-size:.85em' },
|
|
_('Sensitive values (passwords, keys, UUIDs) are redacted.'))
|
|
]),
|
|
textArea
|
|
])
|
|
};
|
|
}
|
|
|
|
/* ── View ─────────────────────────────────────────────────────────────── */
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
L.resolveDefault(hp.getBuiltinFeatures(), {}),
|
|
uci.load('homeproxy')
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
const features = (data && data[0]) || {};
|
|
document.head.appendChild(E('style', {}, [ css ]));
|
|
|
|
const core = buildCoreSection(this);
|
|
const config = buildConfigSection(this);
|
|
const dns = buildDnsSection(this);
|
|
const nft = buildNftSection(this);
|
|
const conn = buildConnectivitySection(this, features.core_type || null);
|
|
const report = buildReportSection(this);
|
|
|
|
dns.run();
|
|
|
|
const runAll = ui.createHandlerFn(this, function() {
|
|
return Promise.all([
|
|
core.run(),
|
|
config.run(),
|
|
nft.run(),
|
|
conn.run()
|
|
]);
|
|
});
|
|
|
|
return E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, _('Re:HomeProxy Diagnostics')),
|
|
E('div', { 'class': 'cbi-section', 'style': 'padding-bottom:.5em' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-action important',
|
|
'click': runAll
|
|
}, _('Run All Tests')),
|
|
E('em', { 'class': 'diag-gray', 'style': 'margin-left:.8em; font-size:.9em' },
|
|
_('Runs Core, Config, Network, and Connectivity checks simultaneously.'))
|
|
]),
|
|
core.el,
|
|
config.el,
|
|
dns.el,
|
|
nft.el,
|
|
conn.el,
|
|
report.el
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|