update 2026-10-07 18:47:42

This commit is contained in:
action committed 2026-10-07 18:47:42 +08:00
1 parent 617f7ca2a6
commit bd061545ed
4832 files changed
+1693456

No files matched your search

@@ -0,0 +1,184 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
function deriveAirportName(value) {
try {
const url = new URL(String(value || ''));
const filename = url.searchParams.get('filename');
if (filename) {
const name = filename.replace(/\.(?:ya?ml)$/i, '').trim();
if (name)
return name;
}
// Use the registrable label, not the whole hostname: sub.ssr.sh -> ssr.
// Dropping the TLD (and any sub.* prefix) keeps the default group name
// clean and avoids leaking the provider's full domain.
const labels = (url.hostname || '').split('.').filter(Boolean);
if (labels.length >= 2)
return labels[labels.length - 2];
return labels[0] || '机场_1';
} catch (e) {
return '机场_1';
}
}
function nextPastedName(names) {
const used = {};
(names || []).forEach(function(name) { used[String(name)] = true; });
let index = 1;
while (used['机场_' + index])
index++;
return '机场_' + index;
}
function makeAirportId() {
return 'airport_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
}
function backendId(id) {
return String(id || '').replace(/[^A-Za-z0-9]/g, '') || 'airport';
}
function backendGroupName(name, backend, id) {
const value = String(name || '').trim();
if (isGroupNameValid(value, backend))
return value || backendId(id);
return backendId(id);
}
function isGroupNameValid(name, backend) {
const value = String(name || '').trim();
return !!value && (backend !== 'dae' || /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value));
}
// Coerce an auto-derived name (URL hostname/filename) into a name the third
// step accepts, so the group name is pre-filled valid and needs no manual fix.
// daed allows any non-empty name (incl. CJK); dae requires
// /^[A-Za-z_][A-Za-z0-9_-]*$/, so collapse illegal chars to '_' and drop a
// leading non-letter/underscore run.
function sanitizeGroupName(name, backend) {
const value = String(name || '').trim();
if (!value || backend !== 'dae')
return value;
const cleaned = value
.replace(/[^A-Za-z0-9_-]+/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^[^A-Za-z_]+/, '')
.replace(/[-_]+$/, '');
return cleaned || 'airport';
}
function hashSource(value) {
// LuCI is commonly served over plain HTTP, where Web Crypto is unavailable.
// This stable opaque key is only used to match a previously managed source.
let h1 = 0x811c9dc5, h2 = 0x9e3779b9, h3 = 0x85ebca6b, h4 = 0xc2b2ae35;
const text = unescape(encodeURIComponent(String(value || '')));
for (let index = 0; index < text.length; index++) {
const code = text.charCodeAt(index);
h1 = Math.imul(h1 ^ code, 0x01000193);
h2 = Math.imul(h2 ^ code, 0x5bd1e995);
h3 = Math.imul(h3 ^ code, 0x27d4eb2d);
h4 = Math.imul(h4 ^ code, 0x165667b1);
}
const words = [ h1, h2, h3, h4, h1 ^ h3, h2 ^ h4, h1 ^ h2 ^ h4, h1 ^ h2 ^ h3 ^ h4 ];
return Promise.resolve(words.map(function(word) {
return (word >>> 0).toString(16).padStart(8, '0');
}).join(''));
}
function list(value) {
if (Array.isArray(value))
return value.filter(Boolean).map(String);
return value ? [ String(value) ] : [];
}
function parseAirportSection(section) {
if (!section)
return null;
return {
sid: section['.name'],
id: String(section.id || ''),
backend: String(section.backend || ''),
name: String(section.name || ''),
source_hash: String(section.source_hash || ''),
group_id: String(section.group_id || ''),
subscription_id: String(section.subscription_id || ''),
node_ids: list(section.node_id)
};
}
function airportSectionValues(record) {
return {
id: String(record.id || ''),
backend: String(record.backend || ''),
name: String(record.name || ''),
source_hash: String(record.sourceHash || record.source_hash || ''),
group_id: String(record.groupId || record.group_id || ''),
subscription_id: String(record.subscriptionId || record.subscription_id || ''),
node_id: list(record.nodeIds || record.node_ids)
};
}
function matchAirport(records, candidate) {
const sameBackend = (records || []).filter(function(record) {
return record.backend === candidate.backend;
});
if (candidate.selectedId) {
const selected = sameBackend.find(function(record) { return record.id === candidate.selectedId; });
if (selected)
return selected;
}
if (candidate.sourceHash) {
const bySource = sameBackend.find(function(record) { return record.source_hash === candidate.sourceHash; });
if (bySource)
return bySource;
}
return sameBackend.find(function(record) { return record.name === candidate.name; }) || null;
}
function safeOldNodeIds(oldIds, newIds, otherAirportNodeIds, otherGroupNodeIds) {
const keep = {};
list(newIds).forEach(function(id) { keep[id] = true; });
(otherAirportNodeIds || []).forEach(function(ids) { list(ids).forEach(function(id) { keep[id] = true; }); });
(otherGroupNodeIds || []).forEach(function(ids) { list(ids).forEach(function(id) { keep[id] = true; }); });
return list(oldIds).filter(function(id) { return !keep[id]; });
}
function findManagedGroup(groups, record) {
if (!record || !record.group_id)
return null;
return (groups || []).find(function(group) {
return group.id === record.group_id && group.name === record.name;
}) || null;
}
function findGroupByName(groups, name) {
return (groups || []).find(function(group) { return group.name === name; }) || null;
}
const api = {
deriveAirportName: deriveAirportName,
nextPastedName: nextPastedName,
makeAirportId: makeAirportId,
backendId: backendId,
backendGroupName: backendGroupName,
isGroupNameValid: isGroupNameValid,
sanitizeGroupName: sanitizeGroupName,
hashSource: hashSource,
parseAirportSection: parseAirportSection,
airportSectionValues: airportSectionValues,
matchAirport: matchAirport,
findManagedGroup: findManagedGroup,
findGroupByName: findGroupByName,
safeOldNodeIds: safeOldNodeIds
};
if (typeof module !== 'undefined' && module.exports)
module.exports = api;
if (typeof baseclass !== 'undefined')
return baseclass.extend(api);
return api;
@@ -0,0 +1,140 @@
// 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',
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() {
/* pidof matches the program name exactly and, unlike busybox `pgrep -x`
(which compares the full cmdline `/usr/bin/dae run …` and never hits),
reliably tells dae from daed without substring false positives. */
return Promise.all([
execOk('/bin/pidof', ['dae']),
execOk('/bin/pidof', ['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']); })
.then(function() { uci.set('daede', 'config', 'active_backend', name); });
}
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
});
@@ -0,0 +1,295 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
function utf8Base64(value) {
const bytes = new TextEncoder().encode(String(value));
let binary = '';
for (let i = 0; i < bytes.length; i++)
binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
function utf8FromBase64(value) {
const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
const bytes = Uint8Array.from(binary, function(ch) { return ch.charCodeAt(0); });
return new TextDecoder('utf-8').decode(bytes);
}
function base64Url(value) {
return utf8Base64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function endpoint(server, port) {
const host = String(server).indexOf(':') >= 0 && String(server)[0] !== '['
? '[' + server + ']'
: server;
return host + ':' + port;
}
function requireFields(node, fields) {
for (let i = 0; i < fields.length; i++) {
const key = fields[i];
if (node[key] === undefined || node[key] === null || node[key] === '')
throw new Error('Missing required field: ' + key);
}
}
function setIf(params, key, value) {
if (value !== undefined && value !== null && value !== '')
params.set(key, String(value));
}
function arrayValue(value) {
return Array.isArray(value) ? value.join(',') : value;
}
function addTransport(params, node) {
const network = node.network || 'tcp';
params.set('type', network);
if (network === 'ws') {
const opts = node['ws-opts'] || {};
setIf(params, 'path', opts.path);
setIf(params, 'host', opts.headers && (opts.headers.Host || opts.headers.host));
} else if (network === 'grpc') {
const opts = node['grpc-opts'] || {};
setIf(params, 'serviceName', opts['grpc-service-name'] || opts.serviceName);
} else if (network === 'http' || network === 'h2') {
const opts = node['h2-opts'] || node['http-opts'] || {};
setIf(params, 'path', Array.isArray(opts.path) ? opts.path[0] : opts.path);
setIf(params, 'host', arrayValue(opts.host));
}
}
function makeResult(node, link) {
return {
ok: true,
name: String(node.name || 'Node'),
type: String(node.type || '').toLowerCase(),
link: link,
error: ''
};
}
function convertSs(node) {
requireFields(node, [ 'server', 'port', 'cipher', 'password' ]);
const auth = base64Url(node.cipher + ':' + node.password);
let query = '';
if (node.plugin) {
const fields = [ String(node.plugin) ];
const opts = node['plugin-opts'] || {};
Object.keys(opts).forEach(function(key) {
const value = opts[key];
if (value === true)
fields.push(key);
else if (value !== false && value !== undefined && value !== null && value !== '')
fields.push(key + '=' + value);
});
const params = new URLSearchParams();
params.set('plugin', fields.join(';'));
query = '?' + params.toString();
}
return 'ss://' + auth + '@' + endpoint(node.server, node.port) + query + '#' + encodeURIComponent(node.name || 'Node');
}
function convertSsr(node) {
requireFields(node, [ 'server', 'port', 'cipher', 'password', 'protocol', 'obfs' ]);
const query = [];
query.push('remarks=' + base64Url(node.name || 'Node'));
if (node['protocol-param'])
query.push('protoparam=' + base64Url(node['protocol-param']));
if (node['obfs-param'])
query.push('obfsparam=' + base64Url(node['obfs-param']));
const payload = [
node.server,
node.port,
node.protocol,
node.cipher,
node.obfs,
base64Url(node.password)
].join(':') + '/?' + query.join('&');
return 'ssr://' + base64Url(payload);
}
function convertVmess(node) {
requireFields(node, [ 'server', 'port', 'uuid' ]);
const network = node.network || 'tcp';
const ws = node['ws-opts'] || {};
const grpc = node['grpc-opts'] || {};
const h2 = node['h2-opts'] || {};
const payload = {
v: '2',
ps: String(node.name || 'Node'),
add: String(node.server),
port: String(node.port),
id: String(node.uuid),
aid: Number(node.alterId || node.alter_id || 0),
scy: String(node.cipher || 'auto'),
net: network,
type: 'none',
host: String((ws.headers && (ws.headers.Host || ws.headers.host)) || arrayValue(h2.host) || ''),
path: String(ws.path || grpc['grpc-service-name'] || (Array.isArray(h2.path) ? h2.path[0] : h2.path) || ''),
tls: node.tls ? 'tls' : 'none',
sni: String(node.servername || node.sni || ''),
alpn: String(arrayValue(node.alpn) || ''),
fp: String(node.fingerprint || node['client-fingerprint'] || '')
};
return 'vmess://' + utf8Base64(JSON.stringify(payload));
}
function convertVless(node) {
requireFields(node, [ 'server', 'port', 'uuid' ]);
const params = new URLSearchParams();
addTransport(params, node);
params.set('encryption', String(node.encryption || 'none'));
setIf(params, 'flow', node.flow);
const reality = node['reality-opts'];
if (reality || node.tls) {
params.set('security', reality ? 'reality' : 'tls');
setIf(params, 'sni', node.servername || node.sni);
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
setIf(params, 'alpn', arrayValue(node.alpn));
if (node['skip-cert-verify'])
params.set('allowInsecure', '1');
if (reality) {
setIf(params, 'pbk', reality['public-key']);
setIf(params, 'sid', reality['short-id']);
setIf(params, 'spx', reality['spider-x']);
}
}
return 'vless://' + encodeURIComponent(node.uuid) + '@' + endpoint(node.server, node.port) +
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
}
function convertTrojan(node) {
requireFields(node, [ 'server', 'port', 'password' ]);
const params = new URLSearchParams();
addTransport(params, node);
setIf(params, 'sni', node.sni || node.servername);
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
setIf(params, 'alpn', arrayValue(node.alpn));
if (node['skip-cert-verify'])
params.set('allowInsecure', '1');
return 'trojan://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
}
function convertTuic(node) {
requireFields(node, [ 'server', 'port', 'uuid', 'password' ]);
const params = new URLSearchParams();
setIf(params, 'sni', node.sni);
setIf(params, 'alpn', arrayValue(node.alpn));
setIf(params, 'congestion_control', node['congestion-controller'] || node['congestion-control']);
setIf(params, 'udp_relay_mode', node['udp-relay-mode']);
if (node['skip-cert-verify'])
params.set('allow_insecure', '1');
return 'tuic://' + encodeURIComponent(node.uuid) + ':' + encodeURIComponent(node.password) + '@' +
endpoint(node.server, node.port) + (params.toString() ? '?' + params.toString() : '') +
'#' + encodeURIComponent(node.name || 'Node');
}
function convertHysteria2(node) {
requireFields(node, [ 'server', 'port', 'password' ]);
const params = new URLSearchParams();
setIf(params, 'sni', node.sni);
setIf(params, 'alpn', arrayValue(node.alpn));
setIf(params, 'obfs', node.obfs);
setIf(params, 'obfs-password', node['obfs-password']);
if (node['skip-cert-verify'])
params.set('insecure', '1');
return 'hysteria2://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
}
function convertAnytls(node) {
requireFields(node, [ 'server', 'port', 'password' ]);
const params = new URLSearchParams();
setIf(params, 'sni', node.sni || node.servername);
setIf(params, 'alpn', arrayValue(node.alpn));
setIf(params, 'client-fingerprint', node['client-fingerprint'] || node.fingerprint);
if (node['skip-cert-verify'])
params.set('insecure', '1');
if (node.udp)
params.set('udp', '1');
return 'anytls://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
}
function isMetadataProxy(node) {
const name = String(node && node.name || '').trim();
if (!name)
return false;
return /(?:官网|QQ|流量|续费|应急|重置|到期|过期|剩余|套餐)/i.test(name) ||
/^(?:traffic|bandwidth|expire|expiry|expiration|subscription(?:\s+info)?|official\s+website|website)\s*[::|]/i.test(name) ||
/^(?:(?:剩余|可用|已用|总)?流量|套餐到期|到期时间|到期日|过期时间|有效期|订阅信息)\s*[::|]/i.test(name) ||
/^(?:官方网站|官网地址|官方网址|网站地址)\s*[::|]/i.test(name) ||
/^(?:加入|联系|客服)?\s*QQ\s*(?:群|交流群|客服|联系)\s*[::|]/i.test(name);
}
function convertProxy(node) {
const type = String(node && node.type || '').toLowerCase();
const converters = {
ss: convertSs,
ssr: convertSsr,
vmess: convertVmess,
vless: convertVless,
trojan: convertTrojan,
tuic: convertTuic,
hysteria2: convertHysteria2,
hy2: convertHysteria2,
anytls: convertAnytls
};
try {
if (!node || typeof node !== 'object')
throw new Error('Node must be an object');
if (!converters[type])
throw new Error('Unsupported protocol: ' + (type || 'unknown'));
return makeResult(node, converters[type](node));
} catch (e) {
return {
ok: false,
name: String(node && node.name || 'Node'),
type: type || 'unknown',
link: '',
error: String(e && e.message || e)
};
}
}
function convertProxies(proxies) {
return Array.isArray(proxies) ? proxies.map(convertProxy) : [];
}
function normalizeLink(link) {
const value = String(link || '').trim();
if (value.indexOf('vmess://') === 0) {
try {
const payload = JSON.parse(utf8FromBase64(value.slice(8)));
delete payload.ps;
return 'vmess://' + utf8Base64(JSON.stringify(payload));
} catch (e) {}
}
return value.replace(/#.*$/, '');
}
const api = {
convertProxy: convertProxy,
convertProxies: convertProxies,
isMetadataProxy: isMetadataProxy,
normalizeLink: normalizeLink
};
if (typeof module !== 'undefined' && module.exports)
module.exports = api;
if (typeof baseclass !== 'undefined')
return baseclass.extend(api);
return api;
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require uci';
'require network';
'require view';
'require view.daede.backend as backend';
'require view.daede.styles as styles';
'require view.daede.widgets as widgets';
'require view.daede.dae as daeView';
'require view.daede.daed as daedView';
return view.extend({
_loadContext: function() {
return backend.detectBackend().then(function(ctx) {
return uci.load(ctx.backend.uci).catch(function() {}).then(function() {
/* enumerate L3 devices so the dae LAN/WAN fields offer a dropdown
(still free-text, so undetectable WANs can be typed) */
return network.getDevices().catch(function() { return []; }).then(function(devs) {
ctx.netDevs = (devs || []).map(function(d) { return d.getName(); })
.filter(function(n) { return n && n !== 'dae0'; }).sort();
return ctx;
});
});
});
},
load: function() {
return this._loadContext();
},
render: function(ctx) {
const self = this;
const themeHref = Array.prototype.map.call(document.styleSheets, function(sheet) { return sheet.href || ''; }).join(' ');
document.documentElement.setAttribute('data-daede-theme', /\/argon\//.test(themeHref) ? 'argon' : 'bootstrap');
/* themes signal dark differently — BootstrapDark sets data-darkmode,
Argon just loads a dark stylesheet with no flag. Read the first opaque
background up the tree; a transparent body (CSS not applied yet) must
NOT count as black, else argon light is misdetected as dark. */
try {
const probe = [document.body, document.documentElement];
for (let i = 0; i < probe.length; i++) {
const m = getComputedStyle(probe[i]).backgroundColor.match(/[\d.]+/g);
if (!m) continue;
const a = m.length >= 4 ? parseFloat(m[3]) : 1;
if (a < 0.1) continue; // transparent — keep looking
if (0.299 * m[0] + 0.587 * m[1] + 0.114 * m[2] < 128)
document.documentElement.setAttribute('data-darkmode', 'true');
break; // first opaque background decides
}
} catch (e) {}
const redrawBackend = function(name, message) {
self._backendHint = message;
return self._loadContext()
.then(function(nextCtx) { return self.render(nextCtx); })
.then(function(nextRoot) {
const current = document.querySelector('.dd-config-page');
if (!current) return;
Array.prototype.forEach.call(current.querySelectorAll('.dd-status-card'), function(card) {
if (card._ddCleanup) card._ddCleanup();
});
current.replaceWith(nextRoot);
if (self._backendHintTimer) clearTimeout(self._backendHintTimer);
self._backendHintTimer = setTimeout(function() {
self._backendHint = '';
const hint = document.querySelector('.dd-config-page .dd-backend-help');
if (hint) hint.textContent = '';
}, 3500);
});
};
const listenAddr = uci.get('daed', 'config', 'listen_addr') || backend.BACKENDS.daed.defaultListen;
const children = [
E('style', {}, styles.CSS),
widgets.renderStatusCard(ctx, listenAddr),
widgets.renderBackendSwitcher(ctx, redrawBackend, self._backendHint)
].filter(function(node) { return !!node; });
if (!ctx.installed[ctx.name]) {
children.push(E('div', { 'class': 'dd-card dd-warning' }, _('Selected backend is not installed. Install dae or daed from the package feed first.')));
} else if (ctx.name === 'dae') {
children.push(daeView.renderDaeImportBanner());
children.push(daeView.renderDaeForms(ctx));
children.push(daeView.renderDaeEditor());
} else {
children.push(daedView.renderDaedSettings());
}
return Promise.all(children.map(function(child) {
return child && child.then ? child : Promise.resolve(child);
})).then(function(nodes) {
return E('div', { 'class': 'dd-wrap dd-config-page' }, nodes.filter(function(n) { return !!n; }));
});
},
/* this view drives its own save buttons (dae form / daed settings / manual
editor) — suppress LuCI's global Save/Apply/Reset footer so there is one
unambiguous primary action per backend */
handleSave: null,
handleSaveApply: null,
handleReset: null
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,794 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
'require form';
'require fs';
'require uci';
'require ui';
'require view.daede.backend as backend';
/* dae 示例里的占位订阅特征 —— example.com、relative/path/to 或中文占位文本 */
const RE_PLACEHOLDER_URL = /(['"])(?:(?:https?|https-file|file):\/\/[^'"]*?(?:example\.com|relative\/path\/to)[^'"]*|你的订阅链接或者机场链接)\1/;
/* Minimal ready-to-run default: gen-dae-config.sh defaults + a placeholder subscription to swap. */
const DEFAULT_TEMPLATE =
'# luci-app-daede 默认配置:把 subscription 里的占位链接换成你的机场订阅,保存即可运行。\n' +
'global {\n' +
' # 端口与网卡:lan_interface 填内网桥(通常 br-lan),wan_interface 保持 auto 即可。\n' +
' tproxy_port: 12345\n' +
' tproxy_port_protect: true\n' +
' so_mark_from_dae: 0\n' +
' log_level: info\n' +
' lan_interface: br-lan\n' +
' wan_interface: auto\n' +
' auto_config_kernel_parameter: true\n' +
' dial_mode: domain\n' +
" tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'\n" +
" udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'\n" +
' check_interval: 30s\n' +
' check_tolerance: 50ms\n' +
'}\n' +
'\n' +
'subscription {\n' +
' # ⚠ 把下面这行的 URL 换成你机场的订阅链接,然后保存。\n' +
" my_airport: 'https://example.com/sub'\n" +
'}\n' +
'\n' +
'dns {\n' +
' # 国内域名走 cndns 解析,其余走 fallbackdns,避免 DNS 污染。\n' +
' ipversion_prefer: 4\n' +
' response_ttl: 0\n' +
' upstream {\n' +
" cndns: 'udp://dns.alidns.com:53'\n" +
" fallbackdns: 'tcp+udp://dns.google:53'\n" +
' }\n' +
' routing {\n' +
' request {\n' +
' qname(suffix:googleapis.cn) -> fallbackdns\n' +
' qname(geosite:cn) -> cndns\n' +
' fallback: fallbackdns\n' +
' }\n' +
' }\n' +
'}\n' +
'\n' +
'group {\n' +
' # 节点分组:filter 决定用订阅里的哪些节点,policy 决定怎么选(min_moving_avg = 自动挑最快)。\n' +
' proxy {\n' +
' filter: subtag(my_airport)\n' +
' policy: min_moving_avg\n' +
' }\n' +
'}\n' +
'\n' +
'routing {\n' +
' # 分流规则:从上往下匹配,命中即停,最后由 fallback 兜底。\n' +
' # 想让某个网站直连,在 fallback 之前加一行,例如:domain(example.com) -> direct\n' +
' pname(NetworkManager) -> direct\n' +
' dip(224.0.0.0/3) -> direct\n' +
' dip(255.255.255.255/32) -> direct\n' +
" dip('ff00::/8') -> direct\n" +
' dip(geoip:private) -> direct\n' +
' l4proto(udp) && dport(123) -> direct\n' +
' domain(connectivitycheck.gstatic.com) -> direct\n' +
' domain(msftconnecttest.com) -> direct\n' +
' domain(suffix:googleapis.cn) -> proxy\n' +
' dip(geoip:cn) -> direct\n' +
' domain(geosite:cn) -> direct\n' +
' fallback: proxy\n' +
'}\n';
function detectPlaceholders(text) {
if (!text) return [];
const lines = text.split('\n');
const out = [];
for (let i = 0; i < lines.length; i++) {
const ln = lines[i];
const stripped = ln.replace(/^\s*#.*$/, '');
if (!stripped) continue;
const m = stripped.match(RE_PLACEHOLDER_URL);
if (m) out.push({ line: i + 1, url: m[2], raw: ln.trim() });
}
return out;
}
const GEN = '/usr/share/luci-app-daede/gen-dae-config.sh';
/* Table cell renderer: truncate long links so rows stay compact; full value
stays visible in the edit dialog and on hover. */
function ellipsisCell(section_id) {
const v = this.cfgvalue(section_id) || '';
/* middle-ellipsis keeps the host head + tail visible (the meaningful parts
of a sub link) while staying short enough for one line on a phone */
const short = v.length > 30 ? v.slice(0, 18) + '…' + v.slice(-9) : v;
return E('span', { 'title': v, 'style': 'font-family:ui-monospace,Menlo,monospace;font-size:12px' }, short);
}
/* Tag is auto-generated and rarely relevant to beginners — render it muted. */
function mutedCell(section_id) {
const v = this.cfgvalue(section_id) || '';
return E('span', { 'class': 'dd-meta', 'style': 'opacity:.55;font-size:11.5px' }, v);
}
/* Fill blank tags of a section type with unique <prefix>_<n> values so dae's
config generator always has a stable identifier. Runs after m.save() wrote
the form values into the uci session, before uci.save(). */
function autofillTags(stype, prefix) {
const secs = uci.sections('dae', stype) || [];
const used = {};
secs.forEach(function(s) { if (s.tag) used[s.tag] = 1; });
let n = 1;
secs.forEach(function(s) {
if (s.tag) return;
while (used[prefix + '_' + n]) n++;
const t = prefix + '_' + n;
uci.set('dae', s['.name'], 'tag', t);
used[t] = 1; n++;
});
}
/* Wrap every form section in a collapsible accordion. openTitles lists the
section titles that should start expanded; the rest start collapsed. */
function accordionizeSections(mapNode, openTitles) {
const secs = mapNode.querySelectorAll('.cbi-section');
for (let i = 0; i < secs.length; i++) {
const sec = secs[i];
const h = sec.querySelector('h3');
const title = h ? h.textContent.trim() : _('Section');
const open = (openTitles || []).indexOf(title) >= 0;
const body = E('div', { 'class': 'dd-adv-body' });
while (sec.firstChild) body.appendChild(sec.firstChild);
/* drop the moved <h3> so it doesn't duplicate the accordion bar title */
const dupTitle = body.querySelector('h3');
if (dupTitle) dupTitle.parentNode.removeChild(dupTitle);
const wrap = E('div', { 'class': 'dd-adv' + (open ? '' : ' dd-closed') }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, title),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
body
]);
wrap.firstChild.addEventListener('click', function() { wrap.classList.toggle('dd-closed'); });
sec.appendChild(wrap);
}
}
function organizeDaeSections(mapNode, openTitles) {
accordionizeSections(mapNode, openTitles);
const advTitles = [ _('Groups'), _('Routing'), _('DNS'), _('Logging') ];
const advWraps = [];
mapNode.querySelectorAll('.dd-adv').forEach(function(w) {
const t = w.querySelector('.dd-adv-bar span');
if (t && advTitles.indexOf(t.textContent.trim()) >= 0) advWraps.push(w);
});
if (!advWraps.length) return;
const outerOpen = (openTitles || []).indexOf(_('Advanced settings')) >= 0;
const outerBody = E('div', { 'class': 'dd-adv-body' });
const outer = E('div', { 'class': 'dd-adv' + (outerOpen ? '' : ' dd-closed') }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, _('Advanced settings')),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
outerBody
]);
outer.firstChild.addEventListener('click', function() { outer.classList.toggle('dd-closed'); });
advWraps[0].parentNode.insertBefore(outer, advWraps[0]);
advWraps.forEach(function(w) { outerBody.appendChild(w); });
}
function captureAccordionState(mapNode) {
if (!mapNode) return [];
return Array.prototype.map.call(
mapNode.querySelectorAll('.dd-adv:not(.dd-closed) > .dd-adv-bar span:first-child'),
function(node) { return node.textContent.trim(); }
);
}
function restoreAccordionState(mapNode, openTitles) {
if (!mapNode || mapNode.querySelector('.dd-adv')) return;
organizeDaeSections(mapNode, openTitles || []);
}
/* Friendly form UI for the dae backend. Form is the source of truth: on save we
commit the `dae` UCI package, then gen-dae-config.sh renders config.dae,
validates it and hot-reloads. */
function renderDaeForms(ctx) {
let m, s, o;
m = new form.Map('dae', null, null);
/* Subscriptions */
s = m.section(form.GridSection, 'subscription', _('Subscriptions'), null);
s.addremove = true;
s.anonymous = true;
s.sortable = false;
o = s.option(form.Value, 'tag', _('Tag'));
o.placeholder = _('auto-generated');
o.textvalue = mutedCell;
o = s.option(form.Value, 'url', _('Subscription URL'));
o.rmempty = false;
o.placeholder = 'https://example.com/sub';
o.textvalue = ellipsisCell; /* keep long links from blowing up the row */
o.validate = function(sid, v) {
if (!v) return _('URL is required');
if (!/^(https?|https-file|file):\/\//.test(v)) return _('Must start with http(s):// or file://');
return true;
};
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '1';
o.editable = true;
/* Manual nodes (share links) */
s = m.section(form.GridSection, 'node', _('Manual nodes'),
_('One node share link per line — ss, vmess, vless, trojan, tuic, hysteria2, socks5.'));
s.addremove = true;
s.anonymous = true;
s.sortable = false;
o = s.option(form.Value, 'tag', _('Tag'));
o.placeholder = _('auto-generated');
o.textvalue = mutedCell;
o = s.option(form.Value, 'link', _('Share Link'));
o.rmempty = false;
o.placeholder = 'vmess://...';
o.textvalue = ellipsisCell;
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '1';
o.editable = true;
/* Subscription / manual-node tags for the group filter dropdowns */
const subTags = (uci.sections('dae', 'subscription') || [])
.map(function(x) { return x.tag; }).filter(function(x) { return !!x; });
const nodeTags = (uci.sections('dae', 'node') || [])
.map(function(x) { return x.tag; }).filter(function(x) { return !!x; });
/* Groups (outbound) */
s = m.section(form.TypedSection, 'group', _('Groups'),
_('Outbound groups. Leave the source empty to use all nodes. Set the route fallback to one of these.'));
s.addremove = true;
s.anonymous = true;
o = s.option(form.Value, 'name', _('Name'));
o.rmempty = false;
o.default = 'proxy'; /* real default value, editable — a bright placeholder looked filled and tripped the required check */
o.placeholder = 'proxy';
o.validate = function(sid, v) {
if (!v) return _('Name is required');
/* dae fatals on duplicate outbound names — keep group names unique */
const dup = (uci.sections('dae', 'group') || []).some(function(g) {
return g['.name'] !== sid && g.name === v;
});
return dup ? _('Group name must be unique') : true;
};
o = s.option(form.ListValue, 'policy', _('Policy'));
o.value('min_moving_avg', _('Min moving average latency'));
o.value('min', _('Min last latency'));
o.value('min_avg10', _('Min average of last 10'));
o.value('random', _('Random'));
o.value('fixed(0)', _('Fixed (first node)'));
o.default = 'min_moving_avg';
o = s.option(form.DynamicList, 'source', _('Source'),
_('Subscriptions and nodes that feed this group. Pick from the list or type a name. Leave empty to use all nodes.'));
subTags.forEach(function(t) { o.value(t, t + ' (' + _('subscription') + ')'); });
nodeTags.forEach(function(t) { o.value(t, t + ' (' + _('node') + ')'); });
o = s.option(form.DynamicList, 'name_filter', _('Name filter'),
_('Optional. Narrows subscriptions in the source to nodes whose name matches — e.g. 香港 or 新加坡|日本. Nodes you picked by name are always kept.'));
o.placeholder = '新加坡|日本';
/* Routing presets */
const groupNames = (uci.sections('dae', 'group') || [])
.map(function(x) { return x.name; }).filter(function(x) { return !!x; });
s = m.section(form.NamedSection, 'routing', 'routing', _('Routing'));
s.addremove = false;
o = s.option(form.Flag, 'private_direct', _('Private IPs direct'),
_('LAN / private addresses bypass the proxy.'));
o.default = '1';
o = s.option(form.Flag, 'cn_direct', _('China direct'),
_('China mainland IPs and domains bypass the proxy.'));
o.default = '1';
o = s.option(form.Flag, 'block_ads', _('Block ads'),
_('Drop traffic matching the ad domain list.'));
o.default = '0';
o = s.option(form.Value, 'fallback', _('Fallback group'),
_('Default outbound for everything else.'));
o.default = 'proxy'; /* matches the default proxy group so the box works out of the box */
if (groupNames.length)
groupNames.forEach(function(n) { o.value(n, n); });
else
o.value('proxy', 'proxy');
o = s.option(form.DynamicList, 'custom', _('Custom rules'),
_('Raw dae routing lines, evaluated before fallback. Target must be a group name above (or direct/block).'));
o.placeholder = 'dip(geoip:jp) -> proxy';
/* DNS presets */
s = m.section(form.NamedSection, 'dns', 'dns', _('DNS'));
s.addremove = false;
o = s.option(form.Value, 'cn_upstream', _('China upstream'),
_('Resolves China-mainland domains.'));
o.default = 'udp://dns.alidns.com:53';
o.placeholder = 'udp://dns.alidns.com:53';
o = s.option(form.Value, 'fallback_upstream', _('Fallback upstream'),
_('Resolves everything else.'));
o.default = 'tcp+udp://dns.google:53';
o.placeholder = 'tcp+udp://dns.google:53';
o = s.option(form.Value, 'response_ttl', _('DNS response TTL'),
_('TTL returned to LAN DNS clients. 0 keeps dae default behavior.'));
o.datatype = 'uinteger';
o.default = '0';
o.placeholder = '60';
/* Logging — folded into the main form so the single Save button covers it
too (no separate native save bar) */
/* Network interfaces — override dae auto WAN detect, fails on legacy swconfig (#15).
Combobox: dropdown of real devices but still free-text for undetectable WANs */
const netDevs = (ctx && ctx.netDevs) || [];
s = m.section(form.NamedSection, 'config', 'dae', _('Network interfaces'));
s.addremove = false;
o = s.option(form.Value, 'lan_interface', _('LAN interface'),
_('Interface to proxy the LAN from. Usually br-lan.'));
o.default = 'br-lan';
o.placeholder = 'br-lan';
netDevs.forEach(function(d) { o.value(d); });
o = s.option(form.Value, 'wan_interface', _('WAN interface'),
_('Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set it, e.g. eth0.2.'));
o.default = 'auto';
o.placeholder = 'auto';
o.value('auto', 'auto');
netDevs.forEach(function(d) { o.value(d); });
s = m.section(form.NamedSection, 'config', 'dae', _('Logging'));
s.addremove = false;
o = s.option(form.ListValue, 'log_level', _('Log level'),
_('Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting.'));
o.value('error', 'ERROR');
o.value('warn', 'WARN');
o.value('info', 'INFO');
o.value('debug', 'DEBUG');
o.value('trace', 'TRACE');
o.default = 'info';
o.rmempty = false;
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
_('Rotate the log file once it grows past this many megabytes.'));
o.datatype = 'uinteger';
o.default = '1';
o = s.option(form.Value, 'log_maxbackups', _('Max Log Backups'),
_('Number of rotated log files to keep.'));
o.datatype = 'uinteger';
o.default = '1';
const status = E('span', { 'class': 'dd-editor-status' }, '');
let renderSaveActions = function() {};
let statusTimer = null;
function flash(text, kind, hold) {
status.textContent = text;
status.classList.remove('ok', 'err');
if (kind) status.classList.add(kind);
status.classList.add('show');
if (statusTimer) clearTimeout(statusTimer);
statusTimer = setTimeout(function() { status.classList.remove('show'); }, hold || 3000);
}
/* one save pipeline, parameterised by intent:
- opts.start: after generate, enable + start dae (stopped → "Save and Start")
- otherwise just save + generate; generate hot-reloads itself when dae runs */
function doSave(opts, btns) {
const settingsCard = status.closest('.dd-settings-card');
const openTitles = captureAccordionState(settingsCard && settingsCard.querySelector('.cbi-map'));
const restoreLiveAccordions = function() {
const liveCard = status.closest('.dd-settings-card');
restoreAccordionState(liveCard && liveCard.querySelector('.cbi-map'), openTitles);
};
btns.forEach(function(b) { b.disabled = true; });
flash(_('Saving…'));
/* ensure the singleton sections exist before parsing the form —
on installs where uci-defaults never seeded them, writing an
option to a missing named section fails with ubus NOT_FOUND (4) */
if (!uci.get('dae', 'routing')) uci.add('dae', 'routing', 'routing');
if (!uci.get('dae', 'dns')) uci.add('dae', 'dns', 'dns');
if (!uci.get('dae', 'config')) uci.add('dae', 'dae', 'config');
return m.save(null, true)
.then(function() {
restoreLiveAccordions();
/* assign unique tags to any rows the user left blank */
autofillTags('subscription', 'subscription');
autofillTags('node', 'node');
})
.then(function() { return uci.save(); })
.then(function() {
/* apply flushes the rpc session to /etc/config so the
generator (reads /etc/config/dae) sees the form data;
skip it when nothing changed — apply on an empty
changeset throws ubus NO_DATA (code 5). */
return uci.changes().then(function(ch) {
return (ch && Object.keys(ch).length) ? uci.apply() : null;
});
})
.then(function() { return fs.exec(GEN, ['generate']); })
.then(function(res) {
if (res && res.code !== 0) {
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
flash(_('Apply failed: %s').format(err), 'err', 9000);
throw new Error('gen-failed');
}
if (!opts.start) {
/* generate already hot-reloaded if dae was running */
flash(opts.okMsg || _('Saved'), 'ok');
return;
}
flash(_('Starting…'));
return fs.exec('/sbin/uci', ['set', 'dae.config.enabled=1'])
.then(function() { return fs.exec('/sbin/uci', ['commit', 'dae']); })
.then(function() { return fs.exec(backend.BACKENDS.dae.initd, ['enable']); })
.then(function() { return fs.exec(backend.BACKENDS.dae.initd, ['start']); })
.then(function(r2) {
if (r2 && r2.code !== 0)
flash(_('Start failed: %s').format(r2.stderr || r2.stdout || ('exit ' + r2.code)), 'err', 9000);
else {
flash(_('Saved · started'), 'ok');
renderSaveActions(true);
}
});
})
.catch(function(e) {
if (e && e.message === 'gen-failed') return;
if (e && e.name === 'CBIValidationError') {
flash(_('Please fix the highlighted fields.'), 'err', 6000);
return;
}
flash(_('Save failed: %s').format(e.message || e), 'err', 9000);
})
.finally(function() {
restoreLiveAccordions();
btns.forEach(function(b) { b.disabled = false; });
window.setTimeout(function() { ui.changes.init(); }, 1800);
});
}
return m.render().then(function(mapNode) {
/* beginner default: only Subscriptions starts open; nodes/groups/routing/dns/logging collapse */
organizeDaeSections(mapNode, [ _('Subscriptions') ]);
const cardChildren = [ E('h4', { 'class': 'dd-card-title' }, _('dae Configuration')) ];
cardChildren.push(mapNode);
/* state-aware primary action: running → one "Save and Apply"; stopped →
"Save config" (secondary) + "Save and Start" (primary). Starting the
proxy is an explicit choice, never an implicit side effect of saving. */
return backend.detectRunning().then(function(rr) {
const running = rr && rr.dae;
/* "Update all subscriptions" only helps while dae runs (hot reload
re-pulls them). A stopped dae re-fetches every subscription on
start, so there is nothing to update — hide the button then. */
if (running) {
const upBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Update all subscriptions'));
const upMsg = E('span', { 'class': 'dd-meta', 'style': 'margin-left:8px;display:none' }, '');
const setUp = function(text, kind) {
upMsg.textContent = text;
upMsg.style.display = '';
upMsg.style.color = (kind === 'err') ? 'var(--error-color, #d33)' : 'var(--success-color, #2a8)';
if (kind !== 'err')
setTimeout(function() { if (upMsg.textContent === text) upMsg.style.display = 'none'; }, 5000);
};
upBtn.addEventListener('click', function(ev) {
ev.preventDefault();
upBtn.disabled = true;
setUp(_('Updating…'));
fs.exec(backend.BACKENDS.dae.initd, ['hot_reload']).then(function(res) {
if (res && res.code !== 0)
setUp(_('Update failed: %s').format(res.stderr || res.stdout || ('exit ' + res.code)), 'err');
else
setUp(_('Subscriptions updated (dae reloaded)'), 'ok');
}).catch(function(e) {
setUp(_('Update failed: %s').format(e.message || e), 'err');
}).finally(function() { upBtn.disabled = false; });
});
const upWrap = E('div', { 'class': 'dd-actions', 'style': 'margin:0 0 8px' }, [ upBtn, upMsg ]);
const advs = mapNode.querySelectorAll('.dd-adv');
for (let i = 0; i < advs.length; i++) {
const t = advs[i].querySelector('.dd-adv-bar span');
if (t && t.textContent.trim() === _('Subscriptions')) {
const body = advs[i].querySelector('.dd-adv-body');
const tbl = body && body.querySelector('.cbi-section-table');
if (tbl) tbl.parentNode.insertBefore(upWrap, tbl);
else if (body) body.insertBefore(upWrap, body.firstChild);
break;
}
}
}
const actionsWrap = E('div', { 'class': 'dd-editor-actions' });
renderSaveActions = function(isRunning) {
while (actionsWrap.firstChild) actionsWrap.removeChild(actionsWrap.firstChild);
let btns;
if (isRunning) {
const apply = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply'));
btns = [ apply ];
apply.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved · applied') }, btns); });
actionsWrap.appendChild(apply);
} else {
const saveOnly = E('button', { 'class': 'cbi-button' }, _('Save config'));
const saveStart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Start'));
btns = [ saveOnly, saveStart ];
saveOnly.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved') }, btns); });
saveStart.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ start: true }, btns); });
actionsWrap.appendChild(saveOnly);
actionsWrap.appendChild(saveStart);
}
actionsWrap.appendChild(status);
};
renderSaveActions(running);
cardChildren.push(actionsWrap);
return E('div', { 'class': 'dd-card dd-settings-card' }, cardChildren);
});
});
}
/* Import banner: offer to pull subscription/node from an existing hand-written
config.dae into the form when the form is still empty. */
function renderDaeImportBanner() {
const hasForms = (uci.sections('dae', 'subscription') || []).length
|| (uci.sections('dae', 'node') || []).length;
if (hasForms)
return Promise.resolve(null);
/* stat first (ubus): avoid a 404 when config.dae doesn't exist yet */
return fs.stat(backend.BACKENDS.dae.config).then(function() {
return fs.read_direct(backend.BACKENDS.dae.config, 'text');
}).then(function(content) {
if (!content || !/\b(subscription|node)\s*\{/.test(content))
return null;
const btn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Import from existing config'));
const note = E('span', { 'class': 'dd-meta', style: 'margin-left:8px' }, '');
btn.addEventListener('click', function(ev) {
ev.preventDefault();
btn.disabled = true;
note.textContent = _('Importing…');
fs.exec(GEN, ['import']).then(function(res) {
if (res && res.code === 0) {
note.textContent = _('Imported, reloading…');
setTimeout(function() { window.location.reload(); }, 600);
} else {
note.textContent = _('Import failed');
btn.disabled = false;
}
}).catch(function() { note.textContent = _('Import failed'); btn.disabled = false; });
});
return E('div', { 'class': 'dd-card', style: 'border-color:rgba(217,158,0,.4)' }, [
E('div', { 'class': 'dd-ph-warn-title' }, _('Existing config detected')),
E('div', { 'class': 'dd-settings-descr', style: 'margin:4px 0 8px' },
_('You have a hand-written config.dae. Import its subscriptions and nodes into the form, or keep editing manually below. Saving the form overwrites config.dae.')),
E('div', { 'class': 'dd-actions' }, [ btn, note ])
]);
}).catch(function() { return null; });
}
/* Lightweight dae-DSL syntax highlighter: tokenises into spans for a <pre>
layer drawn behind a transparent textarea (no editor library needed). */
function highlightDae(src) {
const re = /(#[^\n]*)|('[^'\n]*')|\b(global|subscription|node|group|routing|dns|include|upstream|request|response)\b|\b([A-Za-z_][\w-]*)(?=\s*\()|(->|&&|\|\|)|\b(geoip|geosite)(?=:)/g;
let out = '', last = 0, m;
const esc = function(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); };
while ((m = re.exec(src)) !== null) {
out += esc(src.slice(last, m.index));
const cls = m[1] ? 'c' : m[2] ? 's' : m[3] ? 'k' : m[4] ? 'f' : m[5] ? 'o' : 'g';
out += '<span class="dh-' + cls + '">' + esc(m[0]) + '</span>';
last = m.index + m[0].length;
}
out += esc(src.slice(last));
return out;
}
function renderDaeEditor() {
const textarea = E('textarea', {
'class': 'dd-editor dd-editor-hl',
'spellcheck': 'false',
'wrap': 'off',
'placeholder': _('dae config file is empty. Click "Load default config" to start, or paste your config here.')
}, '');
const hlCode = E('code', {});
const hlPre = E('pre', { 'class': 'dd-hl', 'aria-hidden': 'true' }, hlCode);
const editWrap = E('div', { 'class': 'dd-edit-wrap' }, [ hlPre, textarea ]);
function syncHL() {
hlCode.innerHTML = highlightDae(textarea.value) + '\n';
hlPre.scrollTop = textarea.scrollTop;
hlPre.scrollLeft = textarea.scrollLeft;
}
textarea.addEventListener('scroll', function() {
hlPre.scrollTop = textarea.scrollTop;
hlPre.scrollLeft = textarea.scrollLeft;
});
const save = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save manual config'));
const init = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Load default config'));
const status = E('span', { 'class': 'dd-editor-status' }, '');
const phWarn = E('div', { 'class': 'dd-ph-warn' }, [
E('div', { 'class': 'dd-ph-warn-title' }, ''),
E('div', { 'class': 'dd-ph-howto' }, ''),
E('div', { 'class': 'dd-ph-list-label' }, _('Placeholder lines (click line number to jump):')),
E('ul', { 'class': 'dd-ph-list' })
]);
/* Footer:dae version · 行数 · 占位剩余 */
const fbVer = E('span', { 'class': 'dd-fb-item' }, [ E('span', { 'class': 'dd-fb-key' }, 'dae'), E('span', {}, '—') ]);
const fbLine = E('span', { 'class': 'dd-fb-item' }, [ E('span', { 'class': 'dd-fb-key' }, _('lines')), E('span', {}, '0') ]);
const fbStat = E('span', { 'class': 'dd-fb-item dd-fb-ok' }, '✓ ready');
const footer = E('div', { 'class': 'dd-editor-footer' }, [ fbVer, fbLine, fbStat ]);
let statusTimer = null;
function flashStatus(text, kind, holdMs) {
status.textContent = text;
status.classList.remove('ok', 'err');
if (kind) status.classList.add(kind);
status.classList.add('show');
if (statusTimer) clearTimeout(statusTimer);
statusTimer = setTimeout(function() {
status.classList.remove('show');
}, holdMs || 3000);
}
function jumpToLine(lineNo) {
const lines = textarea.value.split('\n');
let offset = 0;
for (let i = 0; i < lineNo - 1 && i < lines.length; i++)
offset += lines[i].length + 1;
textarea.focus();
textarea.setSelectionRange(offset, offset + (lines[lineNo - 1] || '').length);
const lineHeight = 18;
textarea.scrollTop = Math.max(0, (lineNo - 4) * lineHeight);
}
function refreshFooter(phCount) {
const lines = textarea.value ? textarea.value.split('\n').length : 0;
fbLine.lastChild.textContent = String(lines);
while (fbStat.firstChild) fbStat.removeChild(fbStat.firstChild);
fbStat.classList.remove('dd-fb-ok', 'dd-fb-warn');
if (phCount > 0) {
fbStat.classList.add('dd-fb-warn');
fbStat.textContent = _('⚠ %d placeholder').format(phCount);
} else {
fbStat.classList.add('dd-fb-ok');
fbStat.textContent = lines > 0 ? '✓ ready' : '— empty';
}
}
function refreshPlaceholders() {
syncHL();
const hits = detectPlaceholders(textarea.value);
const titleEl = phWarn.querySelector('.dd-ph-warn-title');
const howtoEl = phWarn.querySelector('.dd-ph-howto');
const listEl = phWarn.querySelector('.dd-ph-list');
while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
refreshFooter(hits.length);
if (!hits.length) {
phWarn.classList.remove('show');
return;
}
titleEl.textContent = _('⚠ Still needs a subscription link: %d placeholder URL line(s) detected').format(hits.length);
while (howtoEl.firstChild) howtoEl.removeChild(howtoEl.firstChild);
/* assemble rich text from DOM nodes so the code spans keep their styling */
howtoEl.appendChild(document.createTextNode(_('These ')));
const c1 = document.createElement('code'); c1.textContent = 'example.com'; howtoEl.appendChild(c1);
howtoEl.appendChild(document.createTextNode(' / '));
const c2 = document.createElement('code'); c2.textContent = 'relative/path/to'; howtoEl.appendChild(c2);
howtoEl.appendChild(document.createTextNode(_(' are placeholder addresses from dae\'s template, not real subscriptions. Replace the URL on any one line with your airport subscription link (delete the spare lines) and save.')));
hits.forEach(function(h) {
const lnSpan = E('span', { 'class': 'dd-ph-ln', 'title': _('Jump to line') }, 'L' + h.line);
lnSpan.addEventListener('click', function() { jumpToLine(h.line); });
const txt = E('span', { 'class': 'dd-ph-txt' }, h.url);
listEl.appendChild(E('li', {}, [ lnSpan, txt ]));
});
phWarn.classList.add('show');
}
textarea.addEventListener('input', refreshPlaceholders);
function loadConfig() {
/* stat first (ubus, no HTTP): a fresh install has no config.dae yet, and
reading a missing file via fs.read_direct logs a noisy 404 */
return fs.stat(backend.BACKENDS.dae.config).then(function() {
return fs.read_direct(backend.BACKENDS.dae.config, 'text');
}).then(function(content) {
textarea.value = content || '';
}).catch(function() {
textarea.value = '';
}).finally(refreshPlaceholders);
}
/* 校验流程:先写到 /tmp/dae-validate.dae 干测 → dae validate 通过才覆盖正式 config */
const VALIDATE_PATH = '/tmp/dae-validate.dae';
save.addEventListener('click', function(ev) {
ev.preventDefault();
save.disabled = true;
flashStatus(_('Validating…'));
fs.write(VALIDATE_PATH, textarea.value, 384)
.then(function() { return fs.exec('/usr/bin/env', ['DAE_LOCATION_ASSET=/usr/share/v2ray', '/usr/bin/dae', 'validate', '-c', VALIDATE_PATH]); })
.then(function(res) {
if (res && res.code !== 0) {
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
flashStatus(_('Validate failed: %s').format(err), 'err', 8000);
throw new Error('validate-failed');
}
flashStatus(_('Saving…'));
return fs.write(backend.BACKENDS.dae.config, textarea.value, 384);
})
.then(function() { return backend.detectRunning(); })
.then(function(running) {
if (running && running.dae)
return fs.exec(backend.BACKENDS.dae.initd, ['hot_reload']).then(function(res) {
if (res && res.code !== 0)
flashStatus(_('Reload failed: %s').format(res.stderr || res.stdout || ('exit ' + res.code)), 'err', 8000);
else
flashStatus(_('Validated · saved · hot-reloaded'), 'ok');
});
flashStatus(_('Saved OK (start dae to apply)'), 'ok');
})
.catch(function(e) {
if (e && e.message === 'validate-failed') return;
flashStatus(_('Save failed: %s').format(e.message || e), 'err', 8000);
})
.finally(function() {
save.disabled = false;
fs.exec('/bin/rm', ['-f', VALIDATE_PATH]).catch(function() {});
});
});
init.addEventListener('click', function(ev) {
ev.preventDefault();
if (textarea.value.trim() && !confirm(_('This will replace your current config. Continue?')))
return;
textarea.value = DEFAULT_TEMPLATE;
refreshPlaceholders();
flashStatus(_('Default config loaded — replace the subscription link, then save'), 'ok');
});
/* 一次性探测 dae --version;失败就显示 unknown,不影响主流程 */
fs.exec('/usr/bin/dae', ['--version']).then(function(res) {
const out = (res && res.stdout || '').trim();
const m = out.match(/dae\s+version\s+(\S+)/);
fbVer.lastChild.textContent = m ? m[1].split('-')[0] : 'unknown';
}).catch(function() {
fbVer.lastChild.textContent = 'unknown';
});
loadConfig();
return E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Advanced / Manual Mode')),
(function() {
var adv = E('div', { 'class': 'dd-adv dd-closed', style: 'margin-top:8px' }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, _('Configuration Editor')),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
E('div', { 'class': 'dd-adv-body' }, [
E('div', { 'class': 'dd-editor-hint', style: 'margin:0 0 10px' }, [
E('b', {}, _('Text-only mode.')),
' ',
_('Edit the dae DSL directly. "Load default config" gives a ready-to-run starting point. Use daed for a GUI.')
]),
phWarn,
editWrap,
footer,
E('div', { 'class': 'dd-editor-actions' }, [ save, init, status ])
])
]);
adv.firstChild.addEventListener('click', function() { adv.classList.toggle('dd-closed'); });
return adv;
})()
]);
}
return baseclass.extend({
renderDaeForms: renderDaeForms,
renderDaeImportBanner: renderDaeImportBanner,
renderDaeEditor: renderDaeEditor
});
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
const TOKEN_KEY = 'luci-app-daede.daed-token';
function clear(storage) {
try { storage.removeItem(TOKEN_KEY); } catch (e) {}
}
function tokenExpiry(token) {
try {
const part = String(token || '').split('.')[1];
if (!part)
return 0;
const normalized = part.replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(atob(normalized));
return Number(payload.exp) || 0;
} catch (e) {
return 0;
}
}
function load(storage, now) {
let token = '';
try { token = storage.getItem(TOKEN_KEY) || ''; } catch (e) { return ''; }
const current = now == null ? Date.now() / 1000 : Number(now);
if (!token || tokenExpiry(token) <= current + 30) {
clear(storage);
return '';
}
return token;
}
function save(storage, token) {
try { storage.setItem(TOKEN_KEY, String(token || '')); } catch (e) {}
}
function isAccessDenied(error) {
return /access denied|unauthori[sz]ed|invalid token/i.test(String(error && error.message || error || ''));
}
const api = {
TOKEN_KEY: TOKEN_KEY,
load: load,
save: save,
clear: clear,
isAccessDenied: isAccessDenied
};
if (typeof module !== 'undefined' && module.exports)
module.exports = api;
if (typeof baseclass !== 'undefined')
return baseclass.extend(api);
return api;
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
'require form';
'require fs';
'require ui';
'require uci';
'require view.daede.widgets as widgets';
function applyUciChanges() {
return uci.save().then(function() {
return uci.changes().then(function(ch) {
return (ch && Object.keys(ch).length) ? uci.apply() : null;
});
}).then(function() {
return new Promise(function(resolve) { window.setTimeout(resolve, 1800); });
}).then(function() {
return ui.changes.init();
});
}
function renderDaedSettings() {
let m, s, o;
m = new form.Map('daed', null, null);
s = m.section(form.NamedSection, 'config', 'daed');
s.addremove = false;
s.anonymous = true;
o = s.option(form.Value, 'listen_addr', _('Listen Address'));
o.datatype = 'ipaddrport(1)';
o.default = '0.0.0.0:2023';
o.rmempty = false;
o = s.option(form.Value, 'dashboard_username', _('daed Username'),
_('Used by LuCI to access daed (subscription updates and import). If daed is not initialized yet, this account is used to create the daed admin.'));
o.placeholder = 'admin';
o.rmempty = false;
o = s.option(form.Value, 'dashboard_password', _('daed Password'));
o.password = true;
o = s.option(form.Flag, 'subscribe_auto_update', _('Enable subscription auto-update'));
o.default = '0';
o = s.option(form.ListValue, 'subscribe_update_cycle', _('Update Cycle'));
o.value('daily', _('Daily'));
o.value('weekly', _('Weekly'));
o.default = 'daily';
o.depends('subscribe_auto_update', '1');
o = s.option(form.ListValue, 'subscribe_update_hour', _('Update Time'));
for (let h = 0; h < 24; h++) {
const hh = ('0' + h).slice(-2);
o.value(String(h), hh + ':00');
}
o.default = '4';
o.depends('subscribe_auto_update', '1');
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
_('Rotate the log file once it grows past this many megabytes.'));
o.datatype = 'uinteger';
o.default = '5';
o = s.option(form.Value, 'log_maxbackups', _('Max Log Backups'),
_('Number of rotated log files to keep.'));
o.datatype = 'uinteger';
o.default = '1';
return widgets.wrapSettingsCard(
_('daede Settings'),
null,
m.render(),
_('Log Settings'),
['log_maxsize', 'log_maxbackups']
).then(function(card) {
/* native Save/Apply footer is suppressed view-wide, so daed carries its
own primary action */
const status = E('span', { 'class': 'dd-editor-status' }, '');
let statusTimer = null;
function flash(text, kind, hold) {
status.textContent = text;
status.classList.remove('ok', 'err');
if (kind) status.classList.add(kind);
status.classList.add('show');
if (statusTimer) clearTimeout(statusTimer);
statusTimer = setTimeout(function() { status.classList.remove('show'); }, hold || 3000);
}
const save = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply'));
const update = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Update subscriptions now'));
update.addEventListener('click', function(ev) {
ev.preventDefault();
update.disabled = true;
flash(_('Updating daed subscriptions…'));
fs.exec('/usr/share/luci-app-daede/daed-sub-update.sh', []).then(function(res) {
if (res && res.code !== 0) {
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
flash(_('Subscription update failed: %s').format(err), 'err', 9000);
} else {
flash(_('Subscriptions updated and applied'), 'ok');
}
}).catch(function(e) {
flash(_('Subscription update failed: %s').format(e.message || e), 'err', 9000);
}).finally(function() { update.disabled = false; });
});
save.addEventListener('click', function(ev) {
ev.preventDefault();
save.disabled = true;
m.save(null, true)
.then(applyUciChanges)
.then(function() {
const enabled = uci.get('daed', 'config', 'subscribe_auto_update') === '1';
return fs.exec('/usr/share/luci-app-daede/daed-sub-cron.sh', [ enabled ? 'enable' : 'disable' ]);
})
.then(function(res) {
if (res && res.code !== 0)
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
})
.then(function() {
/* no success popup — the reload is feedback enough */
setTimeout(function() { window.location.reload(); }, 500);
})
.catch(function(e) {
if (e && e.name === 'CBIValidationError')
flash(_('Please fix the highlighted fields.'), 'err', 6000);
else
flash(_('Save failed: %s').format(e.message || e), 'err', 9000);
})
.finally(function() { save.disabled = false; });
});
card.appendChild(E('div', { 'class': 'dd-editor-actions' }, [ update, save, status ]));
return card;
});
}
return baseclass.extend({ renderDaedSettings: renderDaedSettings });
@@ -0,0 +1,414 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require fs';
'require poll';
'require ui';
'require view';
'require view.daede.backend as backend';
'require view.daede.styles as styles';
const MAX_LINES = 5000;
function execChecked(command, args) {
return fs.exec(command, args || []).then(function(res) {
if (res && res.code !== 0)
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
return res;
});
}
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}',
/* Argon shifts label>checkbox down by top:.4rem; reset so flex centers it with the text */
'.dd-log-toolbar input[type="checkbox"]{position:static;top:auto;right:auto;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;line-height:1.4;min-height:0;height:auto;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-stop{color:#d9534f;border-color:rgba(217,83,79,.45)}',
'.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-trace{color:#68717d;opacity:.62}',
'.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;white-space:nowrap}',
'.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-lvl-trace{color:#68717d;background:rgba(104,113,125,.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*(.*)$/;
const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i;
const RE_PLAIN_LEVEL_LINE = /^\s*(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i;
function detectLevel(line) {
// daed/dae logs use lvl=info / [INFO] / level=warning style
const m = line.match(/\b(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i);
if (!m) return '';
const lvl = m[1].toUpperCase();
if (lvl === 'TRACE') return 'dd-trace';
if (lvl === 'DEBUG') return 'dd-debug';
if (lvl === 'INFO') return 'dd-info';
if (lvl.startsWith('WARN')) return 'dd-warn';
return 'dd-error';
}
function matchesFilter(line, filter) {
if (!filter)
return true;
if (filter === 'trace' || filter === 'debug' || filter === 'info' || filter === 'warn' || filter === 'error')
return detectLevel(line) === 'dd-' + filter;
if (filter === 'node_status')
return /\balive\b/i.test(line)
|| /\bgroup\b.*\bselects dialer\b/i.test(line)
|| /\bgroup\s+'[^']+'\s+\[[^\]]+\]:/i.test(line);
if (filter === 'proxy_traffic')
return line.indexOf('<->') !== -1;
return false;
}
/* 格式化时间戳并按北京时间显示:
* - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成北京时间
* - 新格式和旧 logrus 短格式无时区信息,保留原始完整日期时间
*/
function formatTs(raw) {
if (!raw) return raw;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.test(raw)) {
const d = new Date(raw);
if (!isNaN(d.getTime())) {
const beijing = new Date(d.getTime() + 8 * 60 * 60 * 1000);
const pad = n => String(n).padStart(2, '0');
return [
beijing.getUTCFullYear(),
pad(beijing.getUTCMonth() + 1),
pad(beijing.getUTCDate())
].join('-') + ' ' + [
pad(beijing.getUTCHours()),
pad(beijing.getUTCMinutes()),
pad(beijing.getUTCSeconds())
].join(':');
}
}
return raw;
}
function lvlShort(level) {
const u = (level || '').toUpperCase();
if (u === 'WARNING') return 'WARN';
return u || '-';
}
function lvlClass(level) {
const u = (level || '').toUpperCase();
if (u === 'TRACE') return 'dd-lvl-trace';
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 parseLine(line) {
let m = line.match(RE_LINE);
if (m) {
return {
ts: formatTs(m[1]),
lvl: m[2],
msg: m[3] !== undefined ? m[3] : (m[4] || ''),
kv: (m[5] || '').trim()
};
}
m = line.match(RE_PREFIXED_LINE);
let ts = '', lvl = '', body = '';
if (m) {
ts = formatTs(m[1]);
lvl = m[2];
body = m[3] || '';
} else {
m = line.match(RE_PLAIN_LEVEL_LINE);
if (!m) return null;
lvl = m[1];
body = m[2] || '';
}
const kvStart = body.search(/(?:^|\s)(?=[A-Za-z_][\w.-]*=)/);
return {
ts: ts,
lvl: lvl,
msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(),
kv: kvStart === -1 ? '' : body.slice(kvStart).trim()
};
}
function buildLine(ln) {
const cls = detectLevel(ln);
const parsed = parseLine(ln);
if (!parsed) {
return E('div', { 'class': 'dd-line ' + cls }, ln);
}
const parts = [];
if (parsed.ts) parts.push(E('span', { 'class': 'dd-ts' }, parsed.ts));
parts.push(E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
parts.push(E('span', { 'class': 'dd-msg' }, parsed.msg));
if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.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,
reading: 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 btnAuto = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-auto' });
const btnPause = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-pause' });
const syncControls = function() {
btnAuto.textContent = state.autoScroll ? '✓ ' + _('Auto-scroll: On') : '○ ' + _('Auto-scroll: Off');
btnAuto.setAttribute('aria-pressed', String(state.autoScroll));
btnPause.textContent = state.paused ? '▶ ' + _('Resume') : 'Ⅱ ' + _('Pause');
btnPause.setAttribute('aria-pressed', String(state.paused));
meta.textContent = state.paused ? _('Paused') : '';
};
btnAuto.addEventListener('click', function() {
state.autoScroll = !state.autoScroll;
if (state.autoScroll) {
pane.scrollTop = pane.scrollHeight;
state.userScrolled = false;
}
syncControls();
});
btnPause.addEventListener('click', function() {
state.paused = !state.paused;
syncControls();
if (!state.paused) tick();
});
syncControls();
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
E('option', { 'value': '' }, _('All')),
E('option', { 'value': 'trace' }, 'TRACE'),
E('option', { 'value': 'debug' }, 'DEBUG'),
E('option', { 'value': 'info' }, 'INFO'),
E('option', { 'value': 'warn' }, 'WARN'),
E('option', { 'value': 'error' }, 'ERROR'),
E('option', { 'value': 'node_status' }, _('Node Status')),
E('option', { 'value': 'proxy_traffic' }, _('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() {});
});
let btnStop = null;
if (ctx.name === 'daed') {
btnStop = E('button', { 'class': 'dd-log-btn dd-log-stop' }, _('Stop daed'));
btnStop.addEventListener('click', function() {
if (!confirm(_('Stop daed now?')))
return;
btnStop.disabled = true;
execChecked('/sbin/uci', ['set', 'daed.config.enabled=0'])
.then(function() { return execChecked('/sbin/uci', ['commit', 'daed']); })
.then(function() { return execChecked('/etc/init.d/daed', ['disable']); })
.then(function() { return execChecked('/etc/init.d/daed', ['stop']); })
.then(function() { btnStop.textContent = _('daed stopped'); })
.catch(function(e) {
btnStop.disabled = false;
ui.addNotification(null, E('p', {}, _('Failed to stop daed: %s').format(e.message || e)), 'error');
});
});
}
function applyFilter() {
const f = state.filter;
pane.querySelectorAll('.dd-line').forEach(function(el) {
if (matchesFilter(el._rawLine, f))
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);
el._rawLine = ln;
if (!matchesFilter(ln, state.filter))
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 || state.reading) return Promise.resolve();
state.reading = true;
return fs.stat(LOG_PATH).then(function(st) {
if (state.paused) return;
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) {
if (state.paused) return;
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) {
if (state.paused) return;
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 = '';
}).finally(function() { state.reading = false; });
}
poll.add(tick);
tick();
const toolbarItems = [
btnAuto,
btnPause,
selFilter,
btnClear,
btnDownload,
btnTruncate
];
if (btnStop) toolbarItems.push(btnStop);
toolbarItems.push(meta);
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems);
return E('div', { 'class': 'dd-log-wrap' }, [
E('style', {}, CSS + styles.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,269 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
const CSS = [
'.dd-log-toolbar .dd-log-toggle[aria-pressed="true"]{background:rgba(74,160,101,.16);border-color:#4aa065;box-shadow:inset 0 0 0 1px rgba(74,160,101,.2)}',
'.dd-log-toolbar .dd-log-toggle:focus-visible{outline:2px solid #4aa065;outline-offset:2px}',
'.dd-wrap{padding:4px 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:9px 14px;margin-bottom:7px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
/* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise
indents the title 20px past the card body and looks misaligned */
'.dd-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;padding:0;letter-spacing:.3px;text-transform:uppercase}',
'.dd-status-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:0}',
'.dd-status-row .dd-grow{flex:1 1 auto}',
'.dd-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;font-size:10.5px;font-weight:700;letter-spacing:.3px;border:1px solid transparent;line-height:1.3}',
'.dd-badge-run{color:#3da66a;border-color:rgba(61,166,106,.5)}',
'.dd-badge-stop{color:#d96d6d;border-color:rgba(217,109,109,.55)}',
'.dd-badge-dot{width:6px;height:6px;border-radius:50%;background:currentColor;display:inline-block}',
'.dd-meta{font-size:11.5px;opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}',
'.dd-meta-label{opacity:.55;margin-right:4px}',
'.dd-actions{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 0}',
'.dd-actions .cbi-button{font-size:11.5px;padding:4px 12px;border-radius:5px}',
'.dd-actions a.cbi-button{display:inline-flex;align-items:center;gap:4px}',
'.dd-wrap .cbi-button{font-size:11px !important;line-height:1.4 !important;min-height:0 !important;height:auto !important;padding:4px 12px !important;border-radius:5px !important;border:1px solid rgba(128,128,128,.35) !important;background:transparent !important;color:inherit !important;box-shadow:none !important;cursor:pointer;white-space:nowrap}',
'.dd-wrap .cbi-button:hover{background:rgba(128,128,128,.12) !important}',
'.dd-wrap .cbi-button:disabled{opacity:.45;cursor:not-allowed}',
'.dd-wrap .cbi-button-action,.dd-wrap .cbi-button-positive,.dd-wrap .cbi-button-add,.dd-wrap .cbi-button-edit{border-color:#4aa065 !important;color:#4aa065 !important}',
'.dd-wrap .cbi-button-remove{border-color:#d96d6d !important;color:#d96d6d !important}',
'.dd-wrap .cbi-button-remove:hover{background:rgba(217,109,109,.12) !important}',
'.dd-switch{position:relative;width:42px;height:22px;border:0;border-radius:999px;background:rgba(128,128,128,.28);padding:0;cursor:pointer;transition:background .18s ease,opacity .18s ease;flex-shrink:0}',
'.dd-switch .dd-switch-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:rgba(255,255,255,.96);box-shadow:0 1px 4px rgba(0,0,0,.2);transition:transform .18s ease}',
'.dd-switch.is-on{background:rgba(74,160,101,.65)}',
'.dd-switch.is-on .dd-switch-knob{transform:translateX(20px)}',
'.dd-switch:disabled{opacity:.45;cursor:not-allowed}',
'.dd-switch-label{font-size:10.5px;font-weight:600;opacity:.62;letter-spacing:.3px}',
'.dd-switch-wrap{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}',
'.dd-backend-card{padding:10px 14px}',
'.dd-backend-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
'.dd-backend-label{flex:0 0 calc(200px - 10px);font-size:12px;font-weight:600;opacity:.72}',
'.dd-backend-segment{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:7px;background:rgba(128,128,128,.10)}',
'.dd-backend-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:78px;height:24px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:inherit;font-size:11px;font-weight:500;opacity:.65;cursor:pointer;transition:background .18s ease,color .18s ease,opacity .18s ease}',
'.dd-backend-btn:hover{background:rgba(128,128,128,.10)}',
'.dd-backend-btn.is-active{background:linear-gradient(#3886a1,#2f7288);color:#fff;opacity:1;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.12)}',
'.dd-backend-btn:disabled{opacity:.55;cursor:not-allowed}',
'.dd-backend-state{font-size:10.5px;font-weight:500;opacity:.70;margin-left:2px}',
'.dd-backend-btn.is-active .dd-backend-state{opacity:.85}',
'.dd-backend-help{margin:6px 0 0 200px;font-size:11.5px;line-height:1.45;opacity:.66}',
'.dd-backend-help:empty{display:none}',
'html[data-daede-theme="argon"] .dd-backend-label{flex-basis:calc(240px - 10px)}',
'html[data-daede-theme="argon"] .dd-backend-help{margin-left:240px}',
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card{background:rgba(255,255,255,.78);border-color:rgba(50,50,93,.12);box-shadow:0 2px 8px rgba(50,50,93,.06)}',
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card-title{opacity:.78}',
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-title{opacity:1}',
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field input,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field select,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field textarea{background:#fff!important;border-color:rgba(50,50,93,.24)!important}',
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-adv-bar{background:rgba(50,50,93,.04);border-color:rgba(50,50,93,.14);opacity:.78}',
'@media (max-width:640px){.dd-backend-label{flex-basis:100%!important;width:100%}.dd-backend-segment{width:100%}.dd-backend-btn{flex:1;min-width:0}.dd-backend-help{margin-left:0!important}}',
/* daed/dae settings card —— LuCI form.Map 字体/边框对齐 dd 卡片体系 */
'.dd-settings-card{padding:10px 14px}',
'.dd-settings-card>h2,.dd-settings-card .cbi-map>h2,.dd-settings-card .cbi-section>h3{display:none}',
'.dd-settings-card .cbi-map>.cbi-map-descr,.dd-settings-card .cbi-section-descr{font-size:11.5px;opacity:.62;margin:0 0 8px;line-height:1.45}',
'.dd-settings-descr{font-size:11.5px;opacity:.62;margin:0 0 8px;line-height:1.45}',
'.dd-settings-card .cbi-section{margin:0;padding:0;background:transparent;border:0;box-shadow:none}',
'.dd-settings-card .cbi-value{padding:6px 0;border:0;min-height:0}',
/* Argon's .td.cbi-value-field padding + 40px .cbi-checkbox make table rows
too tall — tighten both for our compact subscription/node tables */
'.dd-settings-card .cbi-section-table-row>td{padding:6px 10px !important}',
'.dd-settings-card .cbi-section-table-row .cbi-checkbox{height:20px !important;min-height:0 !important}',
'.dd-settings-card .cbi-value-title{font-size:12.5px !important;font-weight:500;opacity:.85;padding:6px 12px 6px 0;min-width:140px}',
'.dd-settings-card .cbi-value-field input,.dd-settings-card .cbi-value-field select,.dd-settings-card .cbi-value-field textarea{font-size:12.5px !important;padding:5px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit}',
'.dd-settings-card .cbi-value-field input:focus,.dd-settings-card .cbi-value-field select:focus,.dd-settings-card .cbi-value-field textarea:focus{border-color:rgba(56,134,161,.7);outline:0;box-shadow:0 0 0 2px rgba(56,134,161,.15)}',
'.dd-settings-card .cbi-value-description,.dd-settings-card .cbi-value-helptext{font-size:11.5px !important;opacity:.6;line-height:1.45;padding-top:3px}',
'.dd-settings-card .cbi-section-remove{background:transparent !important;border:0 !important;box-shadow:none !important}',
'.dd-settings-card .cbi-section-remove .cbi-button{border-color:#d96d6d !important;color:#d96d6d !important}',
/* themes (esp. Argon) restyle checkboxes into faint, near-invisible boxes;
force native rendering + brand accent so the checkmark is always clear */
'.dd-settings-card input[type="checkbox"]{-webkit-appearance:checkbox !important;appearance:checkbox !important;width:16px;height:16px;background:none !important;border:0 !important;box-shadow:none !important;background-image:none !important}',
'.dd-settings-card input[type="checkbox"]:before,.dd-settings-card input[type="checkbox"]:after{content:none !important;display:none !important}',
/* line the Subscriptions and Nodes grid tables up column-for-column. fixed
layout + explicit per-column percentages make the widths authoritative
across themes (Argon's auto layout otherwise sizes the empty Nodes table
by content, and even ignores px cell widths, so they drift apart) */
'.dd-settings-card .cbi-section-table{table-layout:fixed !important;width:100%}',
/* flatten the table header: Argon tints header cells (#f6f9fc), which reads
as an off-colour band on the white card — keep it transparent */
'.dd-settings-card .cbi-section-table-titles,.dd-settings-card .cbi-section-table-titles .cbi-section-table-cell{background:transparent !important;background-image:none !important}',
'.dd-settings-card .cbi-section-table-cell:nth-child(1){width:14% !important}',
'.dd-settings-card .cbi-section-table-cell:nth-child(2){width:62% !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.dd-settings-card .cbi-section-table-cell:nth-child(3){width:10% !important}',
'.dd-settings-card .cbi-section-table-cell:nth-child(4){width:auto !important;min-width:0 !important;white-space:nowrap;text-align:right}',
/* icon row-action buttons stay narrow, never stretch to fill the cell */
'.dd-settings-card .cbi-section-actions .cbi-button-edit,.dd-settings-card .cbi-section-actions .cbi-button-remove{width:auto !important;min-width:0 !important;display:inline-block !important;flex:0 0 auto !important}',
/* proxy-test result coloring */
'.dd-meta.dd-ok{color:#3da66a;font-weight:600}',
'.dd-meta.dd-err{color:#d96d6d;font-weight:600}',
/* 手风琴折叠组 —— 视觉与 clashoo cl-component-adv 一致 */
'.dd-adv{margin-top:10px}',
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;cursor:pointer;user-select:none;font-size:12px;font-weight:600;background:rgba(128,128,128,.06);border:1px solid rgba(128,128,128,.16);border-radius:7px;color:inherit;opacity:.55}',
'.dd-adv-bar:hover{background:rgba(56,134,161,.08);opacity:1}',
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s}',
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
'.dd-adv-body{margin-top:8px;padding:2px 4px 4px}',
'.dd-adv.dd-closed .dd-adv-body{display:none}',
/* display:block kills the ~5px inline-block baseline gap that made the
overlay pre (inset:0) ~5px taller than the textarea — the two layers'
scrollbars and glyphs would otherwise drift apart on mobile */
'.dd-editor{display:block;width:100%;min-height:460px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:12px;line-height:1.5;box-sizing:border-box;resize:vertical;border-radius:6px 6px 0 0}',
/* syntax-highlight overlay: a <pre> sits behind a transparent <textarea>.
both share identical box metrics so glyphs line up; the textarea stays the
real editor (caret, selection, insert-at-cursor, jump-to-line all native) */
'.dd-edit-wrap{position:relative}',
'.dd-edit-wrap .dd-editor,.dd-edit-wrap .dd-hl{margin:0;padding:10px 12px;border-width:1px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:12px;line-height:1.5;letter-spacing:0;tab-size:4;white-space:pre;word-break:normal;overflow:auto;box-sizing:border-box}',
/* resize:vertical renders the same corner grip as the textarea (pointer-events
keeps it inert), so both layers' scrollbar tracks stay the same length on
mobile — the textarea's resize grip otherwise shortens its track and the
thumbs drift, ghosting the highlight underneath */
'.dd-edit-wrap .dd-hl{position:absolute;inset:0;margin:0;resize:vertical;border:1px solid transparent;border-radius:6px 6px 0 0;pointer-events:none;background:#f6f8fa;color:#3b4252;z-index:1}',
/* reset Argon's code{background:var(--lighter)} so the overlay inherits the
pre background uniformly — otherwise a dark pre shows as side bars.
color:inherit so plain text follows .dd-hl (light in dark mode) instead of
the theme's own code{color}, which is dark and vanishes on a dark pre */
'.dd-edit-wrap .dd-hl code{font:inherit;white-space:inherit;word-break:inherit;display:block;color:inherit !important;background:transparent !important;padding: 0px}',
'.dd-edit-wrap .dd-editor-hl{position:relative;z-index:2;color:transparent !important;background:transparent !important;caret-color:#2f7288;border:1px solid rgba(128,128,128,.28)}',
'.dd-edit-wrap .dd-editor-hl::placeholder{color:rgba(128,128,128,.55)}',
/* keep selected text transparent too — else the browser force-colors it and
it ghosts over the highlight pre underneath */
'.dd-edit-wrap .dd-editor-hl::selection{background:rgba(56,134,161,.25);color:transparent}',
'@media screen and (max-device-width:600px){.dd-edit-wrap .dd-editor,.dd-edit-wrap .dd-hl,.dd-edit-wrap .dd-hl code{font-size:16px}}',
'.dh-c{color:#8a919a}',
'.dh-s{color:#2a8a4a}',
'.dh-k{color:#9a3fb5}',
'.dh-f{color:#2f7288}',
'.dh-o{color:#c2772a}',
'.dh-g{color:#b0226a}',
'html[data-darkmode="true"] .dd-edit-wrap .dd-hl,body.dark .dd-edit-wrap .dd-hl,html[data-bs-theme="dark"] .dd-edit-wrap .dd-hl{color:#c8cdd6;background:#1e2228}',
'html[data-darkmode="true"] .dd-edit-wrap .dd-editor-hl,body.dark .dd-edit-wrap .dd-editor-hl,html[data-bs-theme="dark"] .dd-edit-wrap .dd-editor-hl{caret-color:#7fd0e8}',
'html[data-darkmode="true"] .dh-c,body.dark .dh-c{color:#6b7280}',
'html[data-darkmode="true"] .dh-s,body.dark .dh-s{color:#7ec699}',
'html[data-darkmode="true"] .dh-k,body.dark .dh-k{color:#c792ea}',
'html[data-darkmode="true"] .dh-f,body.dark .dh-f{color:#82c4dd}',
'html[data-darkmode="true"] .dh-o,body.dark .dh-o{color:#e0a35a}',
'html[data-darkmode="true"] .dh-g,body.dark .dh-g{color:#ec6fa8}',
'.dd-editor-footer{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding:5px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10.5px;background:rgba(128,128,128,.06);border:1px solid rgba(0,0,0,.06);border-top:0;border-radius:0 0 6px 6px;color:inherit;opacity:.78}',
'.dd-editor-footer .dd-fb-item{display:inline-flex;align-items:center;gap:5px}',
'.dd-editor-footer .dd-fb-key{opacity:.55}',
'.dd-editor-footer .dd-fb-warn{color:#b07d00;font-weight:600}',
'.dd-editor-footer .dd-fb-ok{color:#3da66a;font-weight:600}',
'body.dark .dd-editor-footer,html[data-theme="dark"] .dd-editor-footer,html[data-bs-theme="dark"] .dd-editor-footer,html[data-darkmode="true"] .dd-editor-footer{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.04)}',
'body.dark .dd-editor-footer .dd-fb-warn,html[data-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-bs-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-darkmode="true"] .dd-editor-footer .dd-fb-warn{color:#e0b34a}',
/* match the action buttons' size (h25 / 11px / 5px radius) for a consistent row */
'.dd-insert-select{font-size:11px;line-height:1.4;height:25px;padding:0 10px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;box-sizing:border-box}',
'.dd-insert-select:hover{border-color:rgba(56,134,161,.55)}',
'.dd-editor-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}',
'.dd-editor-status{margin-left:auto;font-size:11.5px;opacity:0;transition:opacity .25s ease;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
'.dd-editor-status.show{opacity:1}',
'.dd-editor-status.ok{color:#3da66a}',
'.dd-editor-status.err{color:#d96d6d}',
'.dd-editor-hint{font-size:11.5px;opacity:.62;line-height:1.5;margin:0 0 8px;padding:7px 10px;border-radius:6px;background:rgba(56,134,161,.06)}',
'.dd-editor-hint b{font-weight:600;opacity:.85}',
'.dd-sub-card{margin:0 0 10px;padding:10px 14px;border:0;border-radius:8px;background:transparent}',
'.dd-sub-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
'.dd-sub-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
'.dd-sub-row input{flex:1;min-width:200px;font-size:12px;padding:6px 10px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:var(--text-color,inherit)}',
'.dd-sub-row input:focus{border-color:rgba(74,160,101,.55);outline:0;box-shadow:0 0 0 2px rgba(74,160,101,.12)}.dd-sub-row input::placeholder{opacity:.45;color:rgba(92,102,120,.8)}',
'.dd-sub-apply{font-size:11px;padding:5px 14px;border-radius:5px;border:1px solid #4aa065;color:#4aa065;background:transparent;cursor:pointer;white-space:nowrap;font-weight:600}',
'.dd-sub-apply:hover{background:rgba(74,160,101,.12)}',
'.dd-sub-apply:disabled{opacity:.45;cursor:not-allowed}',
'.dd-sub-status{font-size:11px;margin-left:8px;opacity:0;transition:opacity .2s;white-space:nowrap}',
'.dd-sub-status.show{opacity:1}',
'.dd-ph-warn{display:none;font-size:12px;line-height:1.6;margin:0 0 8px;padding:10px 12px;border-radius:6px;background:rgba(217,158,0,.08);color:inherit}',
'.dd-ph-warn.show{display:block}',
'.dd-ph-warn-title{font-weight:600;margin-bottom:6px;color:#b07d00;font-size:12.5px}',
'body.dark .dd-ph-warn-title,html[data-theme="dark"] .dd-ph-warn-title,html[data-bs-theme="dark"] .dd-ph-warn-title,html[data-darkmode="true"] .dd-ph-warn-title{color:#e0b34a}',
'.dd-ph-howto{margin:4px 0 8px;opacity:.85}',
'.dd-ph-howto code{padding:1px 5px;border-radius:3px;background:rgba(128,128,128,.18);font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
'.dd-ph-list{margin:6px 0 0;padding:6px 0 0;list-style:none;border-top:1px dashed rgba(217,158,0,.3)}',
'.dd-ph-list-label{font-size:11px;opacity:.6;margin:0 0 4px}',
'.dd-ph-list li{padding:2px 0;display:flex;gap:8px;align-items:baseline;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}',
'.dd-ph-list .dd-ph-ln{display:inline-block;min-width:38px;text-align:center;padding:1px 4px;border-radius:3px;background:rgba(217,158,0,.18);color:#8a6300;opacity:.95;cursor:pointer;font-weight:600;text-decoration:none}',
'.dd-ph-list .dd-ph-ln:hover{background:rgba(217,158,0,.32)}',
'body.dark .dd-ph-list .dd-ph-ln,html[data-theme="dark"] .dd-ph-list .dd-ph-ln,html[data-bs-theme="dark"] .dd-ph-list .dd-ph-ln,html[data-darkmode="true"] .dd-ph-list .dd-ph-ln{color:#f0c763;background:rgba(217,158,0,.22)}',
'.dd-ph-list .dd-ph-txt{opacity:.7;word-break:break-all}',
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,html[data-darkmode="true"] .dd-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
'body.dark .dd-adv-bar,html[data-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-bar,html[data-darkmode="true"] .dd-adv-bar{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.10)}',
'body.dark .dd-settings-card .cbi-value-field input,body.dark .dd-settings-card .cbi-value-field select,body.dark .dd-settings-card .cbi-value-field textarea,html[data-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-darkmode="true"] .dd-settings-card .cbi-value-field input,html[data-darkmode="true"] .dd-settings-card .cbi-value-field select,html[data-darkmode="true"] .dd-settings-card .cbi-value-field textarea{border-color:rgba(255,255,255,.18)}',
/* No prefers-color-scheme dark block: OS dark mode must NOT force a dark
editor when the user picked a LIGHT theme. Dark styling is driven solely
by data-darkmode (config.js reads the real page background). */
/* status card line-2 meta (backend · pid), below the badge+toggle line */
'.dd-status-meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}',
/* compact icon-only row actions — the 编辑/删除 text buttons are too wide
(esp. Bootstrap); show a pencil / trash glyph instead, both themes */
'.dd-settings-card .cbi-section-actions .cbi-button-edit,.dd-settings-card .cbi-section-actions .cbi-button-remove{font-size:0 !important;padding:4px 7px !important;min-width:0 !important}',
'.dd-settings-card .cbi-section-actions .cbi-button-edit::before{content:"\\270E";font-size:13px;line-height:1}',
'.dd-settings-card .cbi-section-actions .cbi-button-remove::before{content:"\\1F5D1\\FE0E";font-size:13px;line-height:1}',
/* phone: keep each subscription/node row on ONE line; the URL cell
middle-ellipsizes (via ellipsisCell) and flex-shrinks to fit the screen */
'@media (max-width:600px){',
'.dd-settings-card .cbi-section-table-titles{display:none !important}',
/* drop table layout so flex rows are constrained to the card width and the
URL cell can actually shrink (a flex <tr> inside table-layout:fixed grows
past the table and overflows the screen on narrow/Argon) */
'.dd-settings-card .cbi-section-table{display:block !important;table-layout:auto !important;width:auto !important;overflow:visible !important}',
'.dd-settings-card .cbi-section-table>tbody,.dd-settings-card .cbi-section-table>div{display:block !important;width:auto !important}',
'.dd-settings-card .cbi-section-table-row{display:flex !important;flex-wrap:nowrap !important;width:auto !important;align-items:center;gap:6px;padding:5px 0;margin:0;border:0;border-bottom:1px solid rgba(128,128,128,.15);background:transparent}',
/* target the real cells (td.cbi-value-field / actions) — LuCI forces them
block+full-width at mobile, so override on the direct children */
'.dd-settings-card .cbi-section-table-row>td{display:inline-block !important;align-self:center !important;width:auto !important;padding:0 !important;border:0 !important;background:transparent !important;box-shadow:none !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0 !important}',
'.dd-settings-card .cbi-section-table-row>td::before{content:none !important;display:none !important}',
'.dd-settings-card .cbi-section-table-row>td input{vertical-align:middle !important;margin:0 !important}',
'.dd-settings-card .cbi-section-table-row>td:nth-child(1){flex:0 0 auto !important;max-width:60px;opacity:.6;font-size:11px}',
'.dd-settings-card .cbi-section-table-row>td:nth-child(2){flex:1 1 auto !important}',
'.dd-settings-card .cbi-section-table-row>td:nth-child(3){flex:0 0 auto !important;text-align:center}',
'.dd-settings-card .cbi-section-table-row>td.cbi-section-actions{flex:0 0 auto !important;display:flex;gap:4px}',
/* clear the floated title so a Flag description gets the full row width
instead of wrapping around it (e.g. "局域网 / 私有地址不走代理。") */
'.dd-settings-card .cbi-value-description,.dd-settings-card .cbi-value-helptext{clear:both;width:100%}',
'}',
'.dd-converter{width:100%!important;max-width:1180px;margin:0 auto}',
'.dd-conv-card{padding:18px 20px;margin-bottom:12px}',
'.dd-conv-url-row{display:grid;grid-template-columns:minmax(280px,1fr) 230px max-content;align-items:center;gap:8px}',
'.dd-conv-import,.dd-conv-toolbar,.dd-conv-heading{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
'.dd-converter input,.dd-converter select,.dd-converter .cbi-button{box-sizing:border-box!important;height:32px!important;min-height:32px!important;margin:0!important;font-size:12px!important;line-height:22px!important}',
'.dd-converter input,.dd-converter select{padding:4px 8px!important;border:1px solid rgba(128,128,128,.28)!important;border-radius:6px!important;background-color:transparent;color:inherit;box-shadow:none!important}',
'.dd-converter input::placeholder,.dd-conv-yaml::placeholder,.dd-conv-filter::placeholder,.dd-conv-airport-name::placeholder{color:inherit;opacity:.55}',
'.dd-converter select{padding-right:28px!important}',
'.dd-converter .cbi-button{padding:4px 12px!important}',
'.dd-conv-url-row input,.dd-conv-filter,.dd-conv-target,.dd-conv-ua,.dd-conv-yaml{box-sizing:border-box;border:1px solid rgba(128,128,128,.28);border-radius:6px;background:transparent;color:inherit;font-size:12px;padding:7px 9px}',
'.dd-conv-url-row input{width:100%;min-width:0}',
'.dd-conv-ua{width:230px;max-width:230px}',
'.dd-conv-yaml{width:100%;min-height:150px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.5}',
'.dd-conv-or{text-align:center;font-size:11px;opacity:.5;margin:7px 0}',
'.dd-conv-heading{justify-content:space-between;gap:10px;margin-bottom:10px}',
'.dd-conv-heading .dd-card-title{margin:0}',
'.dd-conv-summary{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:4px 10px;font-size:11px;font-variant-numeric:tabular-nums}',
'.dd-conv-summary-item{display:inline-flex;align-items:baseline;gap:4px;white-space:nowrap;color:inherit;opacity:.62}',
'.dd-conv-summary-item strong{font-size:12px;font-weight:650;opacity:1;color:inherit}',
'.dd-conv-summary-label{font-size:10.5px}',
'.dd-conv-toolbar{margin-bottom:8px}',
'.dd-conv-filter{margin-left:auto;min-width:220px}',
'.dd-conv-results{border:1px solid rgba(128,128,128,.18);border-radius:7px;max-height:420px;overflow:auto}',
'.dd-conv-empty{display:flex;align-items:center;justify-content:center;min-height:72px;padding:16px;text-align:center;font-size:11.5px;opacity:.48}',
'.dd-conv-result{display:grid;grid-template-columns:22px minmax(100px,1fr) 90px minmax(130px,1.3fr);gap:8px;align-items:center;padding:7px 9px;border-bottom:1px solid rgba(128,128,128,.13);font-size:12px}',
'.dd-conv-result:last-child{border-bottom:0}',
'.dd-conv-result input{appearance:checkbox !important;-webkit-appearance:checkbox !important;accent-color:#4aa065;width:15px;height:15px}',
'.dd-conv-name,.dd-conv-result-state{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.dd-conv-proto{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;opacity:.7}',
'.dd-conv-result-state.good,.dd-conv-status.ok{color:#3da66a}',
'.dd-conv-result-state.dup{opacity:.55}',
'.dd-conv-result-state.bad,.dd-conv-status.err{color:#d96d6d}',
'.dd-conv-import label{font-size:12px;font-weight:600;opacity:.72}',
'.dd-conv-airport{display:grid;grid-template-columns:max-content minmax(240px,420px) max-content 130px;gap:8px;align-items:center;margin-bottom:9px;padding-bottom:9px;border-bottom:1px solid rgba(128,128,128,.15)}',
'.dd-conv-airport .dd-conv-target,.dd-conv-airport-name{box-sizing:border-box;width:100% !important;min-width:0 !important;height:32px !important;min-height:32px !important;margin:0 !important;padding:4px 8px !important;border:1px solid rgba(128,128,128,.28);border-radius:6px;background-color:transparent;color:inherit;font-size:12px !important;line-height:22px !important;box-shadow:none}',
'.dd-conv-airport-name{max-width:420px}',
'.dd-conv-group-summary{margin:2px 0 10px;padding:9px 11px;border-left:3px solid rgba(74,160,101,.65);border-radius:3px;background:rgba(74,160,101,.07);font-size:12px;line-height:1.5}',
'.dd-conv-group-summary.err{border-left-color:#d96d6d;background:rgba(217,109,109,.07);color:#d96d6d}',
'.dd-conv-submit{justify-content:flex-end}',
'.dd-conv-import .cbi-button{margin-left:auto}',
'.dd-conv-status{margin-top:9px;font-size:11.5px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;min-height:16px}',
'.dd-daed-login{box-sizing:border-box;width:min(560px,100%);margin:0 auto}',
'.dd-daed-login .cbi-value{display:grid!important;grid-template-columns:110px minmax(0,1fr)!important;gap:12px;align-items:center;padding:6px 0!important}',
'.dd-daed-login .cbi-value-title,.dd-daed-login .cbi-value-field{box-sizing:border-box;width:auto!important;min-width:0!important;margin:0!important;padding:0!important}',
'.dd-daed-login .cbi-value-field input{box-sizing:border-box;width:100%!important;max-width:none!important;min-width:0!important;height:36px!important;margin:0!important;padding:6px 10px!important}',
'.dd-daed-login-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}',
'@media(max-width:640px){.dd-conv-card{padding:14px}.dd-conv-heading{align-items:flex-start}.dd-conv-heading .dd-card-title{width:100%}.dd-conv-summary{justify-content:flex-start;width:100%;gap:5px 12px;padding-top:2px}.dd-conv-summary-item{min-width:74px}.dd-conv-filter{margin-left:0;width:100%}.dd-conv-result{grid-template-columns:20px minmax(90px,1fr) 68px}.dd-conv-result-state{grid-column:2/4;font-size:11px}.dd-conv-submit .cbi-button{width:100%!important}.dd-conv-airport{grid-template-columns:82px minmax(0,1fr)}.dd-conv-airport>*{max-width:none !important}.dd-conv-url-row{grid-template-columns:1fr}.dd-conv-url-row input,.dd-conv-url-row .dd-conv-ua,.dd-conv-url-row .cbi-button{width:100%;max-width:none}.dd-daed-login .cbi-value{grid-template-columns:1fr!important;gap:4px}.dd-daed-login-actions .cbi-button{flex:1 1 0}}'
].join('');
return baseclass.extend({ CSS: CSS });
@@ -0,0 +1,610 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require fs';
'require poll';
'require uci';
'require ui';
'require view';
'require view.daede.backend as backend';
'require view.daede.daed-session as daedSession';
const DATA_PATHS = {
geoip: '/usr/share/v2ray/geoip.dat',
geosite: '/usr/share/v2ray/geosite.dat'
};
// Geo data source presets. `loyalsoldier` keeps both URLs empty so the actual
// default lives in one place — update-geo.sh — avoiding UI/script drift.
const GEO_PRESETS = {
loyalsoldier: { geoip: '', geosite: '' },
v2fly: {
geoip: 'https://github.com/v2fly/geoip/releases/latest/download/geoip.dat',
geosite: 'https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat'
}
};
function currentPreset(gi, gs) {
if (!gi && !gs) return 'loyalsoldier';
if (gi === GEO_PRESETS.v2fly.geoip && gs === GEO_PRESETS.v2fly.geosite) return 'v2fly';
return 'custom';
}
const HEALTH_PATHS = {
btf: '/sys/kernel/btf/vmlinux',
btfDetached: '/usr/lib/debug/boot/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-new{color:#4a8cff}',
'.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;line-height:1.4;min-height:0;height:auto;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-backup-buttons{display:flex;align-items:center;gap:8px}',
'.dd-backup-reset{border-color:rgba(217,109,109,.55);color:#d96d6d}',
'@media(max-width:640px){.dd-backup-row{grid-template-columns:24px minmax(0,1fr)}.dd-backup-row .dd-up-meta{grid-column:2;white-space:normal}.dd-backup-row>span:nth-child(4){display:none}.dd-backup-buttons{grid-column:2;flex-wrap:wrap}}',
'.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}',
'.dd-geo-row label{opacity:.75;font-weight:600}',
'.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}',
/* selects cap at 200px on wide screens, shrink to the column on mobile (no overflow) */
'.dd-geo-row select{width:100%!important;max-width:200px;box-sizing:border-box}',
'.dd-geo-actions{margin-top:10px;display:flex;gap:10px;align-items:center}',
'.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}',
/* accordion — same structure as 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);
}
// Compare two version strings like `sort -V`: split into numeric / non-numeric
// chunks and compare chunk by chunk (numeric chunks numerically). Returns
// <0 if a<b, 0 if equal, >0 if a>b. Used so a stale third-party feed that
// happens to provide an OLDER build (e.g. compile-jell) is never offered as an
// "upgrade" over a newer locally installed package.
function cmpVer(a, b) {
const ax = String(a).match(/(\d+|\D+)/g) || [];
const bx = String(b).match(/(\d+|\D+)/g) || [];
const n = Math.max(ax.length, bx.length);
for (let i = 0; i < n; i++) {
const as = ax[i], bs = bx[i];
if (as === undefined) return -1;
if (bs === undefined) return 1;
if (/^\d+$/.test(as) && /^\d+$/.test(bs)) {
const d = parseInt(as, 10) - parseInt(bs, 10);
if (d !== 0) return d < 0 ? -1 : 1;
} else if (as !== bs) {
return as < bs ? -1 : 1;
}
}
return 0;
}
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: '' };
});
}
// YYYYMMDD-HHMM stamp for backup filenames
function stamp() {
const d = new Date(), p = n => (n < 10 ? '0' : '') + n;
return '' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '-' + p(d.getHours()) + p(d.getMinutes());
}
return view.extend({
load: function() {
// background apk update so new versions show on next poll
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {});
return Promise.all([
backend.detectBackend(),
uci.load('daed').catch(function() {}),
uci.load('daede').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', {}, '')
]);
};
// run a backgrounded script and stream its log into the log pane until it
// logs a final ✓/✗ status line; then refresh the badges. Inline, no popup.
const runJob = function(script, arg, btn, logPath) {
const orig = btn.textContent;
btn.disabled = true;
btn.textContent = '...';
let tries = 0;
const poll = function() {
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
if (/[✓✗]/.test(c)) { refresh(); return; }
if (tries++ > 90) return;
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
});
};
return fs.exec('/usr/share/luci-app-daede/' + script, [arg]).then(function(res) {
if (res.code === 0) return poll();
}).catch(function() {}).finally(function() {
btn.disabled = false;
btn.textContent = orig;
});
};
// === Data updates (GeoIP / GeoSite) ===
const updateGeo = function(kind, btn) {
return runJob('update-geo.sh', kind, btn, '/tmp/luci-app-daede.' + kind + '.log');
};
// === Package updates (dae|daed / luci-app-daede) ===
const upgradePkg = function(pkg, btn) {
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
};
// Configuration operations share a backend lock and one UI busy state.
const backupScript = '/usr/share/luci-app-daede/config-backup.sh';
const exportBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Export'));
const importBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Import'));
const resetBtn = E('button', { 'class': 'dd-up-btn dd-backup-reset', 'type': 'button' }, _('Restore Defaults'));
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
let backupBusy = false;
const showBackupError = function(e) {
ui.addNotification(null, E('p', {}, e.message || String(e)), 'error');
};
const configOperation = function(button, operation) {
if (backupBusy) return Promise.resolve();
backupBusy = true;
const label = button.textContent;
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = true; });
button.textContent = _('Working…');
return uci.changes().then(function(changes) {
if (['dae', 'daed', 'daede'].some(function(name) { return changes && changes[name] && changes[name].length; }))
throw new Error(_('Apply or discard pending daede changes first.'));
return operation();
}).catch(showBackupError).finally(function() {
backupBusy = false;
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = false; });
button.textContent = label;
fileInput.value = '';
});
};
const confirmBackup = function(message) {
return new Promise(function(resolve) {
const answer = function(value) { ui.hideModal(); resolve(value); };
ui.showModal(_('Confirm configuration operation'), [
E('p', {}, message),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': function() { answer(false); } }, _('Cancel')),
' ',
E('button', { 'class': 'cbi-button cbi-button-negative', 'click': function() { answer(true); } }, _('Continue'))
])
]);
});
};
const execBackup = function(args) {
return fs.exec(backupScript, args).then(function(res) {
if (!res || res.code !== 0)
throw new Error(String(res && (res.stderr || res.stdout) || _('Configuration operation failed.')).trim());
return res;
});
};
const waitBackup = function() {
let tries = 0;
const check = function() {
return fs.read_direct('/tmp/luci-app-daede.backup.log', 'text').then(function(content) {
logPane.textContent = content;
logPane.classList.add('show');
if (/^✗/m.test(content)) throw new Error(_('Configuration operation failed. See the log below.'));
if (/^✓/m.test(content)) return;
if (++tries > 90) throw new Error(_('Operation is still running. Check the log before retrying.'));
return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(check);
});
};
return check();
};
const reloadConfig = function(message) {
daedSession.clear(window.localStorage);
['dae', 'daed', 'daede'].forEach(function(name) { uci.unload(name); });
ui.showModal(_('Configuration restored'), [
E('p', {}, message),
E('div', { 'class': 'right' }, E('button', {
'class': 'cbi-button cbi-button-positive', 'click': function() { window.location.reload(); }
}, _('Reload Page')))
]);
};
exportBtn.addEventListener('click', async function() {
if (!await confirmBackup(_('Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?'))) return;
configOperation(exportBtn, function() {
return execBackup(['export']).then(function(res) {
const bin = atob((res.stdout || '').trim());
if (!bin) throw new Error(_('Export failed: empty archive'));
const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
const url = URL.createObjectURL(new Blob([bytes], { 'type': 'application/gzip' }));
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
});
});
});
importBtn.addEventListener('click', function() { fileInput.click(); });
fileInput.addEventListener('change', async function(ev) {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
if (file.size > 184320) {
showBackupError(new Error(_('Backup exceeds the 180 KiB limit. Use System Backup for larger files.')));
fileInput.value = ''; return;
}
if (!await confirmBackup(_('Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?'))) {
fileInput.value = ''; return;
}
configOperation(importBtn, function() {
return new Promise(function(resolve, reject) {
const reader = new FileReader();
reader.onerror = function() { reject(new Error(_('Unable to read backup file.'))); };
reader.onload = function() { resolve(String(reader.result).split(',')[1] || ''); };
reader.readAsDataURL(file);
}).then(function(b64) {
// Per-tab uploads cannot overwrite each other before the backend locks.
const token = Array.from(window.crypto.getRandomValues(new Uint8Array(16)), function(n) { return n.toString(16).padStart(2, '0'); }).join('');
const upload = '/tmp/daede-import.' + token + '.b64';
return fs.write(upload, b64).then(function() { return execBackup(['import', upload]); })
.finally(function() { return fs.remove(upload).catch(function() {}); });
}).then(waitBackup).then(function() {
reloadConfig(_('Backup restored. The active backend follows the saved enabled setting.'));
});
});
});
resetBtn.addEventListener('click', async function() {
if (!await confirmBackup(_('Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept.'))) return;
configOperation(resetBtn, function() {
return execBackup(['reset']).then(waitBackup).then(function() {
reloadConfig(_('All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually.'));
});
});
});
const refresh = function() {
const probes = [
probeFile(DATA_PATHS.geoip),
probeFile(DATA_PATHS.geosite),
probeFile(HEALTH_PATHS.btf),
probeFile(HEALTH_PATHS.btfDetached),
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], btfDetached = r[3], running = r[4], service = r[5];
const pkgOffset = 6;
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); });
// Only treat it as upgradable when the feed version is STRICTLY
// higher than what's installed. A lower/equal feed version (e.g.
// a stale compile-jell build) must never be offered as an upgrade.
const cmp = (entry.r.installed && entry.r.latest)
? cmpVer(entry.r.latest, entry.r.installed) : null;
const updatable = cmp !== null && cmp > 0;
let meta;
if (!entry.r.installed) {
meta = _('not installed via package manager');
btn.disabled = true;
btn.textContent = _('Unavailable');
} else if (!entry.r.latest) {
// latest unknown (registry unreachable) - nothing to upgrade to
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('latest version unknown');
btn.disabled = true;
} else if (updatable) {
meta = _('installed') + ': ' + entry.r.installed + ' → ' + _('latest') + ': ' + entry.r.latest;
} else {
// installed >= feed: already up to date (feed may even be older)
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('up to date');
btn.disabled = true;
}
pkgBody.appendChild(mkRow(
updatable ? '↑' : (entry.r.installed ? '✓' : '✗'),
updatable ? 'dd-up-new' : (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 if (btfDetached.exists)
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btfDetached + ' · ' + fmtBytes(btfDetached.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 && running[ctx.name]) {
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('in use by daed')));
} else if (ctx.backend.useNetns && ns && ns.exists) {
const btn = E('button', { 'class': 'dd-up-btn' }, _('Clean'));
btn.addEventListener('click', function() {
btn.disabled = true;
backend.detectRunning().then(function(now) {
const msg = now[ctx.name]
? _('%s is running. Deleting the daens netns now will break its networking until you restart it. Continue?').format(ctx.name)
: _('Delete the daens netns?');
if (confirm(msg))
return 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();
// === Geo data source (preset / custom URL + auto-update) ===
const geoSettings = (function() {
const gi0 = uci.get('daede', 'config', 'geoip_url') || '';
const gs0 = uci.get('daede', 'config', 'geosite_url') || '';
const auto0 = uci.get('daede', 'config', 'geo_auto') === '1';
const freq0 = uci.get('daede', 'config', 'geo_auto_freq') || 'daily';
const preset0 = currentPreset(gi0, gs0);
const presetSel = E('select', {}, [
E('option', { 'value': 'loyalsoldier' }, 'Loyalsoldier'),
E('option', { 'value': 'v2fly' }, 'v2fly'),
E('option', { 'value': 'custom' }, _('Custom'))
]);
presetSel.value = preset0;
const giInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geoip.dat' });
const gsInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geosite.dat' });
giInput.value = preset0 === 'custom' ? gi0 : '';
gsInput.value = preset0 === 'custom' ? gs0 : '';
const customRows = E('div', {}, [
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoIP URL'), giInput ]),
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoSite URL'), gsInput ])
]);
const syncCustom = function() {
customRows.style.display = presetSel.value === 'custom' ? '' : 'none';
};
presetSel.addEventListener('change', syncCustom);
syncCustom();
const autoSel = E('select', {}, [
E('option', { 'value': 'off' }, _('Disabled')),
E('option', { 'value': 'daily' }, _('Daily')),
E('option', { 'value': 'weekly' }, _('Weekly'))
]);
autoSel.value = auto0 ? freq0 : 'off';
const saveBtn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Save'));
saveBtn.addEventListener('click', function() {
const p = presetSel.value;
let gi = '', gs = '';
const geoAuto = autoSel.value !== 'off';
const geoFreq = autoSel.value === 'weekly' ? 'weekly' : 'daily';
if (p === 'v2fly') { gi = GEO_PRESETS.v2fly.geoip; gs = GEO_PRESETS.v2fly.geosite; }
else if (p === 'custom') { gi = giInput.value.trim(); gs = gsInput.value.trim(); }
uci.set('daede', 'config', 'geoip_url', gi);
uci.set('daede', 'config', 'geosite_url', gs);
uci.set('daede', 'config', 'geo_auto', geoAuto ? '1' : '0');
uci.set('daede', 'config', 'geo_auto_freq', geoFreq);
const orig = saveBtn.textContent;
saveBtn.disabled = true; saveBtn.textContent = '...';
uci.save().then(function() {
return uci.changes();
}).then(function(changes) {
// apply only when changed; empty changeset fails apply (#16)
if (changes && Object.keys(changes).length)
return uci.apply();
}).then(function() {
return fs.exec('/usr/share/luci-app-daede/geo-cron.sh', [geoAuto ? 'enable' : 'disable']);
}).then(function() {
logPane.textContent = _('Geo data source saved.');
logPane.classList.add('show');
ui.changes.init();
}).catch(function(e) {
logPane.textContent = _('Save failed') + ': ' + (e && e.message ? e.message : e);
logPane.classList.add('show');
}).finally(function() {
saveBtn.disabled = false; saveBtn.textContent = orig;
});
});
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, _('Data Source')),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
E('div', { 'class': 'dd-adv-body' }, [
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]),
customRows,
E('div', { 'class': 'dd-geo-row' }, [
E('label', {}, _('Auto-update')),
autoSel
]),
E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ])
])
]);
adv.firstChild.addEventListener('click', function() {
adv.classList.toggle('dd-closed');
});
return adv;
})();
return E('div', { 'class': 'dd-up-wrap' }, [
E('style', {}, CSS),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
dataBody,
geoSettings
]),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
pkgBody
]),
E('div', { 'class': 'dd-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Config Backup')),
E('div', { 'class': 'dd-up-row dd-backup-row' }, [
E('span', { 'class': 'dd-up-icon dd-up-new' }, '⇩'),
E('span', { 'class': 'dd-up-name' }, _('dae + daed')),
E('span', {
'class': 'dd-up-meta',
'title': _('Backups contain account and subscription credentials. Store them securely.')
}, _('Back up / restore the whole daede config (kernels excluded)')),
E('span', {}, ''),
E('div', { 'class': 'dd-backup-buttons' }, [exportBtn, importBtn, resetBtn])
]),
fileInput
]),
(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
});
@@ -0,0 +1,21 @@
(The MIT License)
Copyright (C) 2011-2015 by Vitaly Puzrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,33 @@
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.jsyaml={}))})(this,function(e){Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=s(((e,t)=>{function n(e){return e==null}function r(e){return typeof e==`object`&&!!e}function i(e){return Array.isArray(e)?e:n(e)?[]:[e]}function a(e,t){if(t){let n=Object.keys(t);for(let r=0,i=n.length;r<i;r+=1){let i=n[r];e[i]=t[i]}}return e}function o(e,t){let n=``;for(let r=0;r<t;r+=1)n+=e;return n}function s(e){return e===0&&1/e==-1/0}t.exports.isNothing=n,t.exports.isObject=r,t.exports.toArray=i,t.exports.repeat=o,t.exports.isNegativeZero=s,t.exports.extend=a})),d=s(((e,t)=>{function n(e,t){let n=``,r=e.reason||`(unknown reason)`;return e.mark?(e.mark.name&&(n+=`in "`+e.mark.name+`" `),n+=`(`+(e.mark.line+1)+`:`+(e.mark.column+1)+`)`,!t&&e.mark.snippet&&(n+=`
`+e.mark.snippet),r+` `+n):r}function r(e,t){Error.call(this),this.name=`YAMLException`,this.reason=e,this.mark=t,this.message=n(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=Error().stack||``}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){return this.name+`: `+n(this,e)},t.exports=r})),f=s(((e,t)=>{var n=u();function r(e,t,n,r,i){let a=``,o=``,s=Math.floor(i/2)-1;return r-t>s&&(a=` ... `,t=r-s+a.length),n-r>s&&(o=` ...`,n=r+s-o.length),{str:a+e.slice(t,n).replace(/\t/g,`→`)+o,pos:r-t+a.length}}function i(e,t){return n.repeat(` `,t-e.length)+e}function a(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),typeof t.indent!=`number`&&(t.indent=1),typeof t.linesBefore!=`number`&&(t.linesBefore=3),typeof t.linesAfter!=`number`&&(t.linesAfter=2);let a=/\r?\n|\r|\0/g,o=[0],s=[],c,l=-1;for(;c=a.exec(e.buffer);)s.push(c.index),o.push(c.index+c[0].length),e.position<=c.index&&l<0&&(l=o.length-2);l<0&&(l=o.length-1);let u=``,d=Math.min(e.line+t.linesAfter,s.length).toString().length,f=t.maxLength-(t.indent+d+3);for(let a=1;a<=t.linesBefore&&!(l-a<0);a++){let c=r(e.buffer,o[l-a],s[l-a],e.position-(o[l]-o[l-a]),f);u=n.repeat(` `,t.indent)+i((e.line-a+1).toString(),d)+` | `+c.str+`
`+u}let p=r(e.buffer,o[l],s[l],e.position,f);u+=n.repeat(` `,t.indent)+i((e.line+1).toString(),d)+` | `+p.str+`
`,u+=n.repeat(`-`,t.indent+d+3+p.pos)+`^
`;for(let a=1;a<=t.linesAfter&&!(l+a>=s.length);a++){let c=r(e.buffer,o[l+a],s[l+a],e.position-(o[l]-o[l+a]),f);u+=n.repeat(` `,t.indent)+i((e.line+a+1).toString(),d)+` | `+c.str+`
`}return u.replace(/\n$/,``)}t.exports=a})),p=s(((e,t)=>{var n=d(),r=[`kind`,`multi`,`resolve`,`construct`,`instanceOf`,`predicate`,`represent`,`representName`,`defaultStyle`,`styleAliases`],i=[`scalar`,`sequence`,`mapping`];function a(e){let t={};return e!==null&&Object.keys(e).forEach(function(n){e[n].forEach(function(e){t[String(e)]=n})}),t}function o(e,t){if(t=t||{},Object.keys(t).forEach(function(t){if(r.indexOf(t)===-1)throw new n(`Unknown option "`+t+`" is met in definition of "`+e+`" YAML type.`)}),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=a(t.styleAliases||null),i.indexOf(this.kind)===-1)throw new n(`Unknown kind "`+this.kind+`" is specified for "`+e+`" YAML type.`)}t.exports=o})),m=s(((e,t)=>{var n=d(),r=p();function i(e,t){let n=[];return e[t].forEach(function(e){let t=n.length;n.forEach(function(n,r){n.tag===e.tag&&n.kind===e.kind&&n.multi===e.multi&&(t=r)}),n[t]=e}),n}function a(){let e={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function t(t){t.multi?(e.multi[t.kind].push(t),e.multi.fallback.push(t)):e[t.kind][t.tag]=e.fallback[t.tag]=t}for(let e=0,n=arguments.length;e<n;e+=1)arguments[e].forEach(t);return e}function o(e){return this.extend(e)}o.prototype.extend=function(e){let t=[],s=[];if(e instanceof r)s.push(e);else if(Array.isArray(e))s=s.concat(e);else if(e&&(Array.isArray(e.implicit)||Array.isArray(e.explicit)))e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(s=s.concat(e.explicit));else throw new n(`Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })`);t.forEach(function(e){if(!(e instanceof r))throw new n(`Specified list of YAML types (or a single Type object) contains a non-Type object.`);if(e.loadKind&&e.loadKind!==`scalar`)throw new n(`There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.`);if(e.multi)throw new n(`There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.`)}),s.forEach(function(e){if(!(e instanceof r))throw new n(`Specified list of YAML types (or a single Type object) contains a non-Type object.`)});let c=Object.create(o.prototype);return c.implicit=(this.implicit||[]).concat(t),c.explicit=(this.explicit||[]).concat(s),c.compiledImplicit=i(c,`implicit`),c.compiledExplicit=i(c,`explicit`),c.compiledTypeMap=a(c.compiledImplicit,c.compiledExplicit),c},t.exports=o})),h=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:str`,{kind:`scalar`,construct:function(e){return e===null?``:e}})})),g=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:seq`,{kind:`sequence`,construct:function(e){return e===null?[]:e}})})),_=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:map`,{kind:`mapping`,construct:function(e){return e===null?{}:e}})})),v=s(((e,t)=>{t.exports=new(m())({explicit:[h(),g(),_()]})})),y=s(((e,t)=>{var n=p();function r(e){if(e===null)return!0;let t=e.length;return t===1&&e===`~`||t===4&&(e===`null`||e===`Null`||e===`NULL`)}function i(){return null}function a(e){return e===null}t.exports=new n(`tag:yaml.org,2002:null`,{kind:`scalar`,resolve:r,construct:i,predicate:a,represent:{canonical:function(){return`~`},lowercase:function(){return`null`},uppercase:function(){return`NULL`},camelcase:function(){return`Null`},empty:function(){return``}},defaultStyle:`lowercase`})})),b=s(((e,t)=>{var n=p();function r(e){if(e===null)return!1;let t=e.length;return t===4&&(e===`true`||e===`True`||e===`TRUE`)||t===5&&(e===`false`||e===`False`||e===`FALSE`)}function i(e){return e===`true`||e===`True`||e===`TRUE`}function a(e){return Object.prototype.toString.call(e)===`[object Boolean]`}t.exports=new n(`tag:yaml.org,2002:bool`,{kind:`scalar`,resolve:r,construct:i,predicate:a,represent:{lowercase:function(e){return e?`true`:`false`},uppercase:function(e){return e?`TRUE`:`FALSE`},camelcase:function(e){return e?`True`:`False`}},defaultStyle:`lowercase`})})),x=s(((e,t)=>{var n=u(),r=p();function i(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function a(e){return e>=48&&e<=55}function o(e){return e>=48&&e<=57}function s(e){if(e===null)return!1;let t=e.length,n=0,r=!1;if(!t)return!1;let s=e[n];if((s===`-`||s===`+`)&&(s=e[++n]),s===`0`){if(n+1===t)return!0;if(s=e[++n],s===`b`){for(n++;n<t;n++){if(s=e[n],s!==`0`&&s!==`1`)return!1;r=!0}return r&&Number.isFinite(c(e))}if(s===`x`){for(n++;n<t;n++){if(!i(e.charCodeAt(n)))return!1;r=!0}return r&&Number.isFinite(c(e))}if(s===`o`){for(n++;n<t;n++){if(!a(e.charCodeAt(n)))return!1;r=!0}return r&&Number.isFinite(c(e))}}for(;n<t;n++){if(!o(e.charCodeAt(n)))return!1;r=!0}return r?Number.isFinite(c(e)):!1}function c(e){let t=e Line truncated
\r`;function i(e){if(e===null)return!1;let t=0,n=e.length,i=r;for(let r=0;r<n;r++){let n=i.indexOf(e.charAt(r));if(!(n>64)){if(n<0)return!1;t+=6}}return t%8==0}function a(e){let t=e.replace(/[\r\n=]/g,``),n=t.length,i=r,a=0,o=[];for(let e=0;e<n;e++)e%4==0&&e&&(o.push(a>>16&255),o.push(a>>8&255),o.push(a&255)),a=a<<6|i.indexOf(t.charAt(e));let s=n%4*6;return s===0?(o.push(a>>16&255),o.push(a>>8&255),o.push(a&255)):s===18?(o.push(a>>10&255),o.push(a>>2&255)):s===12&&o.push(a>>4&255),new Uint8Array(o)}function o(e){let t=``,n=0,i=e.length,a=r;for(let r=0;r<i;r++)r%3==0&&r&&(t+=a[n>>18&63],t+=a[n>>12&63],t+=a[n>>6&63],t+=a[n&63]),n=(n<<8)+e[r];let o=i%3;return o===0?(t+=a[n>>18&63],t+=a[n>>12&63],t+=a[n>>6&63],t+=a[n&63]):o===2?(t+=a[n>>10&63],t+=a[n>>4&63],t+=a[n<<2&63],t+=a[64]):o===1&&(t+=a[n>>2&63],t+=a[n<<4&63],t+=a[64],t+=a[64]),t}function s(e){return Object.prototype.toString.call(e)===`[object Uint8Array]`}t.exports=new n(`tag:yaml.org,2002:binary`,{kind:`scalar`,resolve:i,construct:a,predicate:s,represent:o})})),O=s(((e,t)=>{var n=p(),r=Object.prototype.hasOwnProperty,i=Object.prototype.toString;function a(e){if(e===null)return!0;let t=[],n=e;for(let e=0,a=n.length;e<a;e+=1){let a=n[e],o=!1;if(i.call(a)!==`[object Object]`)return!1;let s;for(s in a)if(r.call(a,s))if(!o)o=!0;else return!1;if(!o)return!1;if(t.indexOf(s)===-1)t.push(s);else return!1}return!0}function o(e){return e===null?[]:e}t.exports=new n(`tag:yaml.org,2002:omap`,{kind:`sequence`,resolve:a,construct:o})})),k=s(((e,t)=>{var n=p(),r=Object.prototype.toString;function i(e){if(e===null)return!0;let t=e,n=Array(t.length);for(let e=0,i=t.length;e<i;e+=1){let i=t[e];if(r.call(i)!==`[object Object]`)return!1;let a=Object.keys(i);if(a.length!==1)return!1;n[e]=[a[0],i[a[0]]]}return!0}function a(e){if(e===null)return[];let t=e,n=Array(t.length);for(let e=0,r=t.length;e<r;e+=1){let r=t[e],i=Object.keys(r);n[e]=[i[0],r[i[0]]]}return n}t.exports=new n(`tag:yaml.org,2002:pairs`,{kind:`sequence`,resolve:i,construct:a})})),A=s(((e,t)=>{var n=p(),r=Object.prototype.hasOwnProperty;function i(e){if(e===null)return!0;let t=e;for(let e in t)if(r.call(t,e)&&t[e]!==null)return!1;return!0}function a(e){return e===null?{}:e}t.exports=new n(`tag:yaml.org,2002:set`,{kind:`mapping`,resolve:i,construct:a})})),ee=s(((e,t)=>{t.exports=w().extend({implicit:[T(),E()],explicit:[D(),O(),k(),A()]})})),j=s(((e,t)=>{var n=u(),r=d(),i=f(),a=ee(),o=Object.prototype.hasOwnProperty,s=1,c=2,l=3,p=4,m=1,h=2,g=3,_=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,v=/[\x85\u2028\u2029]/,y=/[,\[\]{}]/,b=/^(?:!|!!|![0-9A-Za-z-]+!)$/,x=/^(?:!|[^,\[\]{}])(?:%[0-9a-f]{2}|[0-9a-z\-#;/?:@&=+$,_.!~*'()\[\]])*$/i;function S(e){return Object.prototype.toString.call(e)}function C(e){return e===10||e===13}function w(e){return e===9||e===32}function T(e){return e===9||e===32||e===10||e===13}function E(e){return e===44||e===91||e===93||e===123||e===125}function D(e){if(e>=48&&e<=57)return e-48;let t=e|32;return t>=97&&t<=102?t-97+10:-1}function O(e){return e===120?2:e===117?4:e===85?8:0}function k(e){return e>=48&&e<=57?e-48:-1}function A(e){switch(e){case 48:return`\0`;case 97:return`\x07`;case 98:return`\b`;case 116:return` `;case 9:return` `;case 110:return`
`;case 118:return`\v`;case 102:return`\f`;case 114:return`\r`;case 101:return`\x1B`;case 32:return` `;case 34:return`"`;case 47:return`/`;case 92:return`\\`;case 78:return`…`;case 95:return`\xA0`;case 76:return`\u2028`;case 80:return`\u2029`;default:return``}}function j(e){return e<=65535?String.fromCharCode(e):String.fromCharCode((e-65536>>10)+55296,(e-65536&1023)+56320)}function te(e,t,n){t===`__proto__`?Object.defineProperty(e,t,{configurable:!0,enumerable:!0,writable:!0,value:n}):e[t]=n}var M=Array(256),N=Array(256);for(let e=0;e<256;e++)M[e]=+!!A(e),N[e]=A(e);function ne(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||a,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.maxDepth=typeof t.maxDepth==`number`?t.maxDepth:100,this.maxMergeSeqLength=typeof t.maxMergeSeqLength==`number`?t.maxMergeSeqLength:20,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.depth=0,this.firstTabInLine=-1,this.documents=[],this.anchorMapTransactions=[]}function P(e,t){let n={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return n.snippet=i(n),new r(t,n)}function F(e,t){throw P(e,t)}function I(e,t){e.onWarning&&e.onWarning.call(null,P(e,t))}function L(e,t,n){let r=e.anchorMapTransactions;if(r.length!==0){let n=r[r.length-1];o.call(n,t)||(n[t]={existed:o.call(e.anchorMap,t),value:e.anchorMap[t]})}e.anchorMap[t]=n}function R(e){e.anchorMapTransactions.push(Object.create(null))}function re(e){let t=e.anchorMapTransactions.pop(),n=e.anchorMapTransactions;if(n.length===0)return;let r=n[n.length-1],i=Object.keys(t);for(let e=0,n=i.length;e<n;e+=1){let n=i[e];o.call(r,n)||(r[n]=t[n])}}function z(e){let t=e.anchorMapTransactions.pop(),n=Object.keys(t);for(let r=n.length-1;r>=0;--r){let i=t[n[r]];i.existed?e.anchorMap[n[r]]=i.value:delete e.anchorMap[n[r]]}}function B(e){return{position:e.position,line:e.line,lineStart:e.lineStart,lineIndent:e.lineIndent,firstTabInLine:e.firstTabInLine,tag:e.tag,anchor:e.anchor,kind:e.kind,result:e.result}}function V(e,t){e.position=t.position,e.line=t.line,e.lineStart=t.lineStart,e.lineIndent=t.lineIndent,e.firstTabInLine=t.firstTabInLine,e.tag=t.tag,e.anchor=t.anchor,e.kind=t.kind,e.result=t.result}var H={YAML:function(e,t,n){e.version!==null&&F(e,`duplication of %YAML directive`),n.length!==1&&F(e,`YAML directive accepts exactly one argument`);let r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]);r===null&&F(e,`ill-formed argument of the YAML directive`);let i=parseInt(r[1],10),a=parseInt(r[2],10);i!==1&&F(e,`unacceptable YAML version of the document`),e.version=n[0],e.checkLineBreaks=a<2,a!==1&&a!==2&&I(e,`unsupported YAML version of the document`)},TAG:function(e,t,n){let r;n.length!==2&&F(e,`TAG directive accepts exactly two arguments`);let i=n[0];r=n[1],b.test(i)||F(e,`ill-formed tag handle (first argument) of the TAG directive`),o.call(e.tagMap,i)&&F(e,`there is a previously declared suffix for "`+i+`" tag handle`),x.test(r)||F(e,`ill-formed tag prefix (second argument) of the TAG directive`);try{r=decodeURIComponent(r)}catch(t){F(e,`tag prefix is malformed: `+r)}e.tagMap[i]=r}};function U(e,t,n,r){if(t<n){let i=e.input.slice(t,n);if(r)for(let t=0,n=i.length;t<n;t+=1){let n=i.charCodeAt(t);n===9||n>=32&&n<=1114111||F(e,`expected valid JSON character`)}else _.test(i)&&F(e,`the stream contains non-printable characters`);e.result+=i}}function W(e,t,r,i){n.isObject(r)||F(e,`cannot merge mappings; the provided source object is unacceptable`);let a=Object.keys(r);for(let e=0,n=a.length;e<n;e+=1){let n=a[e];o.call(t,n)||(te(t,n,r[n]),i[n]=!0)}}function G(e,t,n,r,i,a,s,c,l){if(Array.isArray(i)){i=Array.prototype.slice.call(i);for(let t=0,n=i.length;t<n;t+=1)Array.isArray(i[t])&&F(e,`nested arrays are not supported inside keys`),typeof i==`object`&&S(i[t])===`[object Object]`&&(i[t]=`[object Object]`)}if(typeof i==`object`&&S(i)===`[object Object]`&&(i=`[object Object]`),i=String(i),t===null&&(t={}),r===`tag:yaml.org,2002:merge`)if(Array.isArray(a)){a.length>e.maxMergeSeqLength&&F(e,`merge sequence length exceeded maxMergeSeqLength (`+e.maxMergeSeqLength+`)`);let r=new Set;for(let i=0,o=a.length;i<o;i+=1){let o=a[i];r.has(o)||(r.add(o),W(e,t,o,n))}}else W(e,t,a,n);else !e.json&&!o.call(n,i)&&o.call(t,i)&&(e.line=s||e.line,e.lineStart=c||e.lineStart,e.position=l||e.position,F(e,`duplicated mapping key`)),te(t,i,a),delete n[i];return t}function K(e){let t=e.input.charCodeAt(e.position);t===10?e.position++:t===13?(e.position++,e.input.charCodeAt(e.position)===10&&e.position++):F(e,`a line break is expected`),e.line+=1,e.lineStart=e.position,e.firstTabInLine=-1}function q(e,t,n){let r=0,i=e.input.charCodeAt(e.position);for(;i!==0;){for(;w(i);)i===9&&e.firstTabInLine===-1&&(e.firstTabInLine=e.position),i=e.input.charCodeAt(++e.position);if(t&&i===35)d Line truncated
`,t-1))}function ie(e,t,n){let r,i,a,o,s,c,l=e.kind,u=e.result,d=e.input.charCodeAt(e.position);if(T(d)||E(d)||d===35||d===38||d===42||d===33||d===124||d===62||d===39||d===34||d===37||d===64||d===96)return!1;if(d===63||d===45){let t=e.input.charCodeAt(e.position+1);if(T(t)||n&&E(t))return!1}for(e.kind=`scalar`,e.result=``,r=i=e.position,a=!1;d!==0;){if(d===58){let t=e.input.charCodeAt(e.position+1);if(T(t)||n&&E(t))break}else if(d===35){if(T(e.input.charCodeAt(e.position-1)))break}else if(e.position===e.lineStart&&J(e)||n&&E(d))break;else if(C(d))if(o=e.line,s=e.lineStart,c=e.lineIndent,q(e,!1,-1),e.lineIndent>=t){a=!0,d=e.input.charCodeAt(e.position);continue}else{e.position=i,e.line=o,e.lineStart=s,e.lineIndent=c;break}a&&(U(e,r,i,!1),Y(e,e.line-o),r=i=e.position,a=!1),w(d)||(i=e.position+1),d=e.input.charCodeAt(++e.position)}return U(e,r,i,!1),e.result?!0:(e.kind=l,e.result=u,!1)}function X(e,t){let n,r,i=e.input.charCodeAt(e.position);if(i!==39)return!1;for(e.kind=`scalar`,e.result=``,e.position++,n=r=e.position;(i=e.input.charCodeAt(e.position))!==0;)if(i===39)if(U(e,n,e.position,!0),i=e.input.charCodeAt(++e.position),i===39)n=e.position,e.position++,r=e.position;else return!0;else C(i)?(U(e,n,r,!0),Y(e,q(e,!1,t)),n=r=e.position):e.position===e.lineStart&&J(e)?F(e,`unexpected end of the document within a single quoted scalar`):(e.position++,w(i)||(r=e.position));F(e,`unexpected end of the stream within a single quoted scalar`)}function ae(e,t){let n,r,i,a=e.input.charCodeAt(e.position);if(a!==34)return!1;for(e.kind=`scalar`,e.result=``,e.position++,n=r=e.position;(a=e.input.charCodeAt(e.position))!==0;)if(a===34)return U(e,n,e.position,!0),e.position++,!0;else if(a===92){if(U(e,n,e.position,!0),a=e.input.charCodeAt(++e.position),C(a))q(e,!1,t);else if(a<256&&M[a])e.result+=N[a],e.position++;else if((i=O(a))>0){let t=i,n=0;for(;t>0;t--)a=e.input.charCodeAt(++e.position),(i=D(a))>=0?n=(n<<4)+i:F(e,`expected hexadecimal character`);e.result+=j(n),e.position++}else F(e,`unknown escape sequence`);n=r=e.position}else C(a)?(U(e,n,r,!0),Y(e,q(e,!1,t)),n=r=e.position):e.position===e.lineStart&&J(e)?F(e,`unexpected end of the document within a double quoted scalar`):(e.position++,w(a)||(r=e.position));F(e,`unexpected end of the stream within a double quoted scalar`)}function oe(e,t){let n=!0,r,i,a,o=e.tag,c,l=e.anchor,u,d,f,p,m=Object.create(null),h,g,_,v=e.input.charCodeAt(e.position);if(v===91)u=93,p=!1,c=[];else if(v===123)u=125,p=!0,c={};else return!1;for(e.anchor!==null&&L(e,e.anchor,c),v=e.input.charCodeAt(++e.position);v!==0;){if(q(e,!0,t),v=e.input.charCodeAt(e.position),v===u)return e.position++,e.tag=o,e.anchor=l,e.kind=p?`mapping`:`sequence`,e.result=c,!0;n?v===44&&F(e,`expected the node content, but found ','`):F(e,`missed comma between flow collection entries`),g=h=_=null,d=f=!1,v===63&&T(e.input.charCodeAt(e.position+1))&&(d=f=!0,e.position++,q(e,!0,t)),r=e.line,i=e.lineStart,a=e.position,Q(e,t,s,!1,!0),g=e.tag,h=e.result,q(e,!0,t),v=e.input.charCodeAt(e.position),(f||e.line===r)&&v===58&&(d=!0,v=e.input.charCodeAt(++e.position),q(e,!0,t),Q(e,t,s,!1,!0),_=e.result),p?G(e,c,m,g,h,_,r,i,a):d?c.push(G(e,null,m,g,h,_,r,i,a)):c.push(h),q(e,!0,t),v=e.input.charCodeAt(e.position),v===44?(n=!0,v=e.input.charCodeAt(++e.position)):n=!1}F(e,`unexpected end of the stream within a flow collection`)}function Z(e,t){let r,i=m,a=!1,o=!1,s=t,c=0,l=!1,u,d=e.input.charCodeAt(e.position);if(d===124)r=!1;else if(d===62)r=!0;else return!1;for(e.kind=`scalar`,e.result=``;d!==0;)if(d=e.input.charCodeAt(++e.position),d===43||d===45)m===i?i=d===43?g:h:F(e,`repeat of a chomping mode identifier`);else if((u=k(d))>=0)u===0?F(e,`bad explicit indentation width of a block scalar; it cannot be less than one`):o?F(e,`repeat of an indentation width identifier`):(s=t+u-1,o=!0);else break;if(w(d)){do d=e.input.charCodeAt(++e.position);while(w(d));if(d===35)do d=e.input.charCodeAt(++e.position);while(!C(d)&&d!==0)}for(;d!==0;){for(K(e),e.lineIndent=0,d=e.input.charCodeAt(e.position);(!o||e.lineIndent<s)&&d===32;)e.lineIndent++,d=e.input.charCodeAt(++e.position);if(!o&&e.lineIndent>s&&(s=e.lineIndent),C(d)){c++;continue}if(!o&&s===0&&F(e,`missing indentation for block scalar`),e.lineIndent<s){i===g?e.result+=n.repeat(`
`,a?1+c:c):i===m&&a&&(e.result+=`
`);break}r?w(d)?(l=!0,e.result+=n.repeat(`
`,a?1+c:c)):l?(l=!1,e.result+=n.repeat(`
`,c+1)):c===0?a&&(e.result+=` `):e.result+=n.repeat(`
`,c):e.result+=n.repeat(`
`,a?1+c:c),a=!0,o=!0,c=0;let t=e.position;for(;!C(d)&&d!==0;)d=e.input.charCodeAt(++e.position);U(e,t,e.position,!1)}return!0}function se(e,t){let n=e.tag,r=e.anchor,i=[],a=!1;if(e.firstTabInLine!==-1)return!1;e.anchor!==null&&L(e,e.anchor,i);let o=e.input.charCodeAt(e.position);for(;o!==0&&(e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,F(e,`tab characters must not be used in indentation`)),!(o!==45||!T(e.input.charCodeAt(e.position+1))));){if(a=!0,e.position++,q(e,!0,-1)&&e.lineIndent<=t){i.push(null),o=e.input.charCodeAt(e.position);continue}let n=e.line;if(Q(e,t,l,!1,!0),i.push(e.result),q(e,!0,-1),o=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&o!==0)F(e,`bad indentation of a sequence entry`);else if(e.lineIndent<t)break}return a?(e.tag=n,e.anchor=r,e.kind=`sequence`,e.result=i,!0):!1}function ce(e,t,n){let r,i,a,o,s=e.tag,l=e.anchor,u={},d=Object.create(null),f=null,m=null,h=null,g=!1,_=!1;if(e.firstTabInLine!==-1)return!1;e.anchor!==null&&L(e,e.anchor,u);let v=e.input.charCodeAt(e.position);for(;v!==0;){!g&&e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,F(e,`tab characters must not be used in indentation`));let y=e.input.charCodeAt(e.position+1),b=e.line;if((v===63||v===58)&&T(y))v===63?(g&&(G(e,u,d,f,m,null,i,a,o),f=m=h=null),_=!0,g=!0,r=!0):g?(g=!1,r=!0):F(e,`incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line`),e.position+=1,v=y;else{if(i=e.line,a=e.lineStart,o=e.position,!Q(e,n,c,!1,!0))break;if(e.line===b){for(v=e.input.charCodeAt(e.position);w(v);)v=e.input.charCodeAt(++e.position);if(v===58)v=e.input.charCodeAt(++e.position),T(v)||F(e,`a whitespace character is expected after the key-value separator within a block mapping`),g&&(G(e,u,d,f,m,null,i,a,o),f=m=h=null),_=!0,g=!1,r=!1,f=e.tag,m=e.result;else if(_)F(e,`can not read an implicit mapping pair; a colon is missed`);else return e.tag=s,e.anchor=l,!0}else if(_)F(e,`can not read a block mapping entry; a multiline key may not be an implicit key`);else return e.tag=s,e.anchor=l,!0}if((e.line===b||e.lineIndent>t)&&(g&&(i=e.line,a=e.lineStart,o=e.position),Q(e,t,p,!0,r)&&(g?m=e.result:h=e.result),g||(G(e,u,d,f,m,h,i,a,o),f=m=h=null),q(e,!0,-1),v=e.input.charCodeAt(e.position)),(e.line===b||e.lineIndent>t)&&v!==0)F(e,`bad indentation of a mapping entry`);else if(e.lineIndent<t)break}return g&&G(e,u,d,f,m,null,i,a,o),_&&(e.tag=s,e.anchor=l,e.kind=`mapping`,e.result=u),_}function le(e){let t=!1,n=!1,r,i,a=e.input.charCodeAt(e.position);if(a!==33)return!1;e.tag!==null&&F(e,`duplication of a tag property`),a=e.input.charCodeAt(++e.position),a===60?(t=!0,a=e.input.charCodeAt(++e.position)):a===33?(n=!0,r=`!!`,a=e.input.charCodeAt(++e.position)):r=`!`;let s=e.position;if(t){do a=e.input.charCodeAt(++e.position);while(a!==0&&a!==62);e.position<e.length?(i=e.input.slice(s,e.position),a=e.input.charCodeAt(++e.position)):F(e,`unexpected end of the stream within a verbatim tag`)}else{for(;a!==0&&!T(a);)a===33&&(n?F(e,`tag suffix cannot contain exclamation marks`):(r=e.input.slice(s-1,e.position+1),b.test(r)||F(e,`named tag handle cannot contain such characters`),n=!0,s=e.position+1)),a=e.input.charCodeAt(++e.position);i=e.input.slice(s,e.position),y.test(i)&&F(e,`tag suffix cannot contain flow indicator characters`)}i&&!x.test(i)&&F(e,`tag name cannot contain such characters: `+i);try{i=decodeURIComponent(i)}catch(t){F(e,`tag name is malformed: `+i)}return t?e.tag=i:o.call(e.tagMap,r)?e.tag=e.tagMap[r]+i:r===`!`?e.tag=`!`+i:r===`!!`?e.tag=`tag:yaml.org,2002:`+i:F(e,`undeclared tag handle "`+r+`"`),!0}function ue(e){let t=e.input.charCodeAt(e.position);if(t!==38)return!1;e.anchor!==null&&F(e,`duplication of an anchor property`),t=e.input.charCodeAt(++e.position);let n=e.position;for(;t!==0&&!T(t)&&!E(t);)t=e.input.charCodeAt(++e.position);return e.position===n&&F(e,`name of an anchor node must contain at least one character`),e.anchor=e.input.slice(n,e.position),!0}function de(e){let t=e.input.charCodeAt(e.position);if(t!==42)return!1;t=e.input.charCodeAt(++e.position);let n=e.position;for(;t!==0&&!T(t)&&!E(t);)t=e.input.charCodeAt(++e.position);e.position===n&&F(e,`name of an alias node must contain at least one character`);let r=e.input.slice(n,e.position);return o.call(e.anchorMap,r)||F(e,`unidentified alias "`+r+`"`),e.result=e.anchorMap[r],q(e,!0,-1),!0}function fe(e,t,n,r){let i=B(e);return R(e),V(e,t),e.tag=null,e.anchor=null,e.kind=null,e.result=null,ce(e,n,r)&&e.kind===`mapping`?(re(e),!0):(z(e),V(e,i),!1)}function Q(e,t,n,r,i){let a,u,d=1,f=!1,m=!1,h=null,g,_,v;e.depth>=e.maxDepth&&F(e,`nesting exceeded maxDepth (`+e.maxDepth+`)`),e.depth+=1,e.listener!==null&&e.listener(`open`,e),e.tag=null,e.anchor=null,e.kind=null,e.result=null;let y=a=u=p===n||l===n;if(r&&q(e,!0,-1)&&(f=!0,e.lineIndent>t?d=1:e.lineIndent===t?d=0:e.lineIndent<t&&(d=-1)),d===1)for(;;){let n=e.input.charCodeAt(e.position),r=B(e);if(f&&(n===33&&e.tag!==null||n===38&&e.anchor!==null)||!le(e)&&!ue(e))break;h===nul Line truncated
`),e.charCodeAt(0)===65279&&(e=e.slice(1)));let n=new ne(e,t),r=e.indexOf(`\0`);for(r!==-1&&(n.position=r,F(n,`null byte is not allowed in input`)),n.input+=`\0`;n.input.charCodeAt(n.position)===32;)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)pe(n);return n.documents}function he(e,t,n){typeof t==`object`&&t&&n===void 0&&(n=t,t=null);let r=me(e,n);if(typeof t!=`function`)return r;for(let e=0,n=r.length;e<n;e+=1)t(r[e])}function ge(e,t){let n=me(e,t);if(n.length!==0){if(n.length===1)return n[0];throw new r(`expected a single document in the stream, but found more`)}}t.exports.loadAll=he,t.exports.load=ge})),te=s(((e,t)=>{var n=u(),r=d(),i=ee(),a=Object.prototype.toString,o=Object.prototype.hasOwnProperty,s=65279,c=9,l=10,f=13,p=32,m=33,h=34,g=35,_=37,v=38,y=39,b=42,x=44,S=45,C=58,w=61,T=62,E=63,D=64,O=91,k=93,A=96,j=123,te=124,M=125,N={};N[0]=`\\0`,N[7]=`\\a`,N[8]=`\\b`,N[9]=`\\t`,N[10]=`\\n`,N[11]=`\\v`,N[12]=`\\f`,N[13]=`\\r`,N[27]=`\\e`,N[34]=`\\"`,N[92]=`\\\\`,N[133]=`\\N`,N[160]=`\\_`,N[8232]=`\\L`,N[8233]=`\\P`;var ne=[`y`,`Y`,`yes`,`Yes`,`YES`,`on`,`On`,`ON`,`n`,`N`,`no`,`No`,`NO`,`off`,`Off`,`OFF`],P=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function F(e,t){if(t===null)return{};let n={},r=Object.keys(t);for(let i=0,a=r.length;i<a;i+=1){let a=r[i],s=String(t[a]);a.slice(0,2)===`!!`&&(a=`tag:yaml.org,2002:`+a.slice(2));let c=e.compiledTypeMap.fallback[a];c&&o.call(c.styleAliases,s)&&(s=c.styleAliases[s]),n[a]=s}return n}function I(e){let t,i,a=e.toString(16).toUpperCase();if(e<=255)t=`x`,i=2;else if(e<=65535)t=`u`,i=4;else if(e<=4294967295)t=`U`,i=8;else throw new r(`code point within a string may not be greater than 0xFFFFFFFF`);return`\\`+t+n.repeat(`0`,i-a.length)+a}var L=1,R=2;function re(e){this.schema=e.schema||i,this.indent=Math.max(1,e.indent||2),this.noArrayIndent=e.noArrayIndent||!1,this.skipInvalid=e.skipInvalid||!1,this.flowLevel=n.isNothing(e.flowLevel)?-1:e.flowLevel,this.styleMap=F(this.schema,e.styles||null),this.sortKeys=e.sortKeys||!1,this.lineWidth=e.lineWidth||80,this.noRefs=e.noRefs||!1,this.noCompatMode=e.noCompatMode||!1,this.condenseFlow=e.condenseFlow||!1,this.quotingType=e.quotingType===`"`?R:L,this.forceQuotes=e.forceQuotes||!1,this.replacer=typeof e.replacer==`function`?e.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result=``,this.duplicates=[],this.usedDuplicates=null}function z(e,t){let r=n.repeat(` `,t),i=0,a=``,o=e.length;for(;i<o;){let t,n=e.indexOf(`
`,i);n===-1?(t=e.slice(i),i=o):(t=e.slice(i,n+1),i=n+1),t.length&&t!==`
`&&(a+=r),a+=t}return a}function B(e,t){return`
`+n.repeat(` `,e.indent*t)}function V(e,t){for(let n=0,r=e.implicitTypes.length;n<r;n+=1)if(e.implicitTypes[n].resolve(t))return!0;return!1}function H(e){return e===p||e===c}function U(e){return e>=32&&e<=126||e>=161&&e<=55295&&e!==8232&&e!==8233||e>=57344&&e<=65533&&e!==s||e>=65536&&e<=1114111}function W(e){return U(e)&&e!==s&&e!==f&&e!==l}function G(e,t,n){let r=W(e),i=r&&!H(e);return(n?r:r&&e!==x&&e!==O&&e!==k&&e!==j&&e!==M)&&e!==g&&!(t===C&&!i)||W(t)&&!H(t)&&e===g||t===C&&i}function K(e){return U(e)&&e!==s&&!H(e)&&e!==S&&e!==E&&e!==C&&e!==x&&e!==O&&e!==k&&e!==j&&e!==M&&e!==g&&e!==v&&e!==b&&e!==m&&e!==te&&e!==w&&e!==T&&e!==y&&e!==h&&e!==_&&e!==D&&e!==A}function q(e){return!H(e)&&e!==C}function J(e,t){let n=e.charCodeAt(t),r;return n>=55296&&n<=56319&&t+1<e.length&&(r=e.charCodeAt(t+1),r>=56320&&r<=57343)?(n-55296)*1024+r-56320+65536:n}function Y(e){return/^\n* /.test(e)}var ie=1,X=2,ae=3,oe=4,Z=5;function se(e,t,n,r,i,a,o,s){let c,u=0,d=null,f=!1,p=!1,m=r!==-1,h=-1,g=K(J(e,0))&&q(J(e,e.length-1));if(t||o)for(c=0;c<e.length;u>=65536?c+=2:c++){if(u=J(e,c),!U(u))return Z;g=g&&G(u,d,s),d=u}else{for(c=0;c<e.length;u>=65536?c+=2:c++){if(u=J(e,c),u===l)f=!0,m&&(p=p||c-h-1>r&&e[h+1]!==` `,h=c);else if(!U(u))return Z;g=g&&G(u,d,s),d=u}p=p||m&&c-h-1>r&&e[h+1]!==` `}return!f&&!p?g&&!o&&!i(e)?ie:a===R?Z:X:n>9&&Y(e)?Z:o?a===R?Z:X:p?oe:ae}function ce(e,t,n,i,a){e.dump=function(){if(t.length===0)return e.quotingType===R?`""`:`''`;if(!e.noCompatMode&&(ne.indexOf(t)!==-1||P.test(t)))return e.quotingType===R?`"`+t+`"`:`'`+t+`'`;let o=e.indent*Math.max(1,n),s=e.lineWidth===-1?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-o),c=i||e.flowLevel>-1&&n>=e.flowLevel;function l(t){return V(e,t)}switch(se(t,c,e.indent,s,l,e.quotingType,e.forceQuotes&&!i,a)){case ie:return t;case X:return`'`+t.replace(/'/g,`''`)+`'`;case ae:return`|`+le(t,e.indent)+ue(z(t,o));case oe:return`>`+le(t,e.indent)+ue(z(de(t,s),o));case Z:return`"`+Q(t,s)+`"`;default:throw new r(`impossible error: invalid scalar style`)}}()}function le(e,t){let n=Y(e)?String(t):``,r=e[e.length-1]===`
`;return n+(r&&(e[e.length-2]===`
`||e===`
`)?`+`:r?``:`-`)+`
`}function ue(e){return e[e.length-1]===`
`?e.slice(0,-1):e}function de(e,t){let n=/(\n+)([^\n]*)/g,r=function(){let r=e.indexOf(`
`);return r=r===-1?e.length:r,n.lastIndex=r,fe(e.slice(0,r),t)}(),i=e[0]===`
`||e[0]===` `,a,o;for(;o=n.exec(e);){let e=o[1],n=o[2];a=n[0]===` `,r+=e+(!i&&!a&&n!==``?`
`:``)+fe(n,t),i=a}return r}function fe(e,t){if(e===``||e[0]===` `)return e;let n=/ [^ ]/g,r,i=0,a,o=0,s=0,c=``;for(;r=n.exec(e);)s=r.index,s-i>t&&(a=o>i?o:s,c+=`
`+e.slice(i,a),i=a+1),o=s;return c+=`
`,e.length-i>t&&o>i?c+=e.slice(i,o)+`
`+e.slice(o+1):c+=e.slice(i),c.slice(1)}function Q(e){let t=``,n=0;for(let r=0;r<e.length;n>=65536?r+=2:r++){n=J(e,r);let i=N[n];!i&&U(n)?(t+=e[r],n>=65536&&(t+=e[r+1])):t+=i||I(n)}return t}function pe(e,t,n){let r=``,i=e.tag;for(let i=0,a=n.length;i<a;i+=1){let a=n[i];e.replacer&&(a=e.replacer.call(n,String(i),a)),($(e,t,a,!1,!1)||a===void 0&&$(e,t,null,!1,!1))&&(r!==``&&(r+=`,`+(e.condenseFlow?``:` `)),r+=e.dump)}e.tag=i,e.dump=`[`+r+`]`}function me(e,t,n,r){let i=``,a=e.tag;for(let a=0,o=n.length;a<o;a+=1){let o=n[a];e.replacer&&(o=e.replacer.call(n,String(a),o)),($(e,t+1,o,!0,!0,!1,!0)||o===void 0&&$(e,t+1,null,!0,!0,!1,!0))&&((!r||i!==``)&&(i+=B(e,t)),e.dump&&l===e.dump.charCodeAt(0)?i+=`-`:i+=`- `,i+=e.dump)}e.tag=a,e.dump=i||`[]`}function he(e,t,n){let r=``,i=e.tag,a=Object.keys(n);for(let i=0,o=a.length;i<o;i+=1){let o=``;r!==``&&(o+=`, `),e.condenseFlow&&(o+=`"`);let s=a[i],c=n[s];e.replacer&&(c=e.replacer.call(n,s,c)),$(e,t,s,!1,!1)&&(e.dump.length>1024&&(o+=`? `),o+=e.dump+(e.condenseFlow?`"`:``)+`:`+(e.condenseFlow?``:` `),$(e,t,c,!1,!1)&&(o+=e.dump,r+=o))}e.tag=i,e.dump=`{`+r+`}`}function ge(e,t,n,i){let a=``,o=e.tag,s=Object.keys(n);if(e.sortKeys===!0)s.sort();else if(typeof e.sortKeys==`function`)s.sort(e.sortKeys);else if(e.sortKeys)throw new r(`sortKeys must be a boolean or a function`);for(let r=0,o=s.length;r<o;r+=1){let o=``;(!i||a!==``)&&(o+=B(e,t));let c=s[r],u=n[c];if(e.replacer&&(u=e.replacer.call(n,c,u)),!$(e,t+1,c,!0,!0,!0))continue;let d=e.tag!==null&&e.tag!==`?`||e.dump&&e.dump.length>1024;d&&(e.dump&&l===e.dump.charCodeAt(0)?o+=`?`:o+=`? `),o+=e.dump,d&&(o+=B(e,t)),$(e,t+1,u,!0,d)&&(e.dump&&l===e.dump.charCodeAt(0)?o+=`:`:o+=`: `,o+=e.dump,a+=o)}e.tag=o,e.dump=a||`{}`}function _e(e,t,n){let i=n?e.explicitTypes:e.implicitTypes;for(let s=0,c=i.length;s<c;s+=1){let c=i[s];if((c.instanceOf||c.predicate)&&(!c.instanceOf||typeof t==`object`&&t instanceof c.instanceOf)&&(!c.predicate||c.predicate(t))){if(n?c.multi&&c.representName?e.tag=c.representName(t):e.tag=c.tag:e.tag=`?`,c.represent){let n=e.styleMap[c.tag]||c.defaultStyle,i;if(a.call(c.represent)===`[object Function]`)i=c.represent(t,n);else if(o.call(c.represent,n))i=c.represent[n](t,n);else throw new r(`!<`+c.tag+`> tag resolver accepts not "`+n+`" style`);e.dump=i}return!0}}return!1}function $(e,t,n,i,o,s,c){e.tag=null,e.dump=n,_e(e,n,!1)||_e(e,n,!0);let l=a.call(e.dump),u=i;i&&(i=e.flowLevel<0||e.flowLevel>t);let d=l===`[object Object]`||l===`[object Array]`,f,p;if(d&&(f=e.duplicates.indexOf(n),p=f!==-1),(e.tag!==null&&e.tag!==`?`||p||e.indent!==2&&t>0)&&(o=!1),p&&e.usedDuplicates[f])e.dump=`*ref_`+f;else{if(d&&p&&!e.usedDuplicates[f]&&(e.usedDuplicates[f]=!0),l===`[object Object]`)i&&Object.keys(e.dump).length!==0?(ge(e,t,e.dump,o),p&&(e.dump=`&ref_`+f+e.dump)):(he(e,t,e.dump),p&&(e.dump=`&ref_`+f+` `+e.dump));else if(l===`[object Array]`)i&&e.dump.length!==0?(e.noArrayIndent&&!c&&t>0?me(e,t-1,e.dump,o):me(e,t,e.dump,o),p&&(e.dump=`&ref_`+f+e.dump)):(pe(e,t,e.dump),p&&(e.dump=`&ref_`+f+` `+e.dump));else if(l===`[object String]`)e.tag!==`?`&&ce(e,e.dump,t,s,u);else if(l===`[object Undefined]`)return!1;else{if(e.skipInvalid)return!1;throw new r(`unacceptable kind of an object to dump `+l)}if(e.tag!==null&&e.tag!==`?`){let t=encodeURI(e.tag[0]===`!`?e.tag.slice(1):e.tag).replace(/!/g,`%21`);t=e.tag[0]===`!`?`!`+t:t.slice(0,18)===`tag:yaml.org,2002:`?`!!`+t.slice(18):`!<`+t+`>`,e.dump=t+` `+e.dump}}return!0}function ve(e,t){let n=[],r=[];ye(e,n,r);let i=r.length;for(let e=0;e<i;e+=1)t.duplicates.push(n[r[e]]);t.usedDuplicates=Array(i)}function ye(e,t,n){if(typeof e==`object`&&e){let r=t.indexOf(e);if(r!==-1)n.indexOf(r)===-1&&n.push(r);else if(t.push(e),Array.isArray(e))for(let r=0,i=e.length;r<i;r+=1)ye(e[r],t,n);else{let r=Object.keys(e);for(let i=0,a=r.length;i<a;i+=1)ye(e[r[i]],t,n)}}}function be(e,t){t=t||{};let n=new re(t);n.noRefs||ve(e,n);let r=e;return n.replacer&&(r=n.replacer.call({"":r},``,r)),$(n,0,r,!0,!0)?n.dump+`
`:``}t.exports.dump=be})),M=l(s(((e,t)=>{var n=j(),r=te();function i(e,t){return function(){throw Error(`Function yaml.`+e+` is removed in js-yaml 4. Use yaml.`+t+` instead, which is now safe by default.`)}}t.exports.Type=p(),t.exports.Schema=m(),t.exports.FAILSAFE_SCHEMA=v(),t.exports.JSON_SCHEMA=C(),t.exports.CORE_SCHEMA=w(),t.exports.DEFAULT_SCHEMA=ee(),t.exports.load=n.load,t.exports.loadAll=n.loadAll,t.exports.dump=r.dump,t.exports.YAMLException=d(),t.exports.types={binary:D(),float:S(),map:_(),null:y(),pairs:k(),set:A(),timestamp:T(),bool:b(),int:x(),merge:E(),omap:O(),seq:g(),str:h()},t.exports.safeLoad=i(`safeLoad`,`load`),t.exports.safeLoadAll=i(`safeLoadAll`,`loadAll`),t.exports.safeDump=i(`safeDump`,`dump`)}))(),1),{Type:N,Schema:ne,FAILSAFE_SCHEMA:P,JSON_SCHEMA:F,CORE_SCHEMA:I,DEFAULT_SCHEMA:L,load:R,loadAll:re,dump:z,YAMLException:B,types:V,safeLoad:H,safeLoadAll:U,safeDump:W}=M.default,G=M.default;e.CORE_SCHEMA=I,e.DEFAULT_SCHEMA=L,e.FAILSAFE_SCHEMA=P,e.JSON_SCHEMA=F,e.Schema=ne,e.Type=N,e.YAMLException=B,e.default=G,e.dump=z,e.load=R,e.loadAll=re,e.safeDump=W,e.safeLoad=H,e.safeLoadAll=U,e.types=V});
//# sourceMappingURL=js-yaml.min.js.map
@@ -0,0 +1,392 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
'require fs';
'require poll';
'require view.daede.backend as backend';
function notifyAction() {}
function execChecked(command, args) {
return fs.exec(command, args || []).then(function(res) {
if (res && res.code !== 0)
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
return res;
});
}
function execInit(be, action) {
return fs.exec(be.initd, [action]).then(function(res) {
notifyAction(action, res);
}).catch(function() {});
}
function restartService(be) {
return execChecked(be.initd, ['restart'])
.then(function() { return waitForService(be, true, 80); });
}
function rejectIfOtherRunning(be, running) {
const other = be.name === 'dae' ? 'daed' : 'dae';
if (!running || !running[other])
return Promise.resolve();
return Promise.reject(new Error(_('%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment.').format(other, other, be.name)));
}
function toggleService(be, turnOn) {
const enabled = turnOn ? '1' : '0';
const action = turnOn ? 'start' : 'stop';
// Only guard on start: two backends share the eBPF/cgroup attachment, so
// starting one while the other runs is unsafe. Stopping is always allowed.
const guard = turnOn
? backend.detectRunning().then(function(running) { return rejectIfOtherRunning(be, running); })
: Promise.resolve();
return guard
.then(function() { return execChecked('/sbin/uci', ['set', be.uci + '.config.enabled=' + enabled]); })
.then(function() { return execChecked('/sbin/uci', ['commit', be.uci]); })
.then(function() {
if (turnOn)
return execChecked(be.initd, ['enable']);
return execChecked(be.initd, ['disable']);
})
.then(function() {
if (turnOn && be.useNetns)
return fs.exec('/sbin/ip', ['netns', 'del', 'daens']).catch(function() {});
})
.then(function() { return execChecked(be.initd, [action]); });
}
function waitForService(be, turnOn, attempts) {
return backend.serviceStatus(be.name).then(function(state) {
if (!!state.running === turnOn)
return state;
if (attempts <= 0)
throw new Error(turnOn ? _('Service did not start in time.') : _('Service did not stop in time.'));
return new Promise(function(resolve) { setTimeout(resolve, 250); })
.then(function() { return waitForService(be, turnOn, attempts - 1); });
});
}
function renderBackendSwitcher(ctx, onSwitched, initialHint) {
if (!ctx.installed.dae && !ctx.installed.daed)
return null;
const wrap = E('div', { 'class': 'dd-card dd-backend-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Backend')),
E('div', { 'class': 'dd-backend-row' }, [
E('span', { 'class': 'dd-backend-label' }, _('Active backend')),
E('div', { 'class': 'dd-backend-segment' })
])
]);
const segment = wrap.querySelector('.dd-backend-segment');
const hint = E('div', { 'class': 'dd-backend-help' }, initialHint || '');
let busy = false;
const showHint = function(msg) {
hint.textContent = msg;
};
const stopIfRunning = function(running) {
const stops = [];
['dae', 'daed'].forEach(function(name) {
if (running && running[name])
stops.push(execChecked(backend.BACKENDS[name].initd, ['stop'])
.then(function() { return waitForService(backend.BACKENDS[name], false, 40); }));
});
if (stops.length)
showHint(_('Stopping current backend…'));
return Promise.all(stops);
};
['daed', 'dae'].forEach(function(name) {
const active = ctx.name === name;
const installed = !!ctx.installed[name];
const btn = E('button', {
'class': 'dd-backend-btn' + (active ? ' is-active' : ''),
'type': 'button',
'disabled': null,
'title': installed ? _('Switch to %s').format(name) : _('%s is not installed').format(name)
}, [
E('span', {}, name),
E('span', { 'class': 'dd-backend-state' }, active ? _('Active') : (installed ? '' : _('Not installed')))
]);
btn.addEventListener('click', function(ev) {
ev.preventDefault();
if (busy || active)
return;
if (!installed) {
showHint(_('%s is not installed. Install it first, then switch backend.').format(name));
return;
}
busy = true;
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = true; });
showHint(_('Switching to %s…').format(name));
backend.detectRunning()
.then(stopIfRunning)
.then(function() { return backend.setActiveBackend(name); })
.then(function() {
const message = _('Switched to %s. Click start when you want it to run.').format(name);
showHint(message);
return onSwitched ? onSwitched(name, message) : null;
})
.catch(function(e) {
busy = false;
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = false; });
showHint(_('Switch failed.'));
});
});
segment.appendChild(btn);
});
wrap.appendChild(hint);
return wrap;
}
function renderStatusCard(ctx, listenAddr) {
const be = ctx.backend;
let busy = false;
let lastError = '';
let refreshGeneration = 0;
const probeState = {
running: false,
text: '',
kind: '',
visibleUntil: 0,
hideTimer: null
};
const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…')));
const card = E('div', { 'class': 'dd-card dd-status-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('Service Status')),
body
]);
const render = function(state) {
while (body.firstChild) body.removeChild(body.firstChild);
const badge = state.running
? E('span', { 'class': 'dd-badge dd-badge-run' }, [ E('span', { 'class': 'dd-badge-dot' }), _('RUNNING') ])
: E('span', { 'class': 'dd-badge dd-badge-stop' }, [ E('span', { 'class': 'dd-badge-dot' }), _('STOPPED') ]);
const meta = [
E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, _('Backend')), be.name ])
];
if (state.running && state.pid)
meta.push(E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, 'PID'), state.pid ]));
const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': lastError ? '' : 'display:none' }, lastError);
const sw = E('button', { 'class': 'dd-switch' + (state.running ? ' is-on' : ''), 'type': 'button', 'aria-label': _('Toggle service') }, [
E('span', { 'class': 'dd-switch-knob' })
]);
sw.addEventListener('click', function(ev) {
ev.preventDefault();
if (busy) return;
refreshGeneration++;
busy = true;
sw.disabled = true;
lastError = '';
swErr.style.display = 'none';
const turnOn = !state.running;
/* instant optimistic feedback — the start/stop chain (esp. dae's eBPF
load) takes a few seconds; flip the switch and show a pending label
right away instead of looking frozen */
sw.classList.toggle('is-on', turnOn);
const lbl = sw.parentNode && sw.parentNode.querySelector('.dd-switch-label');
if (lbl) lbl.textContent = '…';
toggleService(be, turnOn)
.then(function() { return waitForService(be, turnOn, 40); })
.then(function() {
busy = false;
return refresh(true);
})
.catch(function(e) {
busy = false;
lastError = _('Toggle failed: %s').format(e.message || e);
return refresh(true);
});
});
/* line 1: badge + toggle (always aligned, never wraps); line 2: meta */
const row = E('div', { 'class': 'dd-status-row' }, [
badge,
E('span', { 'class': 'dd-grow' }),
swErr,
E('span', { 'class': 'dd-switch-wrap' }, [
E('span', { 'class': 'dd-switch-label' }, state.running ? 'ON' : 'OFF'),
sw
])
]);
body.appendChild(row);
if (meta.length)
body.appendChild(E('div', { 'class': 'dd-status-meta' }, meta));
const actions = [];
if (be.hasWebUI && state.running) {
const port = (listenAddr || be.defaultListen).split(':').slice(-1)[0];
actions.push(E('a', {
'class': 'cbi-button cbi-button-action',
'href': 'http://%s:%s'.format(window.location.hostname, port),
'target': '_blank',
'rel': 'noreferrer noopener'
}, _('Open WebUI')));
}
if (state.running) {
const restart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Restart'));
restart.addEventListener('click', function(ev) {
ev.preventDefault();
if (busy) return;
busy = true;
restart.disabled = true;
lastError = '';
swErr.style.display = 'none';
restartService(be)
.then(function() {
busy = false;
return refresh(true);
})
.catch(function(e) {
busy = false;
lastError = _('Restart failed: %s').format(e.message || e);
return refresh(true);
});
});
actions.push(restart);
}
if (state.running && be.name === 'dae') {
const hot = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Hot Reload'));
hot.addEventListener('click', function(ev) {
ev.preventDefault();
hot.disabled = true;
execInit(be, 'hot_reload').finally(function() { hot.disabled = false; });
});
actions.push(hot);
}
if (state.running && be.name === 'dae') {
const ckBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Test YouTube'));
const probeVisible = probeState.running || probeState.visibleUntil > Date.now();
const ckRes = E('span', {
'class': 'dd-meta' + (probeState.kind ? ' ' + probeState.kind : ''),
style: 'margin-left:8px;' + (probeVisible ? '' : 'display:none')
}, probeState.text);
ckBtn.disabled = probeState.running;
ckBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (probeState.running)
return;
if (probeState.hideTimer) {
clearTimeout(probeState.hideTimer);
probeState.hideTimer = null;
}
probeState.running = true;
probeState.text = _('Testing…');
probeState.kind = '';
probeState.visibleUntil = 0;
ckBtn.disabled = true;
ckRes.style.display = 'inline';
ckRes.classList.remove('dd-ok', 'dd-err');
ckRes.textContent = probeState.text;
/* actually probe YouTube through dae's transparent proxy */
fs.exec('/usr/share/luci-app-daede/proxy-check.sh', []).then(function(r) {
const out = (r && r.stdout) || '';
const ok = /\bok=1\b/.test(out);
const code = (out.match(/code=(\S+)/) || [])[1] || '000';
const ms = parseInt((out.match(/ms=(\d+)/) || [])[1] || '0');
if (ok) {
probeState.kind = 'dd-ok';
probeState.text = _('YouTube reachable · %d ms').format(ms);
} else {
probeState.kind = 'dd-err';
probeState.text = (code === '000')
? _('YouTube unreachable (timeout)')
: _('YouTube unreachable (HTTP %s)').format(code);
}
}).catch(function() {
probeState.kind = 'dd-err';
probeState.text = _('YouTube unreachable (timeout)');
}).finally(function() {
probeState.running = false;
probeState.visibleUntil = Date.now() + 8000;
refresh(true);
probeState.hideTimer = setTimeout(function() {
probeState.text = '';
probeState.kind = '';
probeState.visibleUntil = 0;
probeState.hideTimer = null;
refresh(true);
}, 8000);
});
});
actions.push(ckBtn);
actions.push(ckRes);
}
if (actions.length)
body.appendChild(E('div', { 'class': 'dd-actions' }, actions));
};
const refresh = function(force) {
if (busy && !force)
return Promise.resolve();
const generation = refreshGeneration;
return backend.serviceStatus(be.name).then(function(state) {
if (generation !== refreshGeneration || (busy && !force)) return;
render(state);
});
};
poll.add(refresh);
refresh();
card._ddCleanup = function() {
poll.remove(refresh);
if (probeState.hideTimer)
clearTimeout(probeState.hideTimer);
};
return card;
}
function wrapSettingsCard(title, descr, mapPromise, advTitle, advFields) {
return mapPromise.then(function(node) {
const advBody = E('div', { 'class': 'dd-adv-body' });
const advWrap = E('div', { 'class': 'dd-adv dd-closed' }, [
E('div', { 'class': 'dd-adv-bar' }, [
E('span', {}, advTitle),
E('span', { 'class': 'dd-adv-chevron' }, '›')
]),
advBody
]);
advWrap.firstChild.addEventListener('click', function() {
advWrap.classList.toggle('dd-closed');
});
(advFields || []).forEach(function(name) {
const row = node.querySelector('[data-name="' + name + '"]');
if (row) advBody.appendChild(row);
});
const host = node.querySelector('.cbi-section') || node;
if (advBody.firstChild) host.appendChild(advWrap);
const children = [ E('h4', { 'class': 'dd-card-title' }, title) ];
if (descr) children.push(E('div', { 'class': 'dd-settings-descr' }, descr));
children.push(node);
return E('div', { 'class': 'dd-card dd-settings-card' }, children);
});
}
return baseclass.extend({
renderStatusCard: renderStatusCard,
renderBackendSwitcher: renderBackendSwitcher,
wrapSettingsCard: wrapSettingsCard
});