mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 11:45:13 +08:00
✨ Sync 2026-06-07 00:43:45
This commit is contained in:
1 parent
75dec81517
commit
85f7b195f8
18 files changed
+4294
-2
No files matched your search
@@ -0,0 +1,137 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const BACKENDS = {
|
||||
daed: {
|
||||
name: 'daed',
|
||||
label: 'daed',
|
||||
uci: 'daed',
|
||||
initd: '/etc/init.d/daed',
|
||||
log: '/var/log/daed/daed.log',
|
||||
pkg: 'daed',
|
||||
hasWebUI: true,
|
||||
useNetns: true,
|
||||
defaultListen: '0.0.0.0:2023'
|
||||
},
|
||||
dae: {
|
||||
name: 'dae',
|
||||
label: 'dae',
|
||||
uci: 'dae',
|
||||
initd: '/etc/init.d/dae',
|
||||
log: '/var/log/dae/dae.log',
|
||||
pkg: 'dae',
|
||||
config: '/etc/dae/config.dae',
|
||||
example: '/etc/dae/example.dae',
|
||||
hasWebUI: false,
|
||||
useNetns: false
|
||||
}
|
||||
};
|
||||
|
||||
function execOk(cmd, args) {
|
||||
return L.resolveDefault(fs.exec(cmd, args || []), { code: 1 }).then(function(res) {
|
||||
return res && res.code === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function exists(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return !!st;
|
||||
});
|
||||
}
|
||||
|
||||
function serviceStatus(name) {
|
||||
return L.resolveDefault(callServiceList(name), {}).then(function(svc) {
|
||||
let pid = 0, running = false;
|
||||
|
||||
try {
|
||||
const inst = svc[name].instances[name];
|
||||
running = !!inst.running;
|
||||
pid = inst.pid || 0;
|
||||
} catch (e) {}
|
||||
|
||||
return { running: running, pid: pid };
|
||||
});
|
||||
}
|
||||
|
||||
function detectInstalledBackends() {
|
||||
return Promise.all([
|
||||
exists(BACKENDS.dae.initd),
|
||||
exists(BACKENDS.daed.initd)
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function detectRunning() {
|
||||
return Promise.all([
|
||||
execOk('/usr/bin/pgrep', ['-x', 'dae']),
|
||||
execOk('/usr/bin/pgrep', ['-x', 'daed'])
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function loadActiveBackend() {
|
||||
return L.resolveDefault(uci.load('daede'), null).then(function() {
|
||||
const active = uci.get('daede', 'config', 'active_backend');
|
||||
return BACKENDS[active] ? active : '';
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveBackend(name) {
|
||||
if (!BACKENDS[name])
|
||||
return Promise.reject(new Error('invalid backend: ' + name));
|
||||
|
||||
return exists('/etc/config/daede').then(function(ok) {
|
||||
if (!ok)
|
||||
return fs.write('/etc/config/daede', '');
|
||||
}).then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config=daede']); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config.active_backend=' + name]); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); });
|
||||
}
|
||||
|
||||
function detectBackend() {
|
||||
return Promise.all([
|
||||
detectInstalledBackends(),
|
||||
detectRunning(),
|
||||
loadActiveBackend()
|
||||
]).then(function(r) {
|
||||
const installed = r[0];
|
||||
const running = r[1];
|
||||
const preferred = r[2];
|
||||
let name = preferred;
|
||||
|
||||
if (!name || !installed[name])
|
||||
name = installed.daed ? 'daed' : (installed.dae ? 'dae' : 'daed');
|
||||
|
||||
return {
|
||||
name: name,
|
||||
backend: BACKENDS[name],
|
||||
installed: installed,
|
||||
running: running,
|
||||
preferred: preferred,
|
||||
bothInstalled: !!(installed.dae && installed.daed)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
BACKENDS: BACKENDS,
|
||||
detectInstalledBackends: detectInstalledBackends,
|
||||
detectRunning: detectRunning,
|
||||
detectBackend: detectBackend,
|
||||
setActiveBackend: setActiveBackend,
|
||||
serviceStatus: serviceStatus
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,305 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
const MAX_LINES = 5000;
|
||||
|
||||
const CSS = [
|
||||
'.dd-log-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-log-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:10px 14px;margin-bottom:10px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-log-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:0 0 8px;margin-bottom:8px;border-bottom:1px dashed rgba(128,128,128,.2)}',
|
||||
'.dd-log-toolbar label{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;cursor:pointer;margin:0;opacity:.85}',
|
||||
'.dd-log-toolbar input[type="checkbox"]{margin:0}',
|
||||
'.dd-log-toolbar input[type="text"]{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;min-width:160px}',
|
||||
'.dd-log-toolbar .dd-log-btn{font-size:11.5px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;cursor:pointer}',
|
||||
'.dd-log-toolbar .dd-log-btn:hover{background:rgba(128,128,128,.1)}',
|
||||
'.dd-log-toolbar .dd-log-meta{margin-left:auto;font-size:10.5px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
|
||||
'.dd-log-pane{height:60vh;min-height:360px;overflow:auto;padding:8px 10px;border:1px solid rgba(0,0,0,.08);border-radius:7px;background:#1a1d21;color:#d8dde6;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all}',
|
||||
'.dd-log-pane .dd-line{padding:1px 4px;border-radius:3px;display:block}',
|
||||
'.dd-log-pane .dd-line.dd-info{color:#cfd6df}',
|
||||
'.dd-log-pane .dd-line.dd-warn{color:#e8b95a}',
|
||||
'.dd-log-pane .dd-line.dd-error{color:#ea7878;background:rgba(234,120,120,.06)}',
|
||||
'.dd-log-pane .dd-line.dd-debug{color:#7a8290;opacity:.7}',
|
||||
'.dd-log-pane .dd-line.dd-hidden{display:none}',
|
||||
'.dd-log-pane .dd-empty{opacity:.5;font-style:italic}',
|
||||
/* 简化后的字段视觉 */
|
||||
'.dd-log-pane .dd-ts{color:#6b7480;margin-right:8px}',
|
||||
'.dd-log-pane .dd-lvl{display:inline-block;min-width:38px;padding:0 5px;margin-right:8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:.4px;text-align:center;vertical-align:1px}',
|
||||
'.dd-log-pane .dd-lvl-info{color:#7fc7a8;background:rgba(127,199,168,.08)}',
|
||||
'.dd-log-pane .dd-lvl-warn{color:#e8b95a;background:rgba(232,185,90,.10)}',
|
||||
'.dd-log-pane .dd-lvl-error{color:#ea7878;background:rgba(234,120,120,.10)}',
|
||||
'.dd-log-pane .dd-lvl-debug{color:#7a8290;background:rgba(122,130,144,.10)}',
|
||||
'.dd-log-pane .dd-msg{color:inherit}',
|
||||
'.dd-log-pane .dd-kv{margin-left:6px;color:#6b7480;font-size:11px;opacity:.85}',
|
||||
'body.dark .dd-log-card,html[data-theme="dark"] .dd-log-card,html[data-bs-theme="dark"] .dd-log-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
|
||||
'body.dark .dd-log-pane,html[data-theme="dark"] .dd-log-pane,html[data-bs-theme="dark"] .dd-log-pane{border-color:rgba(255,255,255,.1)}'
|
||||
].join('');
|
||||
|
||||
/* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */
|
||||
const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/;
|
||||
|
||||
function detectLevel(line) {
|
||||
// daed/dae logs use lvl=info / [INFO] / level=warning style
|
||||
const m = line.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i);
|
||||
if (!m) return '';
|
||||
const lvl = m[1].toUpperCase();
|
||||
if (lvl === 'DEBUG') return 'dd-debug';
|
||||
if (lvl === 'INFO') return 'dd-info';
|
||||
if (lvl.startsWith('WARN')) return 'dd-warn';
|
||||
return 'dd-error';
|
||||
}
|
||||
|
||||
/* 简化时间戳并按本地时区显示:
|
||||
* - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成本地时间
|
||||
* - 旧 logrus 短格式 (e.g. "May 25 07:04:59") 无时区信息,原样提取
|
||||
*/
|
||||
function shortTs(raw) {
|
||||
if (!raw) return raw;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(raw)) {
|
||||
const d = new Date(raw);
|
||||
if (!isNaN(d.getTime())) {
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
||||
}
|
||||
}
|
||||
const m = raw.match(/(\d{2}:\d{2}:\d{2})/);
|
||||
return m ? m[1] : raw;
|
||||
}
|
||||
|
||||
function lvlShort(level) {
|
||||
const u = (level || '').toUpperCase();
|
||||
if (u === 'WARNING') return 'WARN';
|
||||
return u || '-';
|
||||
}
|
||||
|
||||
function lvlClass(level) {
|
||||
const u = (level || '').toUpperCase();
|
||||
if (u === 'DEBUG') return 'dd-lvl-debug';
|
||||
if (u === 'INFO') return 'dd-lvl-info';
|
||||
if (u === 'WARN' || u === 'WARNING') return 'dd-lvl-warn';
|
||||
return 'dd-lvl-error';
|
||||
}
|
||||
|
||||
function buildLine(ln) {
|
||||
const cls = detectLevel(ln);
|
||||
const m = ln.match(RE_LINE);
|
||||
if (!m) {
|
||||
return E('div', { 'class': 'dd-line ' + cls }, ln);
|
||||
}
|
||||
const ts = shortTs(m[1]);
|
||||
const lvl = m[2];
|
||||
const msg = m[3] !== undefined ? m[3] : (m[4] || '');
|
||||
const kv = (m[5] || '').trim();
|
||||
const parts = [
|
||||
E('span', { 'class': 'dd-ts' }, ts),
|
||||
E('span', { 'class': 'dd-lvl ' + lvlClass(lvl) }, lvlShort(lvl)),
|
||||
E('span', { 'class': 'dd-msg' }, msg)
|
||||
];
|
||||
if (kv) parts.push(E('span', { 'class': 'dd-kv' }, kv));
|
||||
return E('div', { 'class': 'dd-line ' + cls }, parts);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return backend.detectBackend();
|
||||
},
|
||||
|
||||
render(ctx) {
|
||||
const self = this;
|
||||
const LOG_PATH = ctx.backend.log;
|
||||
const state = {
|
||||
lastContent: '',
|
||||
lastSize: -1,
|
||||
paused: false,
|
||||
autoScroll: true,
|
||||
filter: '',
|
||||
userScrolled: false
|
||||
};
|
||||
|
||||
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [
|
||||
E('div', { 'class': 'dd-empty' }, _('Loading…'))
|
||||
]);
|
||||
|
||||
// detect manual scroll → auto-pause auto-scroll
|
||||
pane.addEventListener('scroll', function() {
|
||||
const atBottom = pane.scrollHeight - pane.scrollTop - pane.clientHeight < 4;
|
||||
state.userScrolled = !atBottom;
|
||||
});
|
||||
|
||||
const meta = E('span', { 'class': 'dd-log-meta' }, '');
|
||||
|
||||
const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' });
|
||||
cbAuto.addEventListener('change', function() {
|
||||
state.autoScroll = cbAuto.checked;
|
||||
if (state.autoScroll) {
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
state.userScrolled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const cbPause = E('input', { 'type': 'checkbox' });
|
||||
cbPause.addEventListener('change', function() {
|
||||
state.paused = cbPause.checked;
|
||||
});
|
||||
|
||||
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
|
||||
E('option', { 'value': '' }, _('All')),
|
||||
E('option', { 'value': 'info' }, 'INFO'),
|
||||
E('option', { 'value': 'warn' }, 'WARN'),
|
||||
E('option', { 'value': 'error' }, 'ERROR'),
|
||||
E('option', { 'value': 'alive' }, _('Node Status')),
|
||||
E('option', { 'value': 'my_group' }, _('Proxy Traffic'))
|
||||
]);
|
||||
selFilter.addEventListener('change', function() {
|
||||
state.filter = selFilter.value;
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
const btnClear = E('button', { 'class': 'dd-log-btn' }, _('Clear View'));
|
||||
btnClear.addEventListener('click', function() {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
state.lastContent = '';
|
||||
});
|
||||
|
||||
const btnDownload = E('button', { 'class': 'dd-log-btn' }, _('Download'));
|
||||
btnDownload.addEventListener('click', function() {
|
||||
const blob = new Blob([state.lastContent || ''], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = ctx.name + '-' + (new Date()).toISOString().replace(/[:.]/g, '-') + '.log';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
const btnTruncate = E('button', { 'class': 'dd-log-btn' }, _('Clear File'));
|
||||
btnTruncate.addEventListener('click', function() {
|
||||
if (!confirm(_('Truncate %s log on the router? This cannot be undone.').format(ctx.name)))
|
||||
return;
|
||||
fs.write(LOG_PATH, '').then(function() {
|
||||
state.lastContent = '';
|
||||
state.lastSize = 0;
|
||||
}).catch(function() {});
|
||||
});
|
||||
|
||||
function applyFilter() {
|
||||
const f = state.filter;
|
||||
pane.querySelectorAll('.dd-line').forEach(function(el) {
|
||||
if (!f || el.textContent.toLowerCase().indexOf(f) !== -1)
|
||||
el.classList.remove('dd-hidden');
|
||||
else
|
||||
el.classList.add('dd-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function appendLines(text) {
|
||||
const lines = text.split('\n');
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i];
|
||||
if (!ln) continue;
|
||||
const el = buildLine(ln);
|
||||
if (state.filter && ln.toLowerCase().indexOf(state.filter) === -1)
|
||||
el.classList.add('dd-hidden');
|
||||
frag.appendChild(el);
|
||||
}
|
||||
// drop empty-state placeholder if present
|
||||
const empty = pane.querySelector('.dd-empty');
|
||||
if (empty) pane.removeChild(empty);
|
||||
pane.appendChild(frag);
|
||||
|
||||
// cap rendered rows
|
||||
const overflow = pane.children.length - MAX_LINES;
|
||||
if (overflow > 0) {
|
||||
for (let i = 0; i < overflow; i++) pane.removeChild(pane.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
pane.appendChild(E('div', { 'class': 'dd-empty' }, msg));
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (state.paused) return Promise.resolve();
|
||||
|
||||
return fs.stat(LOG_PATH).then(function(st) {
|
||||
const size = st.size || 0;
|
||||
if (size === state.lastSize) {
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
return;
|
||||
}
|
||||
|
||||
// File rotated/truncated → full reload
|
||||
const rotated = size < state.lastSize;
|
||||
return fs.read_direct(LOG_PATH, 'text').then(function(content) {
|
||||
content = content || '';
|
||||
let delta;
|
||||
if (rotated || state.lastContent === '') {
|
||||
// full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
} else if (content.indexOf(state.lastContent) === 0) {
|
||||
// pure append
|
||||
delta = content.slice(state.lastContent.length);
|
||||
} else {
|
||||
// content changed mid-stream → full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
}
|
||||
|
||||
if (delta) appendLines(delta);
|
||||
state.lastContent = content;
|
||||
state.lastSize = size;
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
|
||||
if (state.autoScroll && !state.userScrolled)
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
});
|
||||
}).catch(function(e) {
|
||||
const msg = String(e);
|
||||
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
|
||||
renderEmpty(_('Log file does not exist yet.'));
|
||||
else
|
||||
renderEmpty(_('Error reading log: %s').format(msg));
|
||||
state.lastSize = -1;
|
||||
state.lastContent = '';
|
||||
meta.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
poll.add(tick);
|
||||
tick();
|
||||
|
||||
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, [
|
||||
E('label', {}, [ cbAuto, _('Auto-scroll') ]),
|
||||
E('label', {}, [ cbPause, _('Pause') ]),
|
||||
selFilter,
|
||||
btnClear,
|
||||
btnDownload,
|
||||
btnTruncate,
|
||||
meta
|
||||
]);
|
||||
|
||||
return E('div', { 'class': 'dd-log-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('div', { 'class': 'dd-log-card' }, [
|
||||
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
|
||||
toolbar,
|
||||
pane
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
const DATA_PATHS = {
|
||||
geoip: '/usr/share/v2ray/geoip.dat',
|
||||
geosite: '/usr/share/v2ray/geosite.dat'
|
||||
};
|
||||
|
||||
const HEALTH_PATHS = {
|
||||
btf: '/sys/kernel/btf/vmlinux',
|
||||
netns: '/run/netns/daens'
|
||||
};
|
||||
|
||||
const CSS = [
|
||||
'.dd-up-wrap{padding:6px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-card-title{font-size:12px;font-weight:600;opacity:.55;margin:0 0 10px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-up-row{display:grid;grid-template-columns:24px 130px 1fr auto auto;gap:10px;align-items:center;font-size:12px;padding:7px 0;border-top:1px dashed rgba(128,128,128,.18)}',
|
||||
'.dd-up-row:first-of-type{border-top:0}',
|
||||
'.dd-up-icon{font-size:14px;text-align:center;line-height:1}',
|
||||
'.dd-up-ok{color:#3da66a}',
|
||||
'.dd-up-warn{color:#d39e00}',
|
||||
'.dd-up-err{color:#d96d6d}',
|
||||
'.dd-up-name{font-weight:600;opacity:.85}',
|
||||
'.dd-up-meta{opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-up-version{font-size:11px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap}',
|
||||
'.dd-up-btn{font-size:11px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;white-space:nowrap}',
|
||||
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
|
||||
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
|
||||
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
|
||||
'.dd-up-log.show{display:block}',
|
||||
'body.dark .dd-up-log,html[data-theme="dark"] .dd-up-log,html[data-bs-theme="dark"] .dd-up-log{color:#a3d9ad}',
|
||||
/* 手风琴折叠组 —— 与 config.js dd-adv 同构 */
|
||||
'.dd-adv{margin-bottom:14px}',
|
||||
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;cursor:pointer;user-select:none;font-size:11.5px;font-weight:600;background:rgba(128,128,128,.04);border:1px solid rgba(0,0,0,.06);border-radius:10px;color:inherit;letter-spacing:.3px;text-transform:uppercase;opacity:.7;box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv-bar:hover{background:rgba(56,134,161,.06);opacity:.95}',
|
||||
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s;text-transform:none}',
|
||||
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
|
||||
'.dd-adv-body{margin-top:8px;padding:10px 14px;border:1px solid rgba(0,0,0,.06);border-radius:10px;background:rgba(255,255,255,.02);box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv.dd-closed{margin-bottom:14px}',
|
||||
'.dd-adv.dd-closed .dd-adv-body{display:none}',
|
||||
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,body.dark .dd-adv-bar,body.dark .dd-adv-body,html[data-theme="dark"] .dd-adv-bar,html[data-theme="dark"] .dd-adv-body,html[data-bs-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-body{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}'
|
||||
].join('');
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (!n && n !== 0) return '-';
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
return (n / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function fmtMtime(epoch) {
|
||||
if (!epoch) return '';
|
||||
const d = new Date(epoch * 1000);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function probeFile(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return { exists: !!st, size: st ? st.size : 0, mtime: st ? st.mtime : 0 };
|
||||
});
|
||||
}
|
||||
|
||||
function probePkg(pkg) {
|
||||
return fs.exec('/usr/share/luci-app-daede/pkg-info.sh', [pkg]).then(function(res) {
|
||||
const out = (res.stdout || '').trim().split('\t');
|
||||
return { installed: out[0] || '', latest: out[1] || '' };
|
||||
}).catch(function() {
|
||||
return { installed: '', latest: '' };
|
||||
});
|
||||
}
|
||||
|
||||
function tailLog(path, into) {
|
||||
return L.resolveDefault(fs.read_direct(path, 'text'), '').then(function(content) {
|
||||
if (content) {
|
||||
into.textContent = content;
|
||||
into.classList.add('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
backend.detectBackend(),
|
||||
uci.load('daed').catch(function() {})
|
||||
]).then(function(r) {
|
||||
return r[0];
|
||||
});
|
||||
},
|
||||
|
||||
render: function(ctx) {
|
||||
const logPane = E('pre', { 'class': 'dd-up-log', 'id': 'dd-up-log' }, '');
|
||||
const corePkgs = [];
|
||||
const addCorePkg = function(name) {
|
||||
if (corePkgs.indexOf(name) === -1)
|
||||
corePkgs.push(name);
|
||||
};
|
||||
addCorePkg(ctx.name);
|
||||
addCorePkg('daed');
|
||||
addCorePkg('dae');
|
||||
|
||||
const dataBody = E('div', { 'id': 'dd-up-data' }, E('em', {}, _('Probing…')));
|
||||
const pkgBody = E('div', { 'id': 'dd-up-pkg' }, E('em', {}, _('Probing…')));
|
||||
const healthBody = E('div', { 'id': 'dd-up-health' }, E('em', {}, _('Probing…')));
|
||||
|
||||
const mkRow = function(icon, iconCls, name, meta, btn) {
|
||||
return E('div', { 'class': 'dd-up-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon ' + iconCls }, icon),
|
||||
E('span', { 'class': 'dd-up-name' }, name),
|
||||
E('span', { 'class': 'dd-up-meta', 'title': meta }, meta),
|
||||
E('span', {}, ''),
|
||||
btn || E('span', {}, '')
|
||||
]);
|
||||
};
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
const label = kind === 'geoip' ? 'GeoIP' : 'GeoSite';
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-geo.sh', [kind]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.' + kind + '.log', logPane); }, 2000);
|
||||
}
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Update');
|
||||
});
|
||||
};
|
||||
|
||||
// === Package updates (dae|daed / luci-app-daede) ===
|
||||
const upgradePkg = function(pkg, btn) {
|
||||
if (!confirm(_('Run "apk upgrade %s" now? This may restart the active backend.').format(pkg)))
|
||||
return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-pkg.sh', [pkg]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.pkg-' + pkg + '.log', logPane); }, 3000);
|
||||
}
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Upgrade');
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = function() {
|
||||
const probes = [
|
||||
probeFile(DATA_PATHS.geoip),
|
||||
probeFile(DATA_PATHS.geosite),
|
||||
probeFile(HEALTH_PATHS.btf),
|
||||
backend.detectRunning(),
|
||||
backend.serviceStatus(ctx.name)
|
||||
];
|
||||
|
||||
corePkgs.forEach(function(pkg) {
|
||||
probes.push(probePkg(pkg));
|
||||
});
|
||||
probes.push(probePkg('luci-app-daede'));
|
||||
|
||||
if (ctx.backend.useNetns)
|
||||
probes.push(probeFile(HEALTH_PATHS.netns));
|
||||
|
||||
return Promise.all(probes).then(function(r) {
|
||||
const geoip = r[0], geosite = r[1], btf = r[2], running = r[3], service = r[4];
|
||||
const pkgOffset = 5;
|
||||
const coreInfo = {};
|
||||
corePkgs.forEach(function(pkg, i) {
|
||||
coreInfo[pkg] = r[pkgOffset + i];
|
||||
});
|
||||
const luci = r[pkgOffset + corePkgs.length];
|
||||
const ns = r[pkgOffset + corePkgs.length + 1];
|
||||
|
||||
// data rows
|
||||
while (dataBody.firstChild) dataBody.removeChild(dataBody.firstChild);
|
||||
[
|
||||
{ k: 'geoip', name: 'GeoIP', r: geoip },
|
||||
{ k: 'geosite', name: 'GeoSite', r: geosite }
|
||||
].forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Update'));
|
||||
btn.addEventListener('click', function() { updateGeo(entry.k, btn); });
|
||||
const meta = entry.r.exists
|
||||
? fmtBytes(entry.r.size) + (entry.r.mtime ? ' · ' + fmtMtime(entry.r.mtime) : '')
|
||||
: _('missing — click Update to fetch');
|
||||
dataBody.appendChild(mkRow(
|
||||
entry.r.exists ? '✓' : '✗',
|
||||
entry.r.exists ? 'dd-up-ok' : 'dd-up-err',
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// pkg rows
|
||||
while (pkgBody.firstChild) pkgBody.removeChild(pkgBody.firstChild);
|
||||
corePkgs.map(function(pkg) {
|
||||
const label = _('%s binary').format(pkg) + (ctx.name === pkg ? ' · ' + _('active') : '');
|
||||
return { k: pkg, name: label, r: coreInfo[pkg] };
|
||||
}).concat([
|
||||
{ k: 'luci-app-daede', name: 'luci-app-daede', r: luci }
|
||||
]).forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Upgrade'));
|
||||
btn.addEventListener('click', function() { upgradePkg(entry.k, btn); });
|
||||
const sameVersion = entry.r.installed && entry.r.latest && entry.r.installed === entry.r.latest;
|
||||
const updatable = entry.r.installed && entry.r.latest && entry.r.installed !== entry.r.latest;
|
||||
let meta;
|
||||
if (!entry.r.installed) {
|
||||
meta = _('not installed via package manager');
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Unavailable');
|
||||
} else if (sameVersion) {
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('up to date');
|
||||
} else if (updatable) {
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' → ' + _('latest') + ': ' + entry.r.latest;
|
||||
} else {
|
||||
meta = _('installed') + ': ' + entry.r.installed + (entry.r.latest ? ' · ' + _('latest') + ': ' + entry.r.latest : '');
|
||||
}
|
||||
pkgBody.appendChild(mkRow(
|
||||
updatable ? '⚠' : (entry.r.installed ? '✓' : '✗'),
|
||||
updatable ? 'dd-up-warn' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// health rows
|
||||
while (healthBody.firstChild) healthBody.removeChild(healthBody.firstChild);
|
||||
healthBody.appendChild(mkRow(
|
||||
running[ctx.name] ? '✓' : '✗',
|
||||
running[ctx.name] ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('%s process').format(ctx.name),
|
||||
running[ctx.name] ? _('pgrep reports the backend is running') : _('backend process is not running')
|
||||
));
|
||||
|
||||
if (btf.exists)
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btf + ' · ' + fmtBytes(btf.size)));
|
||||
else
|
||||
healthBody.appendChild(mkRow('✗', 'dd-up-err', _('Kernel BTF'), _('not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf')));
|
||||
|
||||
if (ctx.backend.hasWebUI) {
|
||||
const listenAddr = uci.get('daed', 'config', 'listen_addr') || ctx.backend.defaultListen;
|
||||
const port = listenAddr.split(':').slice(-1)[0];
|
||||
healthBody.appendChild(mkRow(
|
||||
service.running ? '✓' : '✗',
|
||||
service.running ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('WebUI/API'),
|
||||
service.running ? _('service is running, expected on port %s').format(port) : _('service is stopped')
|
||||
));
|
||||
} else {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Hot reload'), _('/etc/init.d/dae hot_reload is available')));
|
||||
}
|
||||
|
||||
if (ctx.backend.useNetns && ns && ns.exists) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn' }, _('Clean'));
|
||||
btn.addEventListener('click', function() {
|
||||
btn.disabled = true;
|
||||
fs.exec('/sbin/ip', ['netns', 'del', 'daens']).finally(function() { btn.disabled = false; });
|
||||
});
|
||||
healthBody.appendChild(mkRow('⚠', 'dd-up-warn', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('exists, may block daed start'), btn));
|
||||
} else if (ctx.backend.useNetns) {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('clean')));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
poll.add(refresh);
|
||||
refresh();
|
||||
|
||||
return E('div', { 'class': 'dd-up-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
|
||||
dataBody
|
||||
]),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
|
||||
pkgBody
|
||||
]),
|
||||
(function() {
|
||||
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('System Health')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
E('div', { 'class': 'dd-adv-body' }, [ healthBody ])
|
||||
]);
|
||||
adv.firstChild.addEventListener('click', function() {
|
||||
adv.classList.toggle('dd-closed');
|
||||
});
|
||||
return adv;
|
||||
})(),
|
||||
logPane
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
Reference in new issue
Block a user