mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-14 12:18:06 +08:00
update 2026-10-11 22:01:38
This commit is contained in:
1 parent
ebe57dfba9
commit
6f8f22eee4
199 files changed
+8565
-41418
No files matched your search
@@ -0,0 +1,235 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require rpc';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
'require view.honk.converter as converter';
|
||||
|
||||
const DAE_UPLOAD_PATH = '/tmp/honk-v2-upload/import.dae';
|
||||
const DAE_LIMIT = 2 * 1024 * 1024;
|
||||
const PASTE_LIMIT = 2 * 1024 * 1024;
|
||||
|
||||
function utf8Length(value) {
|
||||
return new TextEncoder().encode(value).length;
|
||||
}
|
||||
|
||||
function isHttpUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return (url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadYamlParser() {
|
||||
if (window.jsyaml && window.jsyaml.load)
|
||||
return Promise.resolve(window.jsyaml);
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
const script = document.createElement('script');
|
||||
script.src = L.resource('view/honk/vendor/js-yaml.min.js');
|
||||
script.onload = function() {
|
||||
if (window.jsyaml && window.jsyaml.load)
|
||||
resolve(window.jsyaml);
|
||||
else
|
||||
reject(new Error(_('YAML parser failed to load')));
|
||||
};
|
||||
script.onerror = function() { reject(new Error(_('YAML parser failed to load'))); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function parsePasted(content) {
|
||||
let text = String(content || '').trim();
|
||||
if (!text)
|
||||
return Promise.resolve({ links: [], rejected: 1 });
|
||||
|
||||
if (!converter.looksLikeNodeList(text)) {
|
||||
const decoded = converter.tryBase64Decode(text);
|
||||
if (decoded && (converter.looksLikeNodeList(decoded) ||
|
||||
converter.looksLikeClashYaml(decoded) || converter.looksLikeSurge(decoded)))
|
||||
text = decoded;
|
||||
}
|
||||
|
||||
if (converter.looksLikeMihomoJson(text))
|
||||
return Promise.resolve(converter.parseMihomoJson(text));
|
||||
|
||||
if (converter.looksLikeClashYaml(text)) {
|
||||
return loadYamlParser().then(function(yaml) {
|
||||
const doc = yaml.load(text);
|
||||
const proxies = (doc && doc.proxies) || [];
|
||||
const links = [];
|
||||
let rejected = 0;
|
||||
proxies.forEach(function(node) {
|
||||
if (converter.isMetadataProxy(node)) {
|
||||
rejected++;
|
||||
return;
|
||||
}
|
||||
const result = converter.convertProxy(node);
|
||||
if (result.ok) links.push(result.link);
|
||||
else rejected++;
|
||||
});
|
||||
return { links: links, rejected: rejected };
|
||||
});
|
||||
}
|
||||
|
||||
if (converter.looksLikeSurge(text))
|
||||
return Promise.resolve(converter.parseSurgeProxies(text));
|
||||
|
||||
return Promise.resolve(converter.parseUriList(text));
|
||||
}
|
||||
|
||||
function uploadDae(file) {
|
||||
const form = new FormData();
|
||||
form.append('sessionid', rpc.getSessionID());
|
||||
form.append('filename', DAE_UPLOAD_PATH);
|
||||
form.append('filedata', file, file.name);
|
||||
return fetch(L.env.cgi_base + '/cgi-upload', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: form
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(result) {
|
||||
if (!result || result.failure)
|
||||
throw new Error('upload_failed');
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function() {
|
||||
honk.installStyles();
|
||||
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
page.appendChild(E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Configuration')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Honk reads its nodes, subscriptions and routing from /etc/honk/config.dae.'))
|
||||
]));
|
||||
|
||||
/* --- Add subscription --- */
|
||||
const subStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
|
||||
const nameInput = E('input', {
|
||||
'class': 'cbi-input-text', 'name': 'sub_name',
|
||||
'placeholder': _('Name, e.g. MyAirport'), 'maxlength': '256'
|
||||
});
|
||||
const urlInput = E('input', {
|
||||
'class': 'cbi-input-text', 'name': 'sub_url',
|
||||
'placeholder': 'https://…', 'inputmode': 'url'
|
||||
});
|
||||
const subBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Add subscription') ]);
|
||||
|
||||
subBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
const name = nameInput.value.trim();
|
||||
const url = urlInput.value.trim();
|
||||
if (!name) { subStatus.textContent = _('Name is required.'); nameInput.focus(); return; }
|
||||
if (!isHttpUrl(url)) { subStatus.textContent = _('A valid HTTP or HTTPS URL is required.'); urlInput.focus(); return; }
|
||||
subBtn.disabled = true;
|
||||
subStatus.textContent = _('Adding subscription…');
|
||||
honk.addSubscription(name, url).then(honk.ensureOk).then(function(result) {
|
||||
subStatus.textContent = honk.resultMessage(result, _('Subscription added and Honk reloaded.'));
|
||||
nameInput.value = '';
|
||||
urlInput.value = '';
|
||||
}).catch(function(error) {
|
||||
subStatus.textContent = honk.errorMessage(error, _('Could not add the subscription.'));
|
||||
}).finally(function() { subBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const subSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Add subscription')),
|
||||
E('p', { 'class': 'honk-note' }, _('Enter a name and the subscription URL. Honk writes it into /etc/honk/config.dae, fetches the nodes and reloads.')),
|
||||
E('div', { 'class': 'honk-subscription-add' }, [ nameInput, urlInput, subBtn ]),
|
||||
subStatus
|
||||
]);
|
||||
|
||||
/* --- Paste nodes --- */
|
||||
const pasteStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
|
||||
const pasteInput = E('textarea', {
|
||||
'class': 'cbi-input-textarea', 'name': 'paste',
|
||||
'rows': '10', 'spellcheck': 'false',
|
||||
'placeholder': _('Paste Clash YAML, Surge INI, Base64 or share links.')
|
||||
});
|
||||
const pasteBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Import nodes') ]);
|
||||
|
||||
pasteBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
const content = pasteInput.value.trim();
|
||||
if (!content) { pasteStatus.textContent = _('Paste nodes or subscription content first.'); pasteInput.focus(); return; }
|
||||
if (utf8Length(content) > PASTE_LIMIT) { pasteStatus.textContent = _('The pasted content is too large.'); return; }
|
||||
pasteBtn.disabled = true;
|
||||
pasteStatus.textContent = _('Parsing nodes…');
|
||||
parsePasted(content).then(function(parsed) {
|
||||
const links = (parsed && parsed.links) || [];
|
||||
if (!links.length) {
|
||||
pasteStatus.textContent = _('No nodes could be parsed from the input.');
|
||||
return;
|
||||
}
|
||||
pasteStatus.textContent = _('Importing %d node(s)…').format(links.length);
|
||||
return honk.addNodes(links.join('\n')).then(honk.ensureOk).then(function(result) {
|
||||
pasteStatus.textContent = honk.resultMessage(result, _('%d node(s) imported.').format(links.length));
|
||||
pasteInput.value = '';
|
||||
});
|
||||
}).catch(function(error) {
|
||||
pasteStatus.textContent = honk.errorMessage(error, _('Could not import nodes.'));
|
||||
}).finally(function() { pasteBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const pasteSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Paste nodes')),
|
||||
E('p', { 'class': 'honk-note' }, _('Paste Clash YAML, Surge INI, Base64 or share links; they are parsed locally into nodes and written to /etc/honk/config.dae.')),
|
||||
pasteInput,
|
||||
E('div', { 'class': 'honk-actions' }, [ pasteBtn ]),
|
||||
pasteStatus
|
||||
]);
|
||||
|
||||
/* --- Import dae config --- */
|
||||
const daeStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
|
||||
const daeInput = E('input', { 'type': 'file', 'name': 'dae', 'accept': '.dae,text/plain' });
|
||||
const daeBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Import configuration') ]);
|
||||
|
||||
daeBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
const file = daeInput.files && daeInput.files[0];
|
||||
if (!file) { daeStatus.textContent = _('Choose a .dae file first.'); return; }
|
||||
if (file.size > DAE_LIMIT) { daeStatus.textContent = _('The dae file exceeds the 2 MiB limit.'); return; }
|
||||
daeBtn.disabled = true;
|
||||
daeStatus.textContent = _('Uploading configuration…');
|
||||
uploadDae(file).then(function() {
|
||||
daeStatus.textContent = _('Importing configuration…');
|
||||
return honk.importDae().then(honk.ensureOk);
|
||||
}).then(function(result) {
|
||||
daeStatus.textContent = honk.resultMessage(result, _('Configuration imported and Honk reloaded.'));
|
||||
daeInput.value = '';
|
||||
}).catch(function(error) {
|
||||
daeStatus.textContent = honk.errorMessage(error, _('Could not import the configuration.'));
|
||||
}).finally(function() { daeBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const daeSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Import configuration')),
|
||||
E('p', { 'class': 'honk-note' }, _('Import a native dae config.dae (e.g. exported from Tower). Its node block is merged into /etc/honk/config.dae.')),
|
||||
E('div', { 'class': 'honk-subscription-add' }, [ daeInput, daeBtn ]),
|
||||
daeStatus
|
||||
]);
|
||||
|
||||
/* --- Direct configuration note --- */
|
||||
const note = E('section', { 'class': 'honk-card' }, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Direct configuration')),
|
||||
E('p', { 'class': 'honk-note' }, _('Routing, DNS and groups are managed in the Doona dashboard once Honk is started. You can also edit /etc/honk/config.dae directly.'))
|
||||
]);
|
||||
|
||||
page.appendChild(subSection);
|
||||
page.appendChild(pasteSection);
|
||||
page.appendChild(daeSection);
|
||||
page.appendChild(note);
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,501 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'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);
|
||||
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 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 {
|
||||
ok: true,
|
||||
name: String(node.name || 'Node'),
|
||||
type: type,
|
||||
link: converters[type](node),
|
||||
error: ''
|
||||
};
|
||||
} 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) : [];
|
||||
}
|
||||
|
||||
// --- Paste-import helpers -------------------------------------------------
|
||||
|
||||
const NODE_SCHEMES = /^(ss|ssr|vmess|vless|trojan|tuic|hysteria2?|hy2|anytls|socks5|socks4|juicity|snell):\/\//i;
|
||||
|
||||
function looksLikeNodeList(text) {
|
||||
const lines = String(text || '').split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line && NODE_SCHEMES.test(line))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function looksLikeClashYaml(text) {
|
||||
return /\bproxies\s*:/.test(String(text || ''));
|
||||
}
|
||||
|
||||
function looksLikeSurge(text) {
|
||||
return /^\s*\[Proxy\]\s*$/m.test(String(text || ''));
|
||||
}
|
||||
|
||||
function tryBase64Decode(text) {
|
||||
const compact = String(text || '').replace(/\s+/g, '');
|
||||
if (compact.length < 4 || !/^[A-Za-z0-9+/_-]+={0,2}$/.test(compact))
|
||||
return '';
|
||||
let normalized = compact.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (normalized.length % 4)
|
||||
normalized += '=';
|
||||
try {
|
||||
return utf8FromBase64(normalized);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function parseUriList(text) {
|
||||
const links = [];
|
||||
let rejected = 0;
|
||||
String(text || '').split('\n').forEach(function(raw) {
|
||||
const line = raw.trim();
|
||||
if (!line || line[0] === '#' || line[0] === ';' || line.indexOf('//') === 0)
|
||||
return;
|
||||
if (NODE_SCHEMES.test(line))
|
||||
links.push(line);
|
||||
else
|
||||
rejected++;
|
||||
});
|
||||
return { links: links, rejected: rejected };
|
||||
}
|
||||
|
||||
function splitSurgeFields(value) {
|
||||
const fields = [];
|
||||
let current = '';
|
||||
let quote = '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const ch = value[i];
|
||||
if (ch === '"' || ch === '\'') {
|
||||
if (quote === ch) quote = '';
|
||||
else if (quote === '') quote = ch;
|
||||
current += ch;
|
||||
} else if (ch === ',' && quote === '') {
|
||||
fields.push(surgeScalar(current));
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
fields.push(surgeScalar(current));
|
||||
return fields;
|
||||
}
|
||||
|
||||
function surgeScalar(value) {
|
||||
let t = String(value || '').trim();
|
||||
if (t.length >= 2 && (t[0] === '"' || t[0] === '\'') && t[0] === t[t.length - 1])
|
||||
t = t.slice(1, -1);
|
||||
try { return decodeURIComponent(t); } catch (e) { return t; }
|
||||
}
|
||||
|
||||
function firstNonEmpty() {
|
||||
for (let i = 0; i < arguments.length; i++)
|
||||
if (arguments[i] !== undefined && arguments[i] !== null && arguments[i] !== '')
|
||||
return arguments[i];
|
||||
return '';
|
||||
}
|
||||
|
||||
// Parse a Surge/Shadowrocket [Proxy] section. Only Shadowsocks lines are
|
||||
// understood, matching the upstream parser; other protocols are rejected so an
|
||||
// ordinary URL in the file is never mistaken for a node.
|
||||
function parseSurgeProxies(text) {
|
||||
const links = [];
|
||||
let rejected = 0;
|
||||
let inside = false;
|
||||
String(text || '').split('\n').forEach(function(raw) {
|
||||
const line = raw.trim();
|
||||
if (!line || line[0] === '#' || line[0] === ';' || line.indexOf('//') === 0)
|
||||
return;
|
||||
if (line[0] === '[' && line[line.length - 1] === ']' && line.indexOf('=') < 0) {
|
||||
inside = line.toLowerCase() === '[proxy]';
|
||||
return;
|
||||
}
|
||||
if (!inside)
|
||||
return;
|
||||
const sep = line.indexOf('=');
|
||||
if (sep < 0)
|
||||
return;
|
||||
const name = line.slice(0, sep).trim();
|
||||
const fields = splitSurgeFields(line.slice(sep + 1));
|
||||
if (!name || fields.length < 3 || fields[0].toLowerCase() !== 'ss' || !fields[1]) {
|
||||
rejected++;
|
||||
return;
|
||||
}
|
||||
const port = Number(fields[2]);
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
rejected++;
|
||||
return;
|
||||
}
|
||||
const options = {};
|
||||
for (let i = 3; i < fields.length; i++) {
|
||||
const eq = fields[i].indexOf('=');
|
||||
if (eq < 0)
|
||||
continue;
|
||||
options[fields[i].slice(0, eq).trim().toLowerCase()] = fields[i].slice(eq + 1).trim();
|
||||
}
|
||||
const node = {
|
||||
name: name,
|
||||
type: 'ss',
|
||||
server: fields[1],
|
||||
port: port,
|
||||
cipher: firstNonEmpty(options['encrypt-method'], options.method, options.cipher),
|
||||
password: options.password,
|
||||
plugin: options.plugin || options['obfs'],
|
||||
'plugin-opts': {}
|
||||
};
|
||||
if (node.plugin) {
|
||||
node['plugin-opts'].mode = options['obfs'] || 'http';
|
||||
if (options['obfs-host'])
|
||||
node['plugin-opts'].host = options['obfs-host'];
|
||||
}
|
||||
if (!node.cipher || !node.password) {
|
||||
rejected++;
|
||||
return;
|
||||
}
|
||||
const result = convertProxy(node);
|
||||
if (result.ok) links.push(result.link);
|
||||
else rejected++;
|
||||
});
|
||||
if (!links.length && !rejected)
|
||||
rejected = 1;
|
||||
return { links: links, rejected: rejected };
|
||||
}
|
||||
|
||||
// Mihomo/mihomo-format JSON keeps nodes in `outbounds` and names them with
|
||||
// `tag` instead of `name`, with nested `tls`/`transport` objects. Normalize to
|
||||
// the Clash shape so the existing convertProxy converters work unchanged.
|
||||
function looksLikeMihomoJson(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || t[0] !== '{')
|
||||
return false;
|
||||
try {
|
||||
const doc = JSON.parse(t);
|
||||
return !!(doc && Array.isArray(doc.outbounds));
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMihomoNode(node) {
|
||||
const n = Object.assign({}, node);
|
||||
if (node.tag !== undefined && node.name === undefined) n.name = node.tag;
|
||||
if (node.server_port !== undefined && node.port === undefined) n.port = node.server_port;
|
||||
|
||||
const tls = node.tls;
|
||||
if (tls && typeof tls === 'object') {
|
||||
if (tls.server_name !== undefined && n.servername === undefined) n.servername = tls.server_name;
|
||||
if (tls.insecure && n['skip-cert-verify'] === undefined) n['skip-cert-verify'] = true;
|
||||
if (Array.isArray(tls.alpn) && n.alpn === undefined) n.alpn = tls.alpn;
|
||||
if (tls.utls && tls.utls.fingerprint !== undefined && n['client-fingerprint'] === undefined)
|
||||
n['client-fingerprint'] = tls.utls.fingerprint;
|
||||
if (tls.reality && typeof tls.reality === 'object') {
|
||||
if (!n['reality-opts']) n['reality-opts'] = {};
|
||||
if (tls.reality.public_key !== undefined) n['reality-opts']['public-key'] = tls.reality.public_key;
|
||||
if (tls.reality.short_id !== undefined) n['reality-opts']['short-id'] = tls.reality.short_id;
|
||||
}
|
||||
}
|
||||
|
||||
const transport = node.transport;
|
||||
if (transport && typeof transport === 'object') {
|
||||
if (transport.type && n.network === undefined) n.network = transport.type;
|
||||
if (transport.type === 'ws' && transport.path && !n['ws-opts']) {
|
||||
n['ws-opts'] = { path: transport.path };
|
||||
if (transport.headers && transport.headers.Host)
|
||||
n['ws-opts'].headers = { Host: transport.headers.Host };
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function parseMihomoJson(text) {
|
||||
let doc;
|
||||
try {
|
||||
doc = JSON.parse(String(text || ''));
|
||||
}
|
||||
catch (e) {
|
||||
return { links: [], rejected: 1 };
|
||||
}
|
||||
const outbounds = (doc && doc.outbounds) || [];
|
||||
const links = [];
|
||||
let rejected = 0;
|
||||
outbounds.forEach(function(node) {
|
||||
if (isMetadataProxy(node)) {
|
||||
rejected++;
|
||||
return;
|
||||
}
|
||||
const result = convertProxy(normalizeMihomoNode(node));
|
||||
if (result.ok) links.push(result.link);
|
||||
else rejected++;
|
||||
});
|
||||
return { links: links, rejected: rejected };
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
convertProxy: convertProxy,
|
||||
convertProxies: convertProxies,
|
||||
isMetadataProxy: isMetadataProxy,
|
||||
looksLikeNodeList: looksLikeNodeList,
|
||||
looksLikeClashYaml: looksLikeClashYaml,
|
||||
looksLikeSurge: looksLikeSurge,
|
||||
looksLikeMihomoJson: looksLikeMihomoJson,
|
||||
tryBase64Decode: tryBase64Decode,
|
||||
parseUriList: parseUriList,
|
||||
parseSurgeProxies: parseSurgeProxies,
|
||||
parseMihomoJson: parseMihomoJson,
|
||||
normalizeMihomoNode: normalizeMihomoNode
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return honk.status().then(function(status) {
|
||||
return { status: status };
|
||||
}).catch(function() {
|
||||
return { error: true };
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
honk.installStyles();
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
|
||||
function renderInitialized(status) {
|
||||
const section = E('div', { 'class': 'honk-card' });
|
||||
const webUi = honk.localWebUi(status);
|
||||
|
||||
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Doona')));
|
||||
|
||||
if (webUi) {
|
||||
section.appendChild(E('a', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'href': webUi,
|
||||
'target': '_blank',
|
||||
'rel': 'noopener noreferrer'
|
||||
}, _('Open Doona')));
|
||||
}
|
||||
else if (!status.running) {
|
||||
section.appendChild(E('p', {}, _('Honk is stopped. Start it from the overview to open Doona.')));
|
||||
section.appendChild(E('a', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'href': L.url('admin/services/honk/overview')
|
||||
}, _('Go to overview')));
|
||||
}
|
||||
else {
|
||||
section.appendChild(E('p', {}, _('Doona is not ready yet. Check the overview for status.')));
|
||||
section.appendChild(E('a', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'href': L.url('admin/services/honk/overview')
|
||||
}, _('Go to overview')));
|
||||
}
|
||||
|
||||
page.appendChild(section);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
page.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
|
||||
return page;
|
||||
}
|
||||
|
||||
if (data.status.initialized === true) {
|
||||
renderInitialized(data.status);
|
||||
return page;
|
||||
}
|
||||
|
||||
const form = E('form', { 'class': 'honk-card' });
|
||||
const username = E('input', {
|
||||
'type': 'text',
|
||||
'name': 'username',
|
||||
'autocomplete': 'username',
|
||||
'class': 'cbi-input-text'
|
||||
});
|
||||
const password = E('input', {
|
||||
'type': 'password',
|
||||
'name': 'password',
|
||||
'autocomplete': 'new-password',
|
||||
'class': 'cbi-input-password'
|
||||
});
|
||||
const message = E('p', { 'role': 'status' }, '');
|
||||
const submit = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Initialize Honk'));
|
||||
|
||||
form.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Initial setup')));
|
||||
form.appendChild(E('p', {}, _('Create the Honk administrator account. The password is sent only for this request and is not saved by this page.')));
|
||||
form.appendChild(E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': 'honk-init-username' }, _('Username')),
|
||||
E('div', { 'class': 'cbi-value-field' }, username)
|
||||
]));
|
||||
username.id = 'honk-init-username';
|
||||
form.appendChild(E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': 'honk-init-password' }, _('Password')),
|
||||
E('div', { 'class': 'cbi-value-field' }, password)
|
||||
]));
|
||||
password.id = 'honk-init-password';
|
||||
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, submit));
|
||||
form.appendChild(message);
|
||||
form.addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
const user = username.value.trim();
|
||||
const secret = password.value;
|
||||
if (!user || !secret) {
|
||||
message.textContent = _('Enter both a username and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
submit.disabled = true;
|
||||
message.textContent = _('Initializing…');
|
||||
honk.initialize(user, secret).then(honk.ensureOk).then(function(result) {
|
||||
username.value = '';
|
||||
password.value = '';
|
||||
message.textContent = honk.resultMessage(result, _('Honk initialized.'));
|
||||
form.remove();
|
||||
page.appendChild(E('p', { 'role': 'status' }, _('Refreshing status…')));
|
||||
return honk.status().then(function(status) {
|
||||
while (page.firstChild)
|
||||
page.removeChild(page.firstChild);
|
||||
renderInitialized(status);
|
||||
}).catch(function() {
|
||||
while (page.firstChild)
|
||||
page.removeChild(page.firstChild);
|
||||
page.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
|
||||
});
|
||||
}).catch(function() {
|
||||
password.value = '';
|
||||
message.textContent = _('Initialization failed. Check the system status.');
|
||||
}).finally(function() {
|
||||
submit.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
page.appendChild(form);
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,915 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
|
||||
/* Honk — Apple-style design system.
|
||||
Cards, pill badges, iOS segmented controls and soft-tinted buttons on a
|
||||
translucent surface, so the UI adapts to every LuCI theme (light or dark). */
|
||||
|
||||
.honk-page {
|
||||
/* Accent — Apple system blue */
|
||||
--hk-accent-rgb: 0, 122, 255;
|
||||
--hk-accent: rgba(var(--hk-accent-rgb), 0.9);
|
||||
--hk-accent-strong: rgba(var(--hk-accent-rgb), 1);
|
||||
--hk-accent-border: rgba(var(--hk-accent-rgb), 0.35);
|
||||
--hk-accent-soft: rgba(var(--hk-accent-rgb), 0.09);
|
||||
--hk-accent-soft-hover: rgba(var(--hk-accent-rgb), 0.16);
|
||||
|
||||
/* Status — Apple system green / red */
|
||||
--hk-green: #34c759;
|
||||
--hk-red: #ff3b30;
|
||||
--hk-orange: #ff9500;
|
||||
|
||||
/* Surfaces (translucent → adapts to any theme) */
|
||||
--hk-card-bg: rgba(128, 128, 128, 0.04);
|
||||
--hk-card-border: rgba(128, 128, 128, 0.14);
|
||||
--hk-field-bg: rgba(128, 128, 128, 0.05);
|
||||
--hk-field-border: rgba(128, 128, 128, 0.22);
|
||||
--hk-hover: rgba(128, 128, 128, 0.08);
|
||||
--hk-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
--hk-shadow-hover: 0 8px 28px rgba(0, 0, 0, 0.08);
|
||||
--hk-muted: rgba(74, 86, 106, 0.62);
|
||||
|
||||
--hk-radius: 14px;
|
||||
--hk-radius-sm: 9px;
|
||||
--hk-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Helvetica Neue", "Microsoft YaHei", sans-serif;
|
||||
--hk-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", monospace;
|
||||
|
||||
font-family: var(--hk-font);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.honk-page,
|
||||
.honk-page * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* LuCI themes (Argon/Bootstrap) set #view>div:first-child and .cbi-map to flex;
|
||||
force block so our card/segmented-control layout behaves identically across
|
||||
themes (ID selector specificity requires !important). */
|
||||
.cbi-map.honk-page {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ---------- Hidden attribute guard ---------- */
|
||||
|
||||
.honk-page [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ---------- Page header ---------- */
|
||||
|
||||
.honk-header {
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
|
||||
.honk-header h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.honk-header-sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--hk-muted);
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
|
||||
.honk-page .cbi-section,
|
||||
.honk-page .cbi-section-node {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.honk-card {
|
||||
background: var(--hk-card-bg);
|
||||
border: 1px solid var(--hk-card-border);
|
||||
border-radius: var(--hk-radius);
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--hk-shadow);
|
||||
backdrop-filter: blur(6px);
|
||||
transition: box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.honk-card:hover {
|
||||
box-shadow: var(--hk-shadow-hover);
|
||||
}
|
||||
|
||||
.honk-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.honk-card-title {
|
||||
margin: 0 0 6px;
|
||||
padding: 0 !important;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.55;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.honk-card-head .honk-card-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* ---------- Status badge (pill) ---------- */
|
||||
|
||||
.honk-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 4px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honk-badge::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.honk-badge--run {
|
||||
color: var(--hk-green);
|
||||
border-color: rgba(52, 199, 89, 0.5);
|
||||
}
|
||||
|
||||
.honk-badge--stop {
|
||||
color: var(--hk-red);
|
||||
border-color: rgba(255, 59, 48, 0.55);
|
||||
}
|
||||
|
||||
.honk-badge--idle {
|
||||
color: var(--hk-muted);
|
||||
border-color: rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
|
||||
.honk-page .cbi-button {
|
||||
font-family: var(--hk-font);
|
||||
font-size: 13px !important;
|
||||
font-weight: 500;
|
||||
line-height: 1.35 !important;
|
||||
min-height: 0 !important;
|
||||
height: auto !important;
|
||||
padding: 7px 16px !important;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba(128, 128, 128, 0.32) !important;
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.18s ease, border-color 0.18s ease,
|
||||
opacity 0.18s ease, transform 0.12s ease;
|
||||
}
|
||||
|
||||
.honk-page .cbi-button:hover {
|
||||
background: var(--hk-hover) !important;
|
||||
}
|
||||
|
||||
.honk-page .cbi-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.honk-page .cbi-button:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Primary — soft Apple blue */
|
||||
.honk-page .cbi-button-action {
|
||||
border-color: var(--hk-accent-border) !important;
|
||||
color: var(--hk-accent) !important;
|
||||
background: var(--hk-accent-soft) !important;
|
||||
}
|
||||
|
||||
.honk-page .cbi-button-action:hover {
|
||||
background: var(--hk-accent-soft-hover) !important;
|
||||
border-color: var(--hk-accent) !important;
|
||||
}
|
||||
|
||||
/* Positive — soft green */
|
||||
.honk-page .cbi-button-positive {
|
||||
border-color: rgba(52, 199, 89, 0.5) !important;
|
||||
color: var(--hk-green) !important;
|
||||
background: rgba(52, 199, 89, 0.09) !important;
|
||||
}
|
||||
|
||||
.honk-page .cbi-button-positive:hover {
|
||||
background: rgba(52, 199, 89, 0.16) !important;
|
||||
border-color: var(--hk-green) !important;
|
||||
}
|
||||
|
||||
/* Negative — soft red */
|
||||
.honk-page .cbi-button-negative {
|
||||
border-color: rgba(255, 59, 48, 0.5) !important;
|
||||
color: var(--hk-red) !important;
|
||||
background: rgba(255, 59, 48, 0.08) !important;
|
||||
}
|
||||
|
||||
.honk-page .cbi-button-negative:hover {
|
||||
background: rgba(255, 59, 48, 0.15) !important;
|
||||
border-color: var(--hk-red) !important;
|
||||
}
|
||||
|
||||
/* ---------- Button groups ---------- */
|
||||
|
||||
.honk-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.honk-subscription-add {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.honk-subscription-add .cbi-input-text {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
/* ---------- Meta line (mono, muted) ---------- */
|
||||
|
||||
.honk-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
font-family: var(--hk-mono);
|
||||
font-size: 12px;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.honk-meta-label {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* ---------- Forms / fields ---------- */
|
||||
|
||||
.honk-page .cbi-value {
|
||||
padding: 9px 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.honk-page .cbi-value-title {
|
||||
float: none;
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0 0 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.honk-page .cbi-value-field {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.honk-page .cbi-value-description {
|
||||
font-size: 12px;
|
||||
color: var(--hk-muted);
|
||||
line-height: 1.5;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.honk-page .cbi-input-text,
|
||||
.honk-page .cbi-input-password,
|
||||
.honk-page .cbi-input-select,
|
||||
.honk-page .cbi-input-textarea,
|
||||
.honk-page select,
|
||||
.honk-page input[type="text"],
|
||||
.honk-page input[type="password"],
|
||||
.honk-page input[type="number"],
|
||||
.honk-page input[type="url"],
|
||||
.honk-page textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--hk-font);
|
||||
font-size: 13.5px;
|
||||
height: 38px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--hk-field-border);
|
||||
border-radius: var(--hk-radius-sm);
|
||||
background: var(--hk-field-bg);
|
||||
color: inherit;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease,
|
||||
background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.honk-page textarea,
|
||||
.honk-page .cbi-input-textarea {
|
||||
height: auto;
|
||||
min-height: 96px;
|
||||
resize: vertical;
|
||||
font-family: var(--hk-mono);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.honk-page .cbi-input-text:focus,
|
||||
.honk-page .cbi-input-password:focus,
|
||||
.honk-page .cbi-input-select:focus,
|
||||
.honk-page .cbi-input-textarea:focus,
|
||||
.honk-page select:focus,
|
||||
.honk-page input[type="text"]:focus,
|
||||
.honk-page input[type="password"]:focus,
|
||||
.honk-page input[type="number"]:focus,
|
||||
.honk-page input[type="url"]:focus,
|
||||
.honk-page textarea:focus {
|
||||
outline: 0;
|
||||
border-color: var(--hk-accent-border);
|
||||
box-shadow: 0 0 0 3px var(--hk-accent-soft);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.honk-page .cbi-input-text::placeholder,
|
||||
.honk-page textarea::placeholder,
|
||||
.honk-page input::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Comfortable field widths inside full-width cards */
|
||||
.honk-page .cbi-value-field .cbi-input-text,
|
||||
.honk-page .cbi-value-field .cbi-input-password,
|
||||
.honk-page .cbi-value-field input[type="number"],
|
||||
.honk-page .cbi-value-field input[type="url"],
|
||||
.honk-page .cbi-value-field input[type="text"],
|
||||
.honk-page .cbi-value-field .cbi-input-select,
|
||||
.honk-page .cbi-value-field select,
|
||||
.honk-page .cbi-value-field input[type="file"] {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.honk-page .cbi-value-field textarea,
|
||||
.honk-page .cbi-value-field .cbi-input-textarea {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
/* ---------- iOS toggle switch (boot-enabled checkbox) ---------- */
|
||||
|
||||
.honk-page input[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
position: relative;
|
||||
width: 46px;
|
||||
height: 27px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.honk-page input[type="checkbox"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.honk-page input[type="checkbox"]:checked {
|
||||
background: var(--hk-green);
|
||||
}
|
||||
|
||||
.honk-page input[type="checkbox"]:checked::before {
|
||||
transform: translateX(19px);
|
||||
}
|
||||
|
||||
.honk-page input[type="checkbox"]:focus-visible {
|
||||
outline: 2px solid var(--hk-accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.honk-page input[type="checkbox"]:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- Segmented control (iOS style) ---------- */
|
||||
|
||||
.honk-seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
background: rgba(128, 128, 128, 0.12);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.honk-seg-btn {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: var(--hk-font);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.18s ease, color 0.18s ease,
|
||||
opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.honk-seg-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.honk-seg-btn[aria-selected="true"],
|
||||
.honk-seg-btn.is-active {
|
||||
background: #fff;
|
||||
color: #1c1c1e;
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.honk-seg-btn:focus-visible {
|
||||
outline: 2px solid var(--hk-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ---------- Notes & alerts ---------- */
|
||||
|
||||
.honk-note {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--hk-muted);
|
||||
max-width: 72ch;
|
||||
}
|
||||
|
||||
.honk-page .alert-message {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--hk-radius-sm);
|
||||
background: rgba(255, 59, 48, 0.08);
|
||||
border: 1px solid rgba(255, 59, 48, 0.25);
|
||||
color: inherit;
|
||||
margin: 10px 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.honk-page .alert-message li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.honk-status-msg {
|
||||
font-size: 12.5px;
|
||||
color: var(--hk-muted);
|
||||
min-height: 1.4em;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.honk-status-msg:empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Disclosure (details/summary) ---------- */
|
||||
|
||||
.honk-page details {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.honk-page details > summary {
|
||||
min-height: 36px;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.honk-page details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.honk-page details > summary::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.2s ease;
|
||||
opacity: 0.6;
|
||||
flex: none;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.honk-page details[open] > summary::before {
|
||||
transform: rotate(45deg);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.honk-page details > summary:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.honk-disclosure-danger > summary {
|
||||
color: var(--hk-red);
|
||||
}
|
||||
|
||||
/* ---------- Logs ---------- */
|
||||
|
||||
.honk-log-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.honk-log-toolbar .honk-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.honk-log-toolbar label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: min(12em, 100%);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.honk-log-output {
|
||||
width: 100%;
|
||||
min-height: 380px;
|
||||
max-height: 62vh;
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--hk-radius-sm);
|
||||
border: 1px solid rgba(128, 128, 128, 0.22);
|
||||
background: #1a1d21;
|
||||
color: #d8dde6;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--hk-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-line {
|
||||
display: block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-ts {
|
||||
color: #6b7480;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl {
|
||||
display: inline-block;
|
||||
min-width: 40px;
|
||||
padding: 0 5px;
|
||||
margin-right: 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-align: center;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl-info {
|
||||
color: #7fc7a8;
|
||||
background: rgba(127, 199, 168, 0.08);
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl-warn {
|
||||
color: #e8b95a;
|
||||
background: rgba(232, 185, 90, 0.10);
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl-error {
|
||||
color: #ea7878;
|
||||
background: rgba(234, 120, 120, 0.10);
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl-debug {
|
||||
color: #7a8290;
|
||||
background: rgba(122, 130, 144, 0.10);
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-lvl-trace {
|
||||
color: #68717d;
|
||||
background: rgba(104, 113, 125, 0.10);
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-msg {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-error .honk-log-msg {
|
||||
color: #ea7878;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-warn .honk-log-msg {
|
||||
color: #e8b95a;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-info .honk-log-msg {
|
||||
color: #cfd6df;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-debug .honk-log-msg {
|
||||
color: #7a8290;
|
||||
}
|
||||
|
||||
.honk-log-output .honk-log-trace .honk-log-msg {
|
||||
color: #68717d;
|
||||
}
|
||||
|
||||
/* ---------- Package update rows ---------- */
|
||||
|
||||
.honk-pkg-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 130px minmax(0, 1fr) auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
font-size: 12.5px;
|
||||
padding: 9px 0;
|
||||
border-top: 1px dashed rgba(128, 128, 128, 0.18);
|
||||
}
|
||||
|
||||
.honk-pkg-row:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.honk-pkg-icon {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.honk-pkg-ok {
|
||||
color: var(--hk-green);
|
||||
}
|
||||
|
||||
.honk-pkg-err {
|
||||
color: var(--hk-red);
|
||||
}
|
||||
|
||||
.honk-pkg-new {
|
||||
color: var(--hk-accent);
|
||||
}
|
||||
|
||||
.honk-pkg-name {
|
||||
font-weight: 600;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.honk-pkg-meta {
|
||||
opacity: 0.7;
|
||||
font-family: var(--hk-mono);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honk-up-log {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--hk-field-border);
|
||||
border-radius: var(--hk-radius-sm);
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: var(--hk-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.honk-pkg-row {
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.honk-pkg-row .honk-pkg-icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.honk-pkg-row .honk-pkg-name {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.honk-pkg-row .honk-pkg-meta {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.honk-pkg-row > span:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.honk-pkg-row > button,
|
||||
.honk-pkg-row > span:last-child {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Page actions footer ---------- */
|
||||
|
||||
.honk-page .cbi-page-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 12px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.honk-page {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.honk-header h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.honk-card {
|
||||
padding: 15px 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.honk-card-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.honk-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.honk-actions .cbi-button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.honk-log-output {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Dark mode ---------- */
|
||||
|
||||
body.dark .honk-page,
|
||||
html.dark .honk-page,
|
||||
body[data-theme="dark"] .honk-page,
|
||||
html[data-theme="dark"] .honk-page,
|
||||
body[data-bs-theme="dark"] .honk-page,
|
||||
html[data-bs-theme="dark"] .honk-page,
|
||||
body[data-darkmode="1"] .honk-page,
|
||||
html[data-darkmode="1"] .honk-page,
|
||||
body[data-darkmode="true"] .honk-page,
|
||||
html[data-darkmode="true"] .honk-page {
|
||||
--hk-green: #30d158;
|
||||
--hk-red: #ff453a;
|
||||
--hk-orange: #ff9f0a;
|
||||
--hk-card-bg: rgba(255, 255, 255, 0.04);
|
||||
--hk-card-border: rgba(255, 255, 255, 0.1);
|
||||
--hk-field-bg: rgba(255, 255, 255, 0.05);
|
||||
--hk-field-border: rgba(255, 255, 255, 0.16);
|
||||
--hk-hover: rgba(255, 255, 255, 0.08);
|
||||
--hk-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
--hk-shadow-hover: 0 10px 32px rgba(0, 0, 0, 0.45);
|
||||
--hk-muted: rgba(200, 210, 224, 0.6);
|
||||
}
|
||||
|
||||
body.dark .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
html.dark .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
body[data-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
html[data-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
body[data-bs-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
html[data-bs-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
body[data-darkmode="1"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
html[data-darkmode="1"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
body[data-darkmode="true"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
html[data-darkmode="true"] .honk-page .honk-seg-btn[aria-selected="true"],
|
||||
body.dark .honk-page .honk-seg-btn.is-active,
|
||||
html.dark .honk-page .honk-seg-btn.is-active,
|
||||
body[data-theme="dark"] .honk-page .honk-seg-btn.is-active,
|
||||
html[data-theme="dark"] .honk-page .honk-seg-btn.is-active,
|
||||
body[data-bs-theme="dark"] .honk-page .honk-seg-btn.is-active,
|
||||
html[data-bs-theme="dark"] .honk-page .honk-seg-btn.is-active,
|
||||
body[data-darkmode="1"] .honk-page .honk-seg-btn.is-active,
|
||||
html[data-darkmode="1"] .honk-page .honk-seg-btn.is-active,
|
||||
body[data-darkmode="true"] .honk-page .honk-seg-btn.is-active,
|
||||
html[data-darkmode="true"] .honk-page .honk-seg-btn.is-active {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
body.dark .honk-page .honk-log-output,
|
||||
html.dark .honk-page .honk-log-output,
|
||||
body[data-theme="dark"] .honk-page .honk-log-output,
|
||||
html[data-theme="dark"] .honk-page .honk-log-output,
|
||||
body[data-bs-theme="dark"] .honk-page .honk-log-output,
|
||||
html[data-bs-theme="dark"] .honk-page .honk-log-output,
|
||||
body[data-darkmode="1"] .honk-page .honk-log-output,
|
||||
html[data-darkmode="1"] .honk-page .honk-log-output,
|
||||
body[data-darkmode="true"] .honk-page .honk-log-output,
|
||||
html[data-darkmode="true"] .honk-page .honk-log-output {
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
/* ---------- Reduced motion ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.honk-page .cbi-button,
|
||||
.honk-page .cbi-button:active,
|
||||
.honk-card,
|
||||
.honk-page input[type="checkbox"],
|
||||
.honk-page input[type="checkbox"]::before,
|
||||
.honk-page details > summary::before {
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
|
||||
function detectLogLevel(line) {
|
||||
// honk-core tracing output (e.g. " ERROR honk_core::...")
|
||||
if (/\s(?:ERROR|FATAL|PANIC)\s/.test(line)) return 'error';
|
||||
if (/\sWARN(?:ING)?\s/.test(line)) return 'warn';
|
||||
if (/\sDEBUG\s/.test(line)) return 'debug';
|
||||
if (/\sTRACE\s/.test(line)) return 'trace';
|
||||
if (/\sINFO\s/.test(line)) return 'info';
|
||||
// syslog facility.level (e.g. "daemon.err")
|
||||
if (/daemon\.(?:err|crit|alert|emerg)\b/.test(line)) return 'error';
|
||||
if (/daemon\.(?:warning|warn|notice)\b/.test(line)) return 'warn';
|
||||
if (/daemon\.debug\b/.test(line)) return 'debug';
|
||||
if (/daemon\.info\b/.test(line)) return 'info';
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatLogTs(raw) {
|
||||
// logread timestamp: "Wed Oct 7 18:58:09 2026" -> "2026-10-07 18:58:09"
|
||||
const m = String(raw || '').match(/^[A-Z][a-z]{2}\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\d{4})$/);
|
||||
if (!m) return raw;
|
||||
const months = { Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, Jul: 7, Aug: 8, Sep: 9, Oct: 10, Nov: 11, Dec: 12 };
|
||||
const month = months[m[1]];
|
||||
if (!month) return raw;
|
||||
return m[4] + '-' + String(month).padStart(2, '0') + '-' + String(Number(m[2])).padStart(2, '0') + ' ' + m[3];
|
||||
}
|
||||
|
||||
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 'honk-log-lvl-trace';
|
||||
if (u === 'DEBUG') return 'honk-log-lvl-debug';
|
||||
if (u === 'INFO') return 'honk-log-lvl-info';
|
||||
if (u === 'WARN' || u === 'WARNING') return 'honk-log-lvl-warn';
|
||||
return 'honk-log-lvl-error';
|
||||
}
|
||||
|
||||
function parseLogLine(line) {
|
||||
// "Wed Oct 7 18:58:09 2026 daemon.info honk-core[31377]: INFO honk_core::..."
|
||||
const m = line.match(/^([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+\S+\s+[^:]+:\s*(.*)$/);
|
||||
if (!m) return { ts: '', lvl: '', msg: line };
|
||||
let body = m[2] || '';
|
||||
const lvlMatch = body.match(/^\s*(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i);
|
||||
let lvl = '';
|
||||
if (lvlMatch) {
|
||||
lvl = lvlMatch[1].toUpperCase();
|
||||
body = lvlMatch[2];
|
||||
}
|
||||
return { ts: formatLogTs(m[1]), lvl: lvl, msg: body };
|
||||
}
|
||||
|
||||
function buildLogLine(line) {
|
||||
const level = detectLogLevel(line);
|
||||
const parsed = parseLogLine(line);
|
||||
const parts = [];
|
||||
if (parsed.ts)
|
||||
parts.push(E('span', { 'class': 'honk-log-ts' }, parsed.ts));
|
||||
if (parsed.lvl)
|
||||
parts.push(E('span', { 'class': 'honk-log-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
|
||||
parts.push(E('span', { 'class': 'honk-log-msg' }, parsed.msg));
|
||||
return E('span', { 'class': 'honk-log-line' + (level ? ' honk-log-' + level : '') }, parts);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
_timer: null,
|
||||
|
||||
handleUnload: function() {
|
||||
if (this._timer != null)
|
||||
clearInterval(this._timer);
|
||||
this._timer = null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
honk.installStyles();
|
||||
const source = E('select', { 'class': 'cbi-input-select' }, [
|
||||
E('option', { 'value': 'core' }, _('Core')),
|
||||
E('option', { 'value': 'service' }, _('Service')),
|
||||
E('option', { 'value': 'maintenance' }, _('Maintenance'))
|
||||
]);
|
||||
const level = E('select', { 'class': 'cbi-input-select' }, [
|
||||
E('option', { 'value': 'all' }, _('All levels')),
|
||||
E('option', { 'value': 'debug' }, _('Debug and above')),
|
||||
E('option', { 'value': 'info' }, _('Info and above')),
|
||||
E('option', { 'value': 'warn' }, _('Warnings and errors')),
|
||||
E('option', { 'value': 'error' }, _('Errors only'))
|
||||
]);
|
||||
const output = E('pre', {
|
||||
'class': 'honk-log-output',
|
||||
'aria-live': 'polite'
|
||||
}, _('Loading logs…'));
|
||||
const status = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const pause = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Pause'));
|
||||
const refresh = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Refresh now'));
|
||||
const download = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Download redacted log'));
|
||||
let paused = false;
|
||||
let pendingRequests = 0;
|
||||
let requestSequence = 0;
|
||||
let latest = [];
|
||||
download.disabled = true;
|
||||
|
||||
function loadLogs(force) {
|
||||
if (paused && !force || pendingRequests && !force)
|
||||
return Promise.resolve();
|
||||
|
||||
const requestId = ++requestSequence;
|
||||
const requestedSource = source.value;
|
||||
const requestedLevel = level.value;
|
||||
pendingRequests++;
|
||||
download.disabled = true;
|
||||
return honk.logs(requestedSource, requestedLevel, 200).then(honk.ensureOk).then(function(result) {
|
||||
if (requestId !== requestSequence)
|
||||
return;
|
||||
latest = Array.isArray(result.lines) ? result.lines.map(String) : [];
|
||||
output.replaceChildren();
|
||||
if (latest.length) {
|
||||
latest.forEach(function(line) {
|
||||
output.appendChild(buildLogLine(line));
|
||||
});
|
||||
}
|
||||
else {
|
||||
output.textContent = _('No matching Honk log entries.');
|
||||
}
|
||||
status.textContent = result.capped ? _('Output was capped; showing the latest %d entries.').format(latest.length) : _('Showing the latest %d matching entries.').format(latest.length);
|
||||
}).catch(function(error) {
|
||||
if (requestId === requestSequence)
|
||||
status.textContent = honk.errorMessage(error, _('Could not read Honk logs.'));
|
||||
}).finally(function() {
|
||||
pendingRequests--;
|
||||
download.disabled = pendingRequests > 0;
|
||||
});
|
||||
}
|
||||
|
||||
pause.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
paused = !paused;
|
||||
pause.textContent = paused ? _('Resume') : _('Pause');
|
||||
status.textContent = paused ? _('Log polling is paused.') : _('Log polling resumed.');
|
||||
if (!paused)
|
||||
loadLogs(true);
|
||||
});
|
||||
refresh.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
loadLogs(true);
|
||||
});
|
||||
source.addEventListener('change', function() { loadLogs(true); });
|
||||
level.addEventListener('change', function() { loadLogs(true); });
|
||||
download.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
const blob = new Blob([ latest.join('\n') + (latest.length ? '\n' : '') ], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = E('a', { 'href': url, 'download': 'honk-redacted.log' });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
});
|
||||
|
||||
source.id = 'honk-log-source';
|
||||
level.id = 'honk-log-level';
|
||||
this._timer = setInterval(function() { loadLogs(false); }, 5000);
|
||||
loadLogs(true);
|
||||
|
||||
return E('div', { 'class': 'cbi-map honk-page' }, [
|
||||
E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Logs')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Shows up to 200 Honk-specific lines, capped at 32 KiB, and refreshes every 5 seconds while live. Sensitive URLs and credentials are redacted before display.'))
|
||||
]),
|
||||
E('section', { 'class': 'honk-card' }, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Live log')),
|
||||
E('div', { 'class': 'honk-log-toolbar' }, [
|
||||
E('label', { 'for': source.id }, [ _('Log source'), source ]),
|
||||
E('label', { 'for': level.id }, [ _('Minimum level'), level ]),
|
||||
E('div', { 'class': 'honk-actions' }, [ pause, refresh, download ])
|
||||
]),
|
||||
status,
|
||||
output
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,535 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
|
||||
const BACKUP_PATH = '/tmp/honk-backup.tar.gz';
|
||||
const RESTORE_PATH = '/tmp/honk-maintenance/restore.tar.gz';
|
||||
const PKG_INFO = '/usr/share/luci-app-honk/pkg-info.sh';
|
||||
const UPDATE_PKG = '/usr/share/luci-app-honk/update-pkg.sh';
|
||||
const REFRESH_INDEX = '/usr/share/luci-app-honk/refresh-index.sh';
|
||||
const UPDATE_GEO = '/usr/share/luci-app-honk/update-geo.sh';
|
||||
const GEO_CRON = '/usr/share/luci-app-honk/geo-cron.sh';
|
||||
const PKGS = [ 'honk', 'luci-app-honk' ];
|
||||
const PKG_NAMES = { honk: 'Honk', 'luci-app-honk': 'luci-app-honk' };
|
||||
|
||||
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 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`. Returns <0 / 0 / >0.
|
||||
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;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return uci.load('honk').catch(function() {});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
honk.installStyles();
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
const recoveryMessage = E('p', { 'class': 'alert-message', 'role': 'status' }, '');
|
||||
recoveryMessage.hidden = true;
|
||||
const writeControls = [];
|
||||
let recoveryRequired = false;
|
||||
|
||||
function phaseMessage(job, fallback) {
|
||||
const messages = {
|
||||
preparing: _('Preparing maintenance…'),
|
||||
stopping: _('Stopping Honk…'),
|
||||
applying: _('Applying the staged data…'),
|
||||
validating: _('Validating the new state…'),
|
||||
starting: _('Starting Honk…'),
|
||||
rolling_back: _('Restoring the previous state…'),
|
||||
committing: _('Finishing the transaction…'),
|
||||
recovery: _('Recovery is required before another write operation.')
|
||||
};
|
||||
return messages[job.phase] || fallback;
|
||||
}
|
||||
|
||||
function blockWrites() {
|
||||
recoveryRequired = true;
|
||||
writeControls.forEach(function(button) { button.disabled = true; });
|
||||
}
|
||||
|
||||
/* Shared log pane and row helper for both data and package updates. */
|
||||
const updateLog = E('pre', { 'class': 'honk-up-log', 'hidden': true }, '');
|
||||
|
||||
function mkPkgRow(icon, iconCls, name, meta, btn) {
|
||||
return E('div', { 'class': 'honk-pkg-row' }, [
|
||||
E('span', { 'class': 'honk-pkg-icon ' + iconCls }, icon),
|
||||
E('span', { 'class': 'honk-pkg-name' }, name),
|
||||
E('span', { 'class': 'honk-pkg-meta', 'title': meta }, meta),
|
||||
E('span', {}, ''),
|
||||
btn || E('span', {}, '')
|
||||
]);
|
||||
}
|
||||
|
||||
function probeFile(path) {
|
||||
return fs.stat(path).then(function(st) {
|
||||
return { exists: true, size: st.size, mtime: st.mtime };
|
||||
}).catch(function() {
|
||||
return { exists: false, size: 0, mtime: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Geo data card --- */
|
||||
const dataSection = E('section', { 'class': 'honk-card' });
|
||||
const dataBody = E('div', { 'id': 'honk-geo' }, E('em', {}, _('Probing…')));
|
||||
|
||||
function updateGeo(kind, btn) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
let tries = 0;
|
||||
const pollLog = function() {
|
||||
return fs.read_direct('/tmp/luci-app-honk.' + kind + '.log', 'text').then(function(c) {
|
||||
if (c) {
|
||||
updateLog.textContent = c;
|
||||
updateLog.hidden = false;
|
||||
}
|
||||
if (/[✓✗]/.test(c)) { refreshData(); return; }
|
||||
if (tries++ > 90) return;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(pollLog);
|
||||
}).catch(function() {});
|
||||
};
|
||||
return fs.exec(UPDATE_GEO, [ kind ]).then(function(res) {
|
||||
if (res.code === 0) return pollLog();
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
return Promise.all([ probeFile(DATA_PATHS.geoip), probeFile(DATA_PATHS.geosite) ]).then(function(r) {
|
||||
while (dataBody.firstChild)
|
||||
dataBody.removeChild(dataBody.firstChild);
|
||||
[
|
||||
{ k: 'geoip', name: 'GeoIP', r: r[0] },
|
||||
{ k: 'geosite', name: 'GeoSite', r: r[1] }
|
||||
].forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Update'));
|
||||
btn.addEventListener('click', function(ev) { ev.preventDefault(); 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(mkPkgRow(
|
||||
entry.r.exists ? '✓' : '✗',
|
||||
entry.r.exists ? 'honk-pkg-ok' : 'honk-pkg-err',
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function geoField(label, input) {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
|
||||
E('div', { 'class': 'cbi-value-field' }, input)
|
||||
]);
|
||||
}
|
||||
|
||||
const geoSettings = (function() {
|
||||
const gi0 = uci.get('honk', 'main', 'geoip_url') || '';
|
||||
const gs0 = uci.get('honk', 'main', 'geosite_url') || '';
|
||||
const auto0 = uci.get('honk', 'main', 'geo_auto') === '1';
|
||||
const freq0 = uci.get('honk', 'main', 'geo_auto_freq') || 'daily';
|
||||
const preset0 = (!gi0 && !gs0) ? 'loyalsoldier' :
|
||||
(gi0 === GEO_PRESETS.v2fly.geoip && gs0 === GEO_PRESETS.v2fly.geosite) ? 'v2fly' : 'custom';
|
||||
|
||||
const presetSel = E('select', { 'class': 'cbi-input-select', 'id': 'honk-geo-preset' }, [
|
||||
E('option', { 'value': 'loyalsoldier' }, 'Loyalsoldier'),
|
||||
E('option', { 'value': 'v2fly' }, 'v2fly'),
|
||||
E('option', { 'value': 'custom' }, _('Custom'))
|
||||
]);
|
||||
presetSel.value = preset0;
|
||||
|
||||
const giInput = E('input', { 'class': 'cbi-input-text', 'type': 'text', 'id': 'honk-geo-ip', 'placeholder': 'https://…/geoip.dat' });
|
||||
const gsInput = E('input', { 'class': 'cbi-input-text', 'type': 'text', 'id': 'honk-geo-site', 'placeholder': 'https://…/geosite.dat' });
|
||||
giInput.value = preset0 === 'custom' ? gi0 : '';
|
||||
gsInput.value = preset0 === 'custom' ? gs0 : '';
|
||||
|
||||
const customRows = E('div', {}, [
|
||||
geoField(_('GeoIP URL'), giInput),
|
||||
geoField(_('GeoSite URL'), gsInput)
|
||||
]);
|
||||
const syncCustom = function() { customRows.style.display = presetSel.value === 'custom' ? '' : 'none'; };
|
||||
presetSel.addEventListener('change', syncCustom);
|
||||
syncCustom();
|
||||
|
||||
const autoSel = E('select', { 'class': 'cbi-input-select', 'id': 'honk-geo-auto' }, [
|
||||
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': 'cbi-button cbi-button-action', 'type': 'button' }, _('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();
|
||||
for (const [label, url] of [['GeoIP URL', gi], ['GeoSite URL', gs]]) {
|
||||
if (url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad protocol');
|
||||
} catch (e) {
|
||||
document.getElementById('honk-geo-msg').textContent = _('Invalid %s: must be an http(s) URL.').format(label);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uci.set('honk', 'main', 'geoip_url', gi);
|
||||
uci.set('honk', 'main', 'geosite_url', gs);
|
||||
uci.set('honk', 'main', 'geo_auto', geoAuto ? '1' : '0');
|
||||
uci.set('honk', 'main', 'geo_auto_freq', geoFreq);
|
||||
const orig = saveBtn.textContent;
|
||||
saveBtn.disabled = true; saveBtn.textContent = '…';
|
||||
uci.save().then(function() {
|
||||
return uci.changes();
|
||||
}).then(function(changes) {
|
||||
if (changes && Object.keys(changes).length)
|
||||
return uci.apply();
|
||||
}).then(function() {
|
||||
return fs.exec(GEO_CRON, [ geoAuto ? 'enable' : 'disable' ]);
|
||||
}).then(function() {
|
||||
updateLog.textContent = _('Geo data source saved.');
|
||||
updateLog.hidden = false;
|
||||
ui.changes.init();
|
||||
}).catch(function(e) {
|
||||
updateLog.textContent = _('Save failed') + ': ' + (e && e.message ? e.message : e);
|
||||
updateLog.hidden = false;
|
||||
}).finally(function() {
|
||||
saveBtn.disabled = false; saveBtn.textContent = orig;
|
||||
});
|
||||
});
|
||||
|
||||
return E('details', {}, [
|
||||
E('summary', {}, _('Data source and auto-update')),
|
||||
E('div', {}, [
|
||||
geoField(_('Source'), presetSel),
|
||||
customRows,
|
||||
geoField(_('Auto-update'), autoSel),
|
||||
E('div', { 'class': 'honk-actions' }, [ saveBtn ]),
|
||||
E('p', { 'class': 'honk-note', 'id': 'honk-geo-msg' })
|
||||
])
|
||||
]);
|
||||
})();
|
||||
|
||||
dataSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Geo data')));
|
||||
dataSection.appendChild(E('p', { 'class': 'honk-note' }, _('GeoIP and GeoSite routing data can be updated here. Updates run in the background and reload Honk when done.')));
|
||||
dataSection.appendChild(dataBody);
|
||||
dataSection.appendChild(geoSettings);
|
||||
|
||||
/* --- Software package updates card --- */
|
||||
const updateSection = E('section', { 'class': 'honk-card' });
|
||||
const pkgBody = E('div', { 'id': 'honk-pkg' }, E('em', {}, _('Probing…')));
|
||||
|
||||
function probePkg(pkg) {
|
||||
return fs.exec(PKG_INFO, [ pkg ]).then(function(res) {
|
||||
const out = (res.stdout || '').trim().split('\t');
|
||||
return { installed: out[0] || '', latest: out[1] || '' };
|
||||
}).catch(function() {
|
||||
return { installed: '', latest: '' };
|
||||
});
|
||||
}
|
||||
|
||||
function upgradePkg(pkg, btn) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
let tries = 0;
|
||||
const pollLog = function() {
|
||||
return fs.read_direct('/tmp/luci-app-honk.pkg-' + pkg + '.log', 'text').then(function(c) {
|
||||
if (c) {
|
||||
updateLog.textContent = c;
|
||||
updateLog.hidden = false;
|
||||
}
|
||||
if (/[✓✗]/.test(c)) { refreshPkgs(); return; }
|
||||
if (tries++ > 90) return;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(pollLog);
|
||||
}).catch(function() {});
|
||||
};
|
||||
return fs.exec(UPDATE_PKG, [ pkg ]).then(function(res) {
|
||||
if (res.code === 0) return pollLog();
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshPkgs() {
|
||||
const probes = PKGS.map(probePkg);
|
||||
return Promise.all(probes).then(function(infos) {
|
||||
while (pkgBody.firstChild)
|
||||
pkgBody.removeChild(pkgBody.firstChild);
|
||||
PKGS.forEach(function(pkg, i) {
|
||||
const r = infos[i];
|
||||
const btn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Upgrade'));
|
||||
btn.addEventListener('click', function(ev) { ev.preventDefault(); upgradePkg(pkg, btn); });
|
||||
const cmp = (r.installed && r.latest) ? cmpVer(r.latest, r.installed) : null;
|
||||
const updatable = cmp !== null && cmp > 0;
|
||||
let meta;
|
||||
if (!r.installed) {
|
||||
meta = _('not installed via package manager');
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Unavailable');
|
||||
} else if (!r.latest) {
|
||||
meta = _('installed') + ': ' + r.installed + ' · ' + _('latest version unknown');
|
||||
btn.disabled = true;
|
||||
} else if (updatable) {
|
||||
meta = _('installed') + ': ' + r.installed + ' → ' + _('latest') + ': ' + r.latest;
|
||||
} else {
|
||||
meta = _('installed') + ': ' + r.installed + ' · ' + _('up to date');
|
||||
btn.disabled = true;
|
||||
}
|
||||
pkgBody.appendChild(mkPkgRow(
|
||||
updatable ? '↑' : (r.installed ? '✓' : '✗'),
|
||||
updatable ? 'honk-pkg-new' : (r.installed ? 'honk-pkg-ok' : 'honk-pkg-err'),
|
||||
PKG_NAMES[pkg],
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Package updates')));
|
||||
updateSection.appendChild(E('p', { 'class': 'honk-note' }, _('Honk core and LuCI can be upgraded here without leaving the management page. The package index refreshes in the background.')));
|
||||
updateSection.appendChild(pkgBody);
|
||||
|
||||
/* --- Config backup card --- */
|
||||
const backupSection = E('section', { 'class': 'honk-card' });
|
||||
const backupMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const exportBtn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Export'));
|
||||
const importBtn = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Import'));
|
||||
const restoreCfgBtn = E('button', { 'class': 'cbi-button cbi-button-negative', 'type': 'button' }, _('Restore config'));
|
||||
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
|
||||
let backupBusy = false;
|
||||
writeControls.push(exportBtn, importBtn, restoreCfgBtn);
|
||||
|
||||
function setBackupBusy(busy) {
|
||||
backupBusy = busy;
|
||||
[exportBtn, importBtn, restoreCfgBtn].forEach(function(b) {
|
||||
b.disabled = busy || recoveryRequired;
|
||||
});
|
||||
}
|
||||
|
||||
exportBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (backupBusy) return;
|
||||
setBackupBusy(true);
|
||||
backupMessage.textContent = _('Preparing backup…');
|
||||
honk.backup().then(honk.ensureOk).then(function(result) {
|
||||
return honk.waitJob(result.job_id, function(job) {
|
||||
backupMessage.textContent = job.state === 'running' ? _('Creating backup…') : _('Preparing download…');
|
||||
});
|
||||
}).then(function() {
|
||||
return fs.read_direct(BACKUP_PATH, 'blob');
|
||||
}).then(function(blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = E('a', { 'href': url, 'download': 'honk-backup.tar.gz' });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
backupMessage.textContent = _('Backup downloaded.');
|
||||
}).catch(function(error) {
|
||||
if (error && error.recoveryRequired)
|
||||
blockWrites();
|
||||
backupMessage.textContent = honk.errorMessage(error, _('Backup failed.'));
|
||||
}).finally(function() { setBackupBusy(false); });
|
||||
});
|
||||
|
||||
importBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (backupBusy) return;
|
||||
fileInput.click();
|
||||
});
|
||||
fileInput.addEventListener('change', function(ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
if (!confirm(_('Stop Honk before restoring. Restoring replaces Honk user data. Continue?'))) {
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
setBackupBusy(true);
|
||||
backupMessage.textContent = _('Restoring…');
|
||||
honk.restorePrepare().then(honk.ensureOk).then(function() {
|
||||
return ui.uploadFile(RESTORE_PATH);
|
||||
}).then(function() {
|
||||
return honk.restore();
|
||||
}).then(honk.ensureOk).then(function(result) {
|
||||
return honk.waitJob(result.job_id, function(job) {
|
||||
backupMessage.textContent = phaseMessage(job, _('Restoring Honk data…'));
|
||||
});
|
||||
}).then(function(result) {
|
||||
backupMessage.textContent = honk.resultMessage(result, _('Backup restored.'));
|
||||
}).catch(function(error) {
|
||||
if (error && error.recoveryRequired)
|
||||
blockWrites();
|
||||
backupMessage.textContent = honk.errorMessage(error, _('Restore failed.'));
|
||||
}).finally(function() {
|
||||
setBackupBusy(false);
|
||||
fileInput.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
restoreCfgBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!confirm(_('This permanently clears Honk subscriptions, nodes, policies, DNS data, history, and cache, then restores the default configuration. The administrator account is kept, and Honk will be stopped. Continue?')))
|
||||
return;
|
||||
setBackupBusy(true);
|
||||
backupMessage.textContent = _('Restoring default config…');
|
||||
honk.reset().then(honk.ensureOk).then(function(result) {
|
||||
return honk.waitJob(result.job_id, function(job) {
|
||||
backupMessage.textContent = phaseMessage(job, _('Restoring default config…'));
|
||||
});
|
||||
}).then(function(result) {
|
||||
backupMessage.textContent = honk.resultMessage(result, _('Honk data reset. The service is stopped.'));
|
||||
}).catch(function(error) {
|
||||
if (error && error.recoveryRequired)
|
||||
blockWrites();
|
||||
backupMessage.textContent = honk.errorMessage(error, _('Reset failed.'));
|
||||
}).finally(function() { setBackupBusy(false); });
|
||||
});
|
||||
|
||||
backupSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Config backup')));
|
||||
backupSection.appendChild(E('p', { 'class': 'honk-note' }, _('Export a backup archive, import one from your computer, or restore Honk to its default configuration.')));
|
||||
backupSection.appendChild(E('div', { 'class': 'honk-actions' }, [ exportBtn, importBtn, restoreCfgBtn ]));
|
||||
backupSection.appendChild(fileInput);
|
||||
backupSection.appendChild(backupMessage);
|
||||
|
||||
/* --- System check and repair card --- */
|
||||
const health = E('section', { 'class': 'honk-card' });
|
||||
const healthMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const check = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Run system check'));
|
||||
check.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
check.disabled = true;
|
||||
healthMessage.textContent = _('Checking…');
|
||||
honk.check().then(function(result) {
|
||||
if (result && result.ok === true) {
|
||||
healthMessage.textContent = _('System check passed.');
|
||||
}
|
||||
else if (Array.isArray(result && result.errors) && result.errors.length) {
|
||||
healthMessage.textContent = result.errors.map(honk.statusIssue).join(';');
|
||||
}
|
||||
else {
|
||||
healthMessage.textContent = _('The backend did not confirm the check.');
|
||||
}
|
||||
}).catch(function(error) {
|
||||
healthMessage.textContent = _('System check failed.');
|
||||
}).finally(function() { check.disabled = false; });
|
||||
});
|
||||
const repairMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const repair = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Repair'));
|
||||
writeControls.push(repair);
|
||||
repair.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
repair.disabled = true;
|
||||
repairMessage.textContent = _('Repairing…');
|
||||
honk.repair().then(honk.ensureOk).then(function(result) {
|
||||
repairMessage.textContent = honk.resultMessage(result, _('Repair completed.'));
|
||||
}).catch(function(error) {
|
||||
if (error && error.recoveryRequired)
|
||||
blockWrites();
|
||||
repairMessage.textContent = honk.errorMessage(error, _('Repair failed.'));
|
||||
}).finally(function() { repair.disabled = recoveryRequired; });
|
||||
});
|
||||
health.appendChild(E('h3', { 'class': 'honk-card-title' }, _('System check and repair')));
|
||||
health.appendChild(E('p', { 'class': 'honk-note' }, _('Checks Honk system components and reports detected issues.')));
|
||||
health.appendChild(E('div', { 'class': 'honk-actions' }, [ check, repair ]));
|
||||
health.appendChild(healthMessage);
|
||||
health.appendChild(repairMessage);
|
||||
health.appendChild(E('p', { 'class': 'honk-note' }, _('Repair replaces missing or damaged Honk system files and does not overwrite user configuration.')));
|
||||
|
||||
page.appendChild(E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Maintenance')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Package updates, config backup and system recovery for the Honk installation.'))
|
||||
]));
|
||||
page.appendChild(recoveryMessage);
|
||||
page.appendChild(dataSection);
|
||||
page.appendChild(updateSection);
|
||||
page.appendChild(backupSection);
|
||||
page.appendChild(health);
|
||||
page.appendChild(updateLog);
|
||||
|
||||
// Background index refresh so new versions show on the next poll.
|
||||
fs.exec(REFRESH_INDEX, []).catch(function() {});
|
||||
refreshData();
|
||||
refreshPkgs();
|
||||
poll.add(refreshData, 15);
|
||||
poll.add(refreshPkgs, 15);
|
||||
|
||||
honk.status().then(function(result) {
|
||||
const errors = Array.isArray(result && result.errors) ? result.errors : [];
|
||||
const recoveryToken = errors.indexOf('update_recovery_required') >= 0 ? 'update_recovery_required' :
|
||||
errors.indexOf('maintenance_recovery_required') >= 0 ? 'maintenance_recovery_required' : null;
|
||||
if (recoveryToken) {
|
||||
blockWrites();
|
||||
recoveryMessage.textContent = honk.statusIssue(recoveryToken);
|
||||
recoveryMessage.hidden = false;
|
||||
}
|
||||
}).catch(function() {});
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require network';
|
||||
'require poll';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
|
||||
function getNetworkNames(networks) {
|
||||
if (!networks)
|
||||
return [];
|
||||
|
||||
const list = Array.isArray(networks) ? networks : Object.keys(networks).map(function(key) { return networks[key]; });
|
||||
return list.map(function(net) {
|
||||
if (net && typeof net.getName === 'function')
|
||||
return net.getName();
|
||||
if (net && typeof net.name === 'string')
|
||||
return net.name;
|
||||
return typeof net === 'string' ? net : '';
|
||||
}).filter(function(name, index, names) {
|
||||
return !!name && names.indexOf(name) === index;
|
||||
}).sort();
|
||||
}
|
||||
|
||||
function metaItem(label, value) {
|
||||
return E('span', { 'class': 'honk-meta-item' }, [
|
||||
E('span', { 'class': 'honk-meta-label' }, label),
|
||||
value == null || value === '' ? '—' : String(value)
|
||||
]);
|
||||
}
|
||||
|
||||
function field(label, input, help) {
|
||||
const row = E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
|
||||
E('div', { 'class': 'cbi-value-field' }, input)
|
||||
]);
|
||||
if (help)
|
||||
row.appendChild(E('div', { 'class': 'cbi-value-description honk-note' }, help));
|
||||
return row;
|
||||
}
|
||||
|
||||
function captureFields(root) {
|
||||
const values = {};
|
||||
Array.prototype.forEach.call(root.querySelectorAll('input, select, textarea'), function(input) {
|
||||
const key = input.id || input.name;
|
||||
if (!key)
|
||||
return;
|
||||
|
||||
values[key] = {
|
||||
value: input.value,
|
||||
checked: input.checked,
|
||||
focused: document.activeElement === input,
|
||||
selectionStart: typeof input.selectionStart === 'number' ? input.selectionStart : null,
|
||||
selectionEnd: typeof input.selectionEnd === 'number' ? input.selectionEnd : null
|
||||
};
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function restoreFields(root, values) {
|
||||
Object.keys(values).forEach(function(key) {
|
||||
const input = root.querySelector('#%s'.format(key));
|
||||
if (!input || input.type === 'file')
|
||||
return;
|
||||
|
||||
input.value = values[key].value;
|
||||
input.checked = values[key].checked;
|
||||
if (values[key].focused) {
|
||||
input.focus();
|
||||
if (values[key].selectionStart !== null && typeof input.setSelectionRange === 'function')
|
||||
input.setSelectionRange(values[key].selectionStart, values[key].selectionEnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
honk.status().then(function(status) { return { status: status }; }).catch(function(error) { return { error: error.message || String(error) }; }),
|
||||
network.getNetworks().then(getNetworkNames).then(function(networks) { return { networks: networks }; }).catch(function() { return { networks: [] }; })
|
||||
]).then(function(results) {
|
||||
return Object.assign({}, results[0], results[1]);
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
honk.installStyles();
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
const statusArea = E('div');
|
||||
const actionMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const setupArea = E('div');
|
||||
let currentStatus = data.status;
|
||||
let busy = false;
|
||||
let setupBusy = false;
|
||||
let settingsBusy = false;
|
||||
let setupState = null;
|
||||
|
||||
function renderStatus(status, error) {
|
||||
const fields = captureFields(statusArea);
|
||||
const focusedAction = statusArea.contains(document.activeElement) && document.activeElement.dataset.action;
|
||||
while (statusArea.firstChild)
|
||||
statusArea.removeChild(statusArea.firstChild);
|
||||
|
||||
if (error) {
|
||||
statusArea.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
|
||||
restoreFields(statusArea, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
currentStatus = status;
|
||||
const running = status.running === true;
|
||||
|
||||
/* Status card */
|
||||
const badge = E('span', {
|
||||
'class': 'honk-badge %s'.format(running ? 'honk-badge--run' : 'honk-badge--stop')
|
||||
}, running ? _('Running') : _('Stopped'));
|
||||
|
||||
const webUi = honk.localWebUi(status);
|
||||
const openDoona = E(webUi ? 'a' : 'button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'href': webUi || null,
|
||||
'target': webUi ? '_blank' : null,
|
||||
'rel': webUi ? 'noopener noreferrer' : null,
|
||||
'type': webUi ? null : 'button',
|
||||
'disabled': webUi ? null : true
|
||||
}, _('Open Doona'));
|
||||
|
||||
const toggle = E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'data-action': running ? 'stop' : 'start'
|
||||
}, running ? _('Stop') : _('Start'));
|
||||
toggle.disabled = busy || !status.initialized;
|
||||
toggle.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
runAction(running ? 'stop' : 'start');
|
||||
});
|
||||
|
||||
const actions = [ openDoona, toggle ];
|
||||
if (running) {
|
||||
const restart = E('button', { 'class': 'cbi-button', 'type': 'button', 'data-action': 'restart' }, _('Restart'));
|
||||
restart.disabled = busy;
|
||||
restart.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
runAction('restart');
|
||||
});
|
||||
actions.push(restart);
|
||||
}
|
||||
|
||||
const statusCard = E('section', { 'class': 'honk-card' }, [
|
||||
E('div', { 'class': 'honk-card-head' }, [
|
||||
E('div', {}, [
|
||||
E('h3', { 'class': 'honk-card-title' }, _('Status')),
|
||||
badge
|
||||
]),
|
||||
E('div', { 'class': 'honk-actions' }, actions)
|
||||
]),
|
||||
E('div', { 'class': 'honk-meta' }, [
|
||||
metaItem(_('Core version'), status.core_version),
|
||||
metaItem(_('Doona version'), status.doona_version),
|
||||
metaItem(_('Kernel'), status.kernel)
|
||||
])
|
||||
]);
|
||||
|
||||
if (!webUi) {
|
||||
const reason = !status.initialized ? _('Complete initial setup before opening Doona.') :
|
||||
!running ? _('Start Honk to open Doona.') : _('Doona is not ready yet. Check the service status.');
|
||||
statusCard.appendChild(E('p', { 'class': 'honk-note', 'role': 'status' }, reason));
|
||||
}
|
||||
|
||||
const errors = (Array.isArray(status.errors) ? status.errors : []).filter(function(token) {
|
||||
return token !== 'disabled' && token !== 'not_initialized' && token !== 'service_not_running';
|
||||
});
|
||||
if (errors.length)
|
||||
statusCard.appendChild(E('ul', { 'class': 'alert-message' }, errors.map(function(token) {
|
||||
return E('li', {}, honk.statusIssue(token));
|
||||
})));
|
||||
|
||||
statusArea.appendChild(statusCard);
|
||||
statusArea.appendChild(renderServiceSettings(status));
|
||||
|
||||
if (focusedAction) {
|
||||
const replacement = statusArea.querySelector('[data-action="%s"]'.format(focusedAction));
|
||||
if (replacement)
|
||||
replacement.focus();
|
||||
}
|
||||
restoreFields(statusArea, fields);
|
||||
}
|
||||
|
||||
function renderServiceSettings(status) {
|
||||
const card = E('section', { 'class': 'honk-card' });
|
||||
const form = E('form');
|
||||
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
|
||||
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const bootEnabledInput = E('input', { 'type': 'checkbox', 'name': 'boot_enabled', 'id': 'honk-boot-enabled' });
|
||||
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const save = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Save service settings'));
|
||||
|
||||
(data.networks || []).forEach(function(name) {
|
||||
networkSelect.appendChild(E('option', { 'value': name }, name));
|
||||
});
|
||||
if (status.lan_network && !(data.networks || []).includes(status.lan_network))
|
||||
networkSelect.appendChild(E('option', { 'value': status.lan_network }, status.lan_network));
|
||||
if (status.lan_network)
|
||||
networkSelect.value = status.lan_network;
|
||||
portInput.value = status.listen_port ? String(status.listen_port) : '9527';
|
||||
bootEnabledInput.checked = status.boot_enabled === true;
|
||||
|
||||
card.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Service settings')));
|
||||
if (!(data.networks || []).length)
|
||||
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
|
||||
form.appendChild(field(_('LAN network'), networkSelect));
|
||||
form.appendChild(field(_('Listen port'), portInput));
|
||||
form.appendChild(field(_('Start Honk at boot'), bootEnabledInput));
|
||||
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, save));
|
||||
form.appendChild(message);
|
||||
form.addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
const port = Number(portInput.value);
|
||||
if (!networkSelect.value || !Number.isInteger(port) || port < 1024 || port > 65535) {
|
||||
message.textContent = _('Choose a LAN network and a port from 1024 to 65535.');
|
||||
return;
|
||||
}
|
||||
|
||||
save.disabled = true;
|
||||
settingsBusy = true;
|
||||
message.textContent = _('Saving…');
|
||||
honk.settings(networkSelect.value, port, bootEnabledInput.checked).then(honk.ensureOk).then(function(result) {
|
||||
message.textContent = honk.resultMessage(result, _('Service settings saved.'));
|
||||
}).catch(function(error) {
|
||||
message.textContent = honk.errorMessage(error, _('Settings could not be applied.'));
|
||||
}).finally(function() {
|
||||
settingsBusy = false;
|
||||
save.disabled = false;
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
card.appendChild(form);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderSetup(status) {
|
||||
if (setupBusy)
|
||||
return;
|
||||
if (!status)
|
||||
return;
|
||||
const initialized = status.initialized === true;
|
||||
if (setupState === initialized)
|
||||
return;
|
||||
setupState = initialized;
|
||||
const fields = captureFields(setupArea);
|
||||
while (setupArea.firstChild)
|
||||
setupArea.removeChild(setupArea.firstChild);
|
||||
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
const form = E('form', { 'class': 'honk-card' });
|
||||
const username = E('input', { 'type': 'text', 'name': 'username', 'id': 'honk-init-username', 'autocomplete': 'username', 'required': true, 'class': 'cbi-input-text' });
|
||||
const password = E('input', { 'type': 'password', 'name': 'password', 'id': 'honk-init-password', 'autocomplete': 'new-password', 'required': true, 'class': 'cbi-input-password' });
|
||||
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const submit = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Initialize Honk'));
|
||||
form.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Initial setup')));
|
||||
form.appendChild(E('p', { 'class': 'honk-note' }, _('Create the Honk administrator account. The password is sent only for this request and is not saved by this page.')));
|
||||
form.appendChild(field(_('Username'), username));
|
||||
form.appendChild(field(_('Password'), password));
|
||||
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, submit));
|
||||
form.appendChild(message);
|
||||
form.addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
const user = username.value.trim();
|
||||
const secret = password.value;
|
||||
if (!user || !secret) {
|
||||
message.textContent = _('Enter both a username and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
submit.disabled = true;
|
||||
setupBusy = true;
|
||||
message.textContent = _('Initializing…');
|
||||
honk.initialize(user, secret).then(honk.ensureOk).then(function(result) {
|
||||
username.value = '';
|
||||
password.value = '';
|
||||
message.textContent = honk.resultMessage(result, _('Honk initialized.'));
|
||||
return honk.status();
|
||||
}).then(function(updatedStatus) {
|
||||
setupBusy = false;
|
||||
currentStatus = updatedStatus;
|
||||
renderSetup(updatedStatus);
|
||||
renderStatus(updatedStatus, null);
|
||||
}).catch(function() {
|
||||
password.value = '';
|
||||
message.textContent = _('Initialization failed. Check the system status.');
|
||||
}).finally(function() {
|
||||
setupBusy = false;
|
||||
submit.disabled = false;
|
||||
});
|
||||
});
|
||||
setupArea.appendChild(form);
|
||||
restoreFields(setupArea, fields);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (busy || setupBusy || settingsBusy)
|
||||
return Promise.resolve();
|
||||
return honk.status().then(function(status) {
|
||||
renderStatus(status, null);
|
||||
renderSetup(status);
|
||||
}).catch(function() {
|
||||
renderStatus(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
function runAction(method) {
|
||||
if (busy)
|
||||
return;
|
||||
busy = true;
|
||||
actionMessage.textContent = _('Working…');
|
||||
renderStatus(currentStatus, null);
|
||||
honk[method]().then(honk.ensureOk).then(function(result) {
|
||||
actionMessage.textContent = honk.resultMessage(result, _('Operation completed.'));
|
||||
}).catch(function(error) {
|
||||
actionMessage.textContent = honk.errorMessage(error, _('Operation failed. Check Honk status.'));
|
||||
}).finally(function() {
|
||||
busy = false;
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
page.appendChild(E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Honk')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Transparent proxy core for OpenWrt. Start, stop and manage the Honk service.'))
|
||||
]));
|
||||
page.appendChild(statusArea);
|
||||
page.appendChild(actionMessage);
|
||||
page.appendChild(setupArea);
|
||||
renderStatus(data.status, data.error);
|
||||
if (data.status)
|
||||
renderSetup(data.status);
|
||||
poll.add(refresh, 5);
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,308 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
const callStatus = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callCheck = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'check',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callInitialize = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'initialize',
|
||||
params: [ 'username', 'password' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callStart = rpc.declare({ object: 'honk', method: 'start', expect: { '': {} } });
|
||||
const callStop = rpc.declare({ object: 'honk', method: 'stop', expect: { '': {} } });
|
||||
const callRestart = rpc.declare({ object: 'honk', method: 'restart', expect: { '': {} } });
|
||||
|
||||
const callSettings = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'settings',
|
||||
params: [ 'lan_network', 'listen_port', 'boot_enabled' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callAddSubscription = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'add_subscription',
|
||||
params: [ 'name', 'url' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callAddNodes = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'add_nodes',
|
||||
params: [ 'links' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callImportDae = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'import_dae',
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callRepair = rpc.declare({ object: 'honk', method: 'repair', expect: { '': {} } });
|
||||
const callBackup = rpc.declare({ object: 'honk', method: 'backup', expect: { '': {} } });
|
||||
const callRestore = rpc.declare({ object: 'honk', method: 'restore', expect: { '': {} } });
|
||||
const callRestorePrepare = rpc.declare({ object: 'honk', method: 'restore_prepare', expect: { '': {} } });
|
||||
const callReset = rpc.declare({ object: 'honk', method: 'reset', expect: { '': {} } });
|
||||
const callJobStatus = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'job_status',
|
||||
params: [ 'job_id' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callLogs = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'logs',
|
||||
params: [ 'source', 'level', 'limit' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callUpdateCheck = rpc.declare({ object: 'honk', method: 'update_check', expect: { '': {} } });
|
||||
const callUpdateApply = rpc.declare({ object: 'honk', method: 'update_apply', expect: { '': {} } });
|
||||
|
||||
let pageStylesInstalled = false;
|
||||
|
||||
function installStyles() {
|
||||
if (pageStylesInstalled || document.getElementById('honk-page-styles'))
|
||||
return;
|
||||
|
||||
const stylesheet = document.createElement('link');
|
||||
stylesheet.id = 'honk-page-styles';
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = L.resource('view/honk/honk.css');
|
||||
document.head.appendChild(stylesheet);
|
||||
pageStylesInstalled = true;
|
||||
}
|
||||
|
||||
function localWebUi(status) {
|
||||
if (!status || !status.initialized || !status.running || !status.api_ready || !status.api_url)
|
||||
return null;
|
||||
|
||||
try {
|
||||
const raw = String(status.api_url);
|
||||
const authority = raw.match(/^https?:\/\/([^/?#]+)/i);
|
||||
if (!authority || authority[1].indexOf('@') >= 0)
|
||||
return null;
|
||||
|
||||
const portMatch = authority[1].match(/:(\d+)$/);
|
||||
const url = new URL(raw);
|
||||
const hostname = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
||||
const port = portMatch ? Number(portMatch[1]) : 0;
|
||||
if (!portMatch || !Number.isInteger(port) || port < 1 || port > 65535 ||
|
||||
url.protocol !== 'http:' && url.protocol !== 'https:' ||
|
||||
url.username || url.password || url.search || url.hash)
|
||||
return null;
|
||||
|
||||
const octets = hostname.split('.');
|
||||
let localIPv4 = false;
|
||||
if (octets.length === 4 && octets.every(function(part) { return /^\d{1,3}$/.test(part) && Number(part) <= 255; })) {
|
||||
const ip = octets.map(Number);
|
||||
localIPv4 = ip[0] === 10 || ip[0] === 127 || ip[0] === 192 && ip[1] === 168 ||
|
||||
ip[0] === 172 && ip[1] >= 16 && ip[1] <= 31 || ip[0] === 169 && ip[1] === 254;
|
||||
}
|
||||
const localIPv6 = hostname === '::1' || /^f[cd][0-9a-f:]*$/.test(hostname) || /^fe[89ab][0-9a-f:]*$/.test(hostname);
|
||||
if (!localIPv4 && !localIPv6)
|
||||
return null;
|
||||
|
||||
return url.origin.replace(/\/$/, '') + '/ui/';
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function tokenMessage(token) {
|
||||
const messages = {
|
||||
disabled: _('Honk is disabled.'),
|
||||
not_initialized: _('Complete initial setup before starting Honk.'),
|
||||
service_not_running: _('Honk is enabled but currently stopped.'),
|
||||
api_unavailable: _('Honk is running, but its management interface is not ready.'),
|
||||
operation_in_progress: _('Another Honk operation is in progress.'),
|
||||
stale_operation_lock: _('A previous operation did not finish cleanly.'),
|
||||
started: _('Honk started.'),
|
||||
already_running: _('Honk is already running.'),
|
||||
stopped: _('Honk stopped.'),
|
||||
restarted: _('Honk restarted.'),
|
||||
settings_saved: _('Settings saved and applied.'),
|
||||
initialized: _('Honk initialized.'),
|
||||
administrator_initialized: _('Honk initialized.'),
|
||||
system_config_healthy: _('System check passed.'),
|
||||
system_config_repaired: _('Repair completed.'),
|
||||
backup_created: _('Backup created.'),
|
||||
restored: _('Backup restored.'),
|
||||
data_reset: _('Honk data reset. The service is stopped.'),
|
||||
worker_lost: _('The maintenance worker stopped unexpectedly; check system recovery status before retrying.'),
|
||||
operation_failed: _('The maintenance operation failed.'),
|
||||
job_timeout: _('The maintenance operation timed out.'),
|
||||
maintenance_recovery_required: _('Recovery is required. Run system recovery before starting another maintenance operation.'),
|
||||
update_recovery_required: _('The signed update did not finish. Keep Honk stopped and reinstall a complete verified package set before retrying.'),
|
||||
rollback_tuple_unavailable: _('A compatible update is available, but a complete verified rollback package set is not available yet.'),
|
||||
trusted_feed_unavailable: _('No trusted update source is configured.'),
|
||||
manifest_incompatible: _('The signed update manifest does not match this device.'),
|
||||
signed_manifest_unavailable: _('The signed update manifest could not be fetched or verified.'),
|
||||
update_available: _('A compatible update is available.'),
|
||||
no_update: _('No compatible update is available.'),
|
||||
already_current: _('Honk is already up to date.'),
|
||||
update_apply_failed: _('The update could not be installed.'),
|
||||
update_check_failed: _('The signed update could not be verified for this device.'),
|
||||
signed_manifest_missing: _('Check for updates again to refresh the signed manifest.'),
|
||||
signed_manifest_expired: _('The signed manifest expired. Check for updates again.'),
|
||||
reset_failed: _('Reset failed; the previous service state was restored.'),
|
||||
reset_failed_rollback_failed: _('Reset failed and automatic recovery did not finish. Run system recovery before retrying.'),
|
||||
restore_failed_rollback_failed: _('Restore failed and automatic recovery did not finish. Run system recovery before retrying.'),
|
||||
logs_unavailable: _('Honk logs are unavailable.'),
|
||||
unsupported_core: _('This installed core does not support the requested import operation.'),
|
||||
invalid_request: _('The import request is incomplete or invalid.'),
|
||||
import_failed: _('The core could not prepare this import preview.'),
|
||||
share_links_too_large: _('Share links exceed the 16 KiB per request limit.'),
|
||||
share_links_invalid_or_conflicting: _('A share link is invalid or conflicts with an existing node. Review the input and try again.'),
|
||||
subscription_invalid: _('The subscription name or URL is invalid.'),
|
||||
subscription_invalid_or_conflicting: _('The subscription is invalid or its name already exists.'),
|
||||
subscription_added: _('Subscription added and Honk reloaded.'),
|
||||
nodes_added: _('Nodes added and Honk reloaded.'),
|
||||
dae_imported: _('Configuration imported and Honk reloaded.'),
|
||||
config_missing: _('The configuration file is missing.'),
|
||||
config_write_failed: _('Could not write the configuration file.'),
|
||||
dae_too_large: _('The dae file exceeds the 2 MiB limit.'),
|
||||
dae_invalid_or_unsupported: _('The dae file contains invalid sections or unsupported includes.'),
|
||||
candidate_invalid: _('The imported configuration could not be parsed.'),
|
||||
candidate_rejected: _('The imported configuration failed core validation.'),
|
||||
source_changed: _('Honk or Doona configuration changed after the preview. Create a new preview.'),
|
||||
preview_expired: _('The preview expired. Create a new preview.'),
|
||||
preview_corrupt: _('The saved preview is damaged. Create a new preview.'),
|
||||
dae_upload_unavailable: _('The dae upload is missing or unavailable. Upload the file again.'),
|
||||
dae_upload_invalid: _('The uploaded dae file is invalid.'),
|
||||
dae_upload_changed: _('The uploaded dae file changed. Upload it again.'),
|
||||
upload_unavailable: _('A private upload location could not be prepared.'),
|
||||
settings_write_failed: _('Could not save Honk settings.'),
|
||||
start_failed: _('Honk could not start.'),
|
||||
stop_failed: _('Honk could not stop.'),
|
||||
restart_failed: _('Honk could not restart.'),
|
||||
managed_service_unhealthy: _('The managed Honk service is not healthy.'),
|
||||
unmanaged_core_running: _('An unmanaged Honk core process is running.'),
|
||||
invalid_lan_network: _('The selected LAN network is invalid or unavailable.'),
|
||||
lan_network_unavailable: _('The selected LAN network is unavailable.'),
|
||||
listen_port_occupied: _('The listen port is already in use.'),
|
||||
invalid_listen_port: _('The listen port is invalid.'),
|
||||
initialize_before_enabling_service: _('Complete initial setup before enabling Honk.'),
|
||||
system_config_unavailable: _('Honk system configuration is unavailable.'),
|
||||
package_config_unavailable: _('Honk configuration is unavailable.'),
|
||||
service_apply_failed_rolled_back: _('Settings could not be applied. The previous settings were restored.')
|
||||
};
|
||||
|
||||
return messages[token] || null;
|
||||
}
|
||||
|
||||
function statusIssue(token) {
|
||||
return tokenMessage(token) || _('Honk reported a system issue.');
|
||||
}
|
||||
|
||||
function resultMessage(result, fallback) {
|
||||
return result && tokenMessage(result.message) || fallback;
|
||||
}
|
||||
|
||||
function errorMessage(error, fallback) {
|
||||
return error && tokenMessage(error.message) || fallback;
|
||||
}
|
||||
|
||||
function jobPhaseMessage(phase) {
|
||||
const messages = {
|
||||
queued: _('Waiting for the operation to start…'),
|
||||
checking_feed: _('Checking the signed update source…'),
|
||||
downloading: _('Downloading the signed package set…'),
|
||||
preparing: _('Preparing configuration apply…'),
|
||||
validating_candidate: _('Validating the candidate in an isolated runtime…'),
|
||||
stopping: _('Stopping Honk safely…'),
|
||||
applying: _('Applying the reviewed configuration…'),
|
||||
validating: _('Checking the updated configuration…'),
|
||||
starting: _('Restoring the previous service state…'),
|
||||
rolling_back: _('Restoring the previous configuration…'),
|
||||
committing: _('Finishing the transaction…'),
|
||||
recovery: _('Recovery is required before another operation.')
|
||||
};
|
||||
return messages[phase] || _('Applying configuration…');
|
||||
}
|
||||
|
||||
function ensureOk(result) {
|
||||
if (!result || result.ok !== true)
|
||||
throw new Error(result && result.message ? result.message : _('The backend did not confirm the operation.'));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function waitJob(jobId, onProgress) {
|
||||
if (!/^[0-9a-f]{32}$/.test(jobId || ''))
|
||||
return Promise.reject(new Error('invalid_job_id'));
|
||||
|
||||
let attempts = 0;
|
||||
return new Promise(function(resolve, reject) {
|
||||
function poll() {
|
||||
callJobStatus(jobId).then(function(result) {
|
||||
if (!result || typeof result.state !== 'string')
|
||||
throw new Error('invalid_job_status');
|
||||
if (onProgress)
|
||||
onProgress(result);
|
||||
if (result.state === 'succeeded') {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
if (result.state === 'failed' || result.state === 'rollback_required') {
|
||||
const error = new Error(result.state === 'rollback_required' ? 'maintenance_recovery_required' : result.message || 'operation_failed');
|
||||
error.recoveryRequired = result.state === 'rollback_required';
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (result.state !== 'queued' && result.state !== 'running') {
|
||||
reject(new Error('invalid_job_status'));
|
||||
return;
|
||||
}
|
||||
if (++attempts >= 1800) {
|
||||
reject(new Error('job_timeout'));
|
||||
return;
|
||||
}
|
||||
setTimeout(poll, 1000);
|
||||
}).catch(reject);
|
||||
}
|
||||
poll();
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
status: callStatus,
|
||||
check: callCheck,
|
||||
initialize: callInitialize,
|
||||
start: callStart,
|
||||
stop: callStop,
|
||||
restart: callRestart,
|
||||
settings: callSettings,
|
||||
addSubscription: callAddSubscription,
|
||||
addNodes: callAddNodes,
|
||||
importDae: callImportDae,
|
||||
repair: callRepair,
|
||||
backup: callBackup,
|
||||
restore: callRestore,
|
||||
restorePrepare: callRestorePrepare,
|
||||
reset: callReset,
|
||||
logs: callLogs,
|
||||
updateCheck: callUpdateCheck,
|
||||
updateApply: callUpdateApply,
|
||||
waitJob: waitJob,
|
||||
ensureOk: ensureOk,
|
||||
localWebUi: localWebUi,
|
||||
statusIssue: statusIssue,
|
||||
resultMessage: resultMessage,
|
||||
errorMessage: errorMessage,
|
||||
jobPhaseMessage: jobPhaseMessage,
|
||||
installStyles: installStyles
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require network';
|
||||
'require view';
|
||||
'require view.honk.rpc as honk';
|
||||
|
||||
function getNetworkNames(networks) {
|
||||
if (!networks)
|
||||
return [];
|
||||
|
||||
const list = Array.isArray(networks) ? networks : Object.keys(networks).map(function(key) { return networks[key]; });
|
||||
return list.map(function(net) {
|
||||
if (net && typeof net.getName === 'function')
|
||||
return net.getName();
|
||||
if (net && typeof net.name === 'string')
|
||||
return net.name;
|
||||
return typeof net === 'string' ? net : '';
|
||||
}).filter(function(name, index, names) {
|
||||
return !!name && names.indexOf(name) === index;
|
||||
}).sort();
|
||||
}
|
||||
|
||||
function field(label, input) {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
|
||||
E('div', { 'class': 'cbi-value-field' }, input)
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
honk.status().then(function(status) { return { status: status }; }).catch(function(error) { return { statusError: error.message || String(error) }; }),
|
||||
network.getNetworks().then(getNetworkNames).then(function(names) { return { networks: names }; }).catch(function(error) { return { networkError: error.message || String(error), networks: [] }; })
|
||||
]).then(function(results) {
|
||||
return Object.assign({}, results[0], results[1]);
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
honk.installStyles();
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
const section = E('form', { 'class': 'honk-card' });
|
||||
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
|
||||
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
|
||||
const bootEnabledInput = E('input', { 'type': 'checkbox', 'name': 'boot_enabled', 'id': 'honk-boot-enabled' });
|
||||
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
|
||||
const save = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Save settings'));
|
||||
|
||||
(data.networks || []).forEach(function(name) {
|
||||
networkSelect.appendChild(E('option', { 'value': name }, name));
|
||||
});
|
||||
if (data.status && data.status.lan_network && !(data.networks || []).includes(data.status.lan_network))
|
||||
networkSelect.appendChild(E('option', { 'value': data.status.lan_network }, data.status.lan_network));
|
||||
if (data.status && data.status.lan_network)
|
||||
networkSelect.value = data.status.lan_network;
|
||||
portInput.value = data.status && data.status.listen_port ? String(data.status.listen_port) : '9527';
|
||||
bootEnabledInput.checked = !!(data.status && data.status.boot_enabled === true);
|
||||
|
||||
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Honk settings')));
|
||||
if (data.statusError)
|
||||
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read current settings: %s').format(data.statusError)));
|
||||
if (data.networkError)
|
||||
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not load LAN networks: %s').format(data.networkError)));
|
||||
if (!(data.networks || []).length)
|
||||
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
|
||||
section.appendChild(field(_('LAN network'), networkSelect));
|
||||
section.appendChild(field(_('Listen port'), portInput));
|
||||
section.appendChild(field(_('Start Honk at boot'), bootEnabledInput));
|
||||
section.appendChild(E('div', { 'class': 'cbi-page-actions' }, save));
|
||||
section.appendChild(message);
|
||||
section.addEventListener('submit', function(ev) {
|
||||
ev.preventDefault();
|
||||
const port = Number(portInput.value);
|
||||
if (!networkSelect.value || !Number.isInteger(port) || port < 1024 || port > 65535) {
|
||||
message.textContent = _('Choose a LAN network and a port from 1024 to 65535.');
|
||||
return;
|
||||
}
|
||||
|
||||
save.disabled = true;
|
||||
message.textContent = _('Saving…');
|
||||
honk.settings(networkSelect.value, port, bootEnabledInput.checked).then(honk.ensureOk).then(function(result) {
|
||||
message.textContent = honk.resultMessage(result, _('Settings saved and applied.'));
|
||||
}).catch(function(error) {
|
||||
message.textContent = honk.errorMessage(error, _('Save failed. Check the settings and system status.'));
|
||||
}).finally(function() {
|
||||
save.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
page.appendChild(E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Settings')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Network interface, listen port and boot behaviour for the Honk service.'))
|
||||
]));
|
||||
page.appendChild(section);
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
function sha256(input) {
|
||||
const bytes = new Uint8Array(input);
|
||||
const bitLength = bytes.length * 8;
|
||||
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
|
||||
const padded = new Uint8Array(paddedLength);
|
||||
padded.set(bytes);
|
||||
padded[bytes.length] = 0x80;
|
||||
const lowBits = bitLength >>> 0;
|
||||
const highBits = Math.floor(bitLength / 0x100000000) >>> 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
padded[paddedLength - 8 + i] = highBits >>> (24 - i * 8);
|
||||
padded[paddedLength - 4 + i] = lowBits >>> (24 - i * 8);
|
||||
}
|
||||
|
||||
const constants = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
];
|
||||
const state = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];
|
||||
const words = new Uint32Array(64);
|
||||
const rotateRight = function(value, count) { return (value >>> count) | (value << (32 - count)); };
|
||||
|
||||
for (let offset = 0; offset < paddedLength; offset += 64) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const at = offset + i * 4;
|
||||
words[i] = ((padded[at] << 24) | (padded[at + 1] << 16) | (padded[at + 2] << 8) | padded[at + 3]) >>> 0;
|
||||
}
|
||||
for (let i = 16; i < 64; i++) {
|
||||
const x = words[i - 15];
|
||||
const y = words[i - 2];
|
||||
const sigma0 = rotateRight(x, 7) ^ rotateRight(x, 18) ^ (x >>> 3);
|
||||
const sigma1 = rotateRight(y, 17) ^ rotateRight(y, 19) ^ (y >>> 10);
|
||||
words[i] = (words[i - 16] + sigma0 + words[i - 7] + sigma1) >>> 0;
|
||||
}
|
||||
|
||||
let a = state[0], b = state[1], c = state[2], d = state[3];
|
||||
let e = state[4], f = state[5], g = state[6], h = state[7];
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
||||
const choice = (e & f) ^ (~e & g);
|
||||
const temp1 = (h + sum1 + choice + constants[i] + words[i]) >>> 0;
|
||||
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
||||
const majority = (a & b) ^ (a & c) ^ (b & c);
|
||||
const temp2 = (sum0 + majority) >>> 0;
|
||||
h = g; g = f; f = e; e = (d + temp1) >>> 0;
|
||||
d = c; c = b; b = a; a = (temp1 + temp2) >>> 0;
|
||||
}
|
||||
state[0] = (state[0] + a) >>> 0;
|
||||
state[1] = (state[1] + b) >>> 0;
|
||||
state[2] = (state[2] + c) >>> 0;
|
||||
state[3] = (state[3] + d) >>> 0;
|
||||
state[4] = (state[4] + e) >>> 0;
|
||||
state[5] = (state[5] + f) >>> 0;
|
||||
state[6] = (state[6] + g) >>> 0;
|
||||
state[7] = (state[7] + h) >>> 0;
|
||||
}
|
||||
|
||||
return state.map(function(value) { return value.toString(16).padStart(8, '0'); }).join('');
|
||||
}
|
||||
|
||||
return baseclass.extend({ sha256: sha256 });
|
||||
@@ -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
|
||||
Reference in new issue
Block a user