mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🐶 Sync 2026-10-20 22:40:23
This commit is contained in:
1 parent
e813f5cc54
commit
64cea2243a
216 files changed
+11913
-2968
No files matched your search
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
parseAutoJoin: function(val) {
|
||||
if (!val || !val.trim())
|
||||
return { channels: [], keys: [] };
|
||||
|
||||
var trimmed = val.trim();
|
||||
var spaceIdx = trimmed.search(/\s/);
|
||||
if (spaceIdx === -1) {
|
||||
return {
|
||||
channels: trimmed.split(',').map(function(c) { return c.trim(); }).filter(Boolean),
|
||||
keys: []
|
||||
};
|
||||
}
|
||||
|
||||
var firstPart = trimmed.substring(0, spaceIdx).trim();
|
||||
var rest = trimmed.substring(spaceIdx + 1).trim();
|
||||
|
||||
/* Keys are positional, so keep empty ones to stay aligned with channels */
|
||||
return {
|
||||
channels: firstPart.split(',').map(function(c) { return c.trim(); }).filter(Boolean),
|
||||
keys: rest.length ? rest.split(',').map(function(k) { return k.trim(); }) : []
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,412 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
var callGetCertStatus = rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'get_certificate_status',
|
||||
params: [ 'path' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callGenerateCert = rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'generate_certificate',
|
||||
params: [ 'cn', 'san', 'path' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callRcInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: [ 'name', 'action' ]
|
||||
});
|
||||
|
||||
/* Mirrors check_path() in the init script, which refuses to start otherwise */
|
||||
function validatePath(value, allowPercent) {
|
||||
if (!value)
|
||||
return true;
|
||||
if (value.charAt(0) !== '/' || /[\r\n]/.test(value) || /(^|\/)\.\.?(\/|$)/.test(value))
|
||||
return _('Must be an absolute path without . or .. components');
|
||||
if (value !== value.trim())
|
||||
return _('Must not start or end with a space');
|
||||
if (value.indexOf('${') !== -1)
|
||||
return _('Must not contain "${"');
|
||||
if (!allowPercent && value.indexOf('%') !== -1)
|
||||
return _('Must not contain "%"');
|
||||
return true;
|
||||
}
|
||||
|
||||
/* WeeChat only reads the certificate when it starts, so restart a running relay */
|
||||
function restartIfRunning() {
|
||||
return callServiceList('weechat').then(function(res) {
|
||||
var instances = (res && res.weechat && res.weechat.instances) || {};
|
||||
var running = Object.keys(instances).some(function(k) { return instances[k].running; });
|
||||
|
||||
if (!running)
|
||||
return false;
|
||||
|
||||
return callRcInit('weechat', 'restart').then(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('weechat');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
var defaultPath = '/etc/weechat/tls/relay.pem';
|
||||
var refreshCertStatus = null;
|
||||
|
||||
/* Only certificates in this directory are checked and generated here */
|
||||
function isManagedPath(path) {
|
||||
return /^\/etc\/weechat\/tls\/[A-Za-z0-9._-]+\.pem$/.test(path) && path.indexOf('..') === -1;
|
||||
}
|
||||
|
||||
/* IPv6 literals come with brackets, which are not valid in a SAN */
|
||||
var hostname = (window.location.hostname || 'OpenWrt').replace(/^\[(.*)\]$/, '$1');
|
||||
var defaultSan = hostname;
|
||||
|
||||
m = new form.Map('weechat', _('WeeChat - Relay'),
|
||||
_('Configure the WeeChat relay and chat logging.'));
|
||||
|
||||
/* Relay */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Relay'));
|
||||
|
||||
var optRelayEnabled = s.option(form.Flag, 'relay_enabled', _('Enable Relay Server'));
|
||||
optRelayEnabled.default = optRelayEnabled.disabled;
|
||||
optRelayEnabled.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'relay_address', _('Listen Address'),
|
||||
_('IP address to bind (leave empty for all interfaces). The address must exist when WeeChat starts.'));
|
||||
o.datatype = 'ipaddr("nomask")';
|
||||
o.placeholder = _('All interfaces');
|
||||
/* WeeChat cannot bind to it and the init script refuses it */
|
||||
o.validate = function(section_id, value) {
|
||||
var m = (value || '').match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/);
|
||||
if (m && m[1].split('.').some(function(p) { return p.length > 1 && p.charAt(0) === '0'; }))
|
||||
return _('Leading zeros are not allowed in an IPv4 address');
|
||||
return true;
|
||||
};
|
||||
o.rmempty = true;
|
||||
o.depends('relay_enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'relay_port', _('Port'),
|
||||
_('Port for relay connections (WebSocket).'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = '9000';
|
||||
o.placeholder = '9000';
|
||||
o.depends('relay_enabled', '1');
|
||||
|
||||
/* Security */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Security'));
|
||||
|
||||
o = s.option(form.Flag, 'relay_tls', _('Enable TLS'),
|
||||
_('Encrypt relay connections with TLS.'));
|
||||
o.default = o.disabled;
|
||||
|
||||
var optCertKey = s.option(form.Value, 'relay_cert_key', _('Certificate'),
|
||||
_('PEM file containing the certificate and private key. It must be readable by the weechat user.'));
|
||||
optCertKey.placeholder = defaultPath;
|
||||
optCertKey.rmempty = true;
|
||||
optCertKey.depends('relay_tls', '1');
|
||||
optCertKey.validate = function(section_id, value) {
|
||||
return validatePath(value, true);
|
||||
};
|
||||
optCertKey.onchange = function() {
|
||||
if (refreshCertStatus)
|
||||
refreshCertStatus();
|
||||
};
|
||||
|
||||
/* An empty field means the default path, as in the init script */
|
||||
function getActiveCertPath() {
|
||||
var val = optCertKey.formvalue('weechat');
|
||||
if (val == null)
|
||||
val = uci.get('weechat', 'weechat', 'relay_cert_key');
|
||||
return (val || '').trim() || defaultPath;
|
||||
}
|
||||
|
||||
/* Certificate status widget */
|
||||
var optCertStatus = s.option(form.DummyValue, '_cert_status', _('Certificate status'));
|
||||
optCertStatus.depends('relay_tls', '1');
|
||||
|
||||
optCertStatus.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var container = E('div', { 'class': 'cbi-value-field' });
|
||||
var isReadonly = (this.readonly != null) ? this.readonly : this.map.readonly;
|
||||
|
||||
function updateWidget(path) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
|
||||
if (!isManagedPath(path)) {
|
||||
container.appendChild(E('div', { 'class': 'alert-message notice' },
|
||||
_('This certificate is managed outside of LuCI, so it is not checked here. The service refuses to start if the file does not exist.')));
|
||||
return;
|
||||
}
|
||||
|
||||
container.appendChild(E('em', {}, _('Checking certificate status…')));
|
||||
|
||||
callGetCertStatus(path || defaultPath).then(function(res) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
|
||||
var elements = [];
|
||||
|
||||
if (res && res.exists && res.has_cert && res.has_key) {
|
||||
elements.push(E('div', {}, [
|
||||
E('span', { 'class': 'label success' }, [ '● ', _('Certificate and key present') ])
|
||||
]));
|
||||
|
||||
elements.push(E('p', { 'class': 'cbi-value-description' }, [
|
||||
E('strong', {}, _('File: ')),
|
||||
E('code', {}, res.path)
|
||||
]));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res.generator_available);
|
||||
}
|
||||
}, _('Generate new certificate')));
|
||||
} else if (res && res.exists && (!res.has_cert || !res.has_key)) {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('Certificate file exists but is incomplete (must contain both certificate and private key).')));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res.generator_available);
|
||||
}
|
||||
}, _('Generate self-signed certificate')));
|
||||
} else {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('TLS is enabled but no certificate file was found.')));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res && res.generator_available);
|
||||
}
|
||||
}, _('Generate self-signed certificate')));
|
||||
}
|
||||
|
||||
if (res && res.generator_available === false) {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('Certificate generation requires the %s or %s utility.').format('px5g', 'openssl')));
|
||||
}
|
||||
|
||||
for (var i = 0; i < elements.length; i++)
|
||||
container.appendChild(elements[i]);
|
||||
}).catch(function(err) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
container.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
_('Unable to verify certificate status: %s').format(err.message || err)));
|
||||
});
|
||||
}
|
||||
|
||||
function showGenerateDialog(targetPath, generatorAvailable) {
|
||||
var inputCn, inputSan, btnGenerate, btnCancel, msgBox;
|
||||
|
||||
var modalContent = [
|
||||
E('div', { 'class': 'alert-message notice' }, [
|
||||
E('p', {},
|
||||
_('A self-signed certificate will be generated for WeeChat Relay at %s.').format(targetPath)),
|
||||
E('p', {},
|
||||
E('strong', {}, _('Self-signed certificates provide encryption but are not trusted automatically by browsers.'))),
|
||||
E('p', {},
|
||||
_('For Internet-facing deployments, use a certificate issued by a trusted CA.'))
|
||||
]),
|
||||
|
||||
E('h5', {}, _('Certificate identity')),
|
||||
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Common name')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
inputCn = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'value': hostname,
|
||||
'placeholder': 'router.local'
|
||||
}),
|
||||
E('div', { 'class': 'cbi-value-description' },
|
||||
_('Host name identifying this router.'))
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Subject alternative names')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
inputSan = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'value': defaultSan,
|
||||
'placeholder': 'router.local, 192.168.1.1'
|
||||
}),
|
||||
E('div', { 'class': 'cbi-value-description' },
|
||||
_('Comma-separated list of host names and IP addresses used to reach the relay.'))
|
||||
])
|
||||
]),
|
||||
|
||||
msgBox = E('div', {}),
|
||||
|
||||
E('div', { 'class': 'right' }, [
|
||||
btnCancel = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
btnGenerate = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
var cnVal = inputCn.value.trim();
|
||||
var sanVal = inputSan.value.trim();
|
||||
|
||||
if (!cnVal) {
|
||||
ui.addNotification(null, E('p', _('Please enter a valid Common Name.')), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
btnGenerate.disabled = true;
|
||||
btnCancel.disabled = true;
|
||||
btnGenerate.textContent = _('Generating…');
|
||||
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('p', { 'class': 'spinning' }, _('Generating self-signed certificate…')));
|
||||
|
||||
callGenerateCert(cnVal, sanVal, targetPath).then(function(res) {
|
||||
if (res && res.success) {
|
||||
ui.hideModal();
|
||||
updateWidget(getActiveCertPath());
|
||||
/* Only the saved relay settings decide which certificate WeeChat loads */
|
||||
(res.in_use ? restartIfRunning() : Promise.resolve(null)).then(function(restarted) {
|
||||
var msg;
|
||||
if (restarted === true)
|
||||
msg = _('Self-signed certificate generated for %s. WeeChat was restarted to use it.');
|
||||
else if (restarted === false)
|
||||
msg = _('Self-signed certificate generated for %s. WeeChat uses it from its next start.');
|
||||
else
|
||||
msg = _('Self-signed certificate generated for %s. Save and apply TLS with this certificate to use it.');
|
||||
ui.addNotification(null, E('p', msg.format(cnVal)), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p',
|
||||
_('Self-signed certificate generated for %s, but WeeChat could not be restarted: %s').format(cnVal, err.message || err)),
|
||||
'warning');
|
||||
});
|
||||
} else {
|
||||
btnGenerate.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnGenerate.textContent = _('Generate');
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
res && res.error ? res.error : _('Certificate generation failed.')));
|
||||
}
|
||||
}).catch(function(err) {
|
||||
btnGenerate.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnGenerate.textContent = _('Generate');
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
err.message || err));
|
||||
});
|
||||
}
|
||||
}, _('Generate'))
|
||||
])
|
||||
];
|
||||
|
||||
if (generatorAvailable === false) {
|
||||
btnGenerate.disabled = true;
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
_('Cannot generate certificate: neither %s nor %s is installed on this router.').format('px5g', 'openssl')));
|
||||
}
|
||||
|
||||
ui.showModal(_('Generate certificate?'), modalContent);
|
||||
}
|
||||
|
||||
refreshCertStatus = function() {
|
||||
updateWidget(getActiveCertPath());
|
||||
};
|
||||
|
||||
/* The field is not in the page yet while rendering, start from its stored value */
|
||||
updateWidget(String(optCertKey.cfgvalue(section_id) || '').trim() || defaultPath);
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'relay_password', _('Relay Password'),
|
||||
_('Password required by clients to connect.'));
|
||||
o.password = true;
|
||||
o.validate = function(section_id, value) {
|
||||
var formVal = optRelayEnabled ? optRelayEnabled.formvalue(section_id) : null;
|
||||
var isEnabled = (formVal !== null) ? (formVal === '1') : (uci.get('weechat', section_id, 'relay_enabled') === '1');
|
||||
if (isEnabled && (!value || !value.trim())) {
|
||||
return _('Relay password is required when relay server is enabled.');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Logging */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Logging'));
|
||||
|
||||
o = s.option(form.Flag, 'log_enabled', _('Enable Logging'),
|
||||
_('Record chat logs to disk. Disabled by default to protect flash memory.'));
|
||||
o.default = o.disabled;
|
||||
|
||||
o = s.option(form.Value, 'log_dir', _('Log Directory'),
|
||||
_('Directory where WeeChat stores chat logs. The default /var/log/weechat is created automatically, any other directory must exist and be writable by the weechat user.'));
|
||||
o.placeholder = '/var/log/weechat';
|
||||
o.depends('log_enabled', '1');
|
||||
o.validate = function(section_id, value) {
|
||||
return validatePath(value, false);
|
||||
};
|
||||
|
||||
/* Web Client */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Web Client'));
|
||||
|
||||
o = s.option(form.DummyValue, '_gb_info');
|
||||
o.cfgvalue = function() {
|
||||
return E('div', {}, [
|
||||
E('p', { 'style': 'margin-bottom: 12px; line-height: 1.5;' },
|
||||
_('Glowing Bear is a web client that connects to WeeChat using the relay configured above.')),
|
||||
E('a', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'href': 'https://www.glowing-bear.org/',
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'margin-bottom: 6px; display: inline-block;'
|
||||
}, [ _('Open Glowing Bear (Hosted)'), ' ↗' ])
|
||||
]);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require tools.weechat as weechatTools';
|
||||
|
||||
/* WeeChat evaluates these options, the init script refuses "${" in them */
|
||||
function validateNoExpr(section_id, value) {
|
||||
if (value && value.indexOf('${') !== -1)
|
||||
return _('Must not contain "${"');
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkAutoJoinRows(rows) {
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var chanInput = rows[i].querySelector('.chan-input');
|
||||
var keyInput = rows[i].querySelector('.key-input');
|
||||
var chan = (chanInput ? chanInput.value : '').trim();
|
||||
var key = (keyInput ? keyInput.value : '').trim();
|
||||
|
||||
if (!chan && key)
|
||||
return _('Channel key cannot be specified without a channel name');
|
||||
if (chan && (chan.indexOf(',') !== -1 || /\s/.test(chan)))
|
||||
return _('Channel name "%s" cannot contain commas or spaces').format(chan);
|
||||
if (key && (key.indexOf(',') !== -1 || /\s/.test(key)))
|
||||
return _('Channel key cannot contain commas or spaces');
|
||||
if (validateNoExpr(null, chan) !== true || validateNoExpr(null, key) !== true)
|
||||
return _('Channels and keys must not contain "${"');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var CBIAutoJoin = form.Value.extend({
|
||||
validate: function(section_id, value) {
|
||||
var el = document.getElementById(this.cbid(section_id));
|
||||
if (!el || !el.parentNode)
|
||||
return true;
|
||||
|
||||
var res = checkAutoJoinRows(el.parentNode.querySelectorAll('tr[data-channel-row]'));
|
||||
|
||||
/* The modal shows no message for this widget, so show it here */
|
||||
var box = el.parentNode.querySelector('.autojoin-error');
|
||||
if (box) {
|
||||
box.textContent = (res === true) ? '' : res;
|
||||
box.style.display = (res === true) ? 'none' : '';
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
/*
|
||||
* The value lives in a hidden field, which never runs validation on its
|
||||
* own, so let parse() see the result of validate() through these.
|
||||
*/
|
||||
isValid: function(section_id) {
|
||||
return this.validate(section_id, this.formvalue(section_id)) === true;
|
||||
},
|
||||
|
||||
getValidationError: function(section_id) {
|
||||
var res = this.validate(section_id, this.formvalue(section_id));
|
||||
return (res === true) ? '' : res;
|
||||
},
|
||||
|
||||
textvalue: function(section_id) {
|
||||
var val = this.cfgvalue(section_id);
|
||||
var parsed = weechatTools.parseAutoJoin(val);
|
||||
if (parsed.channels.length === 0)
|
||||
return null;
|
||||
|
||||
var nodes = [];
|
||||
parsed.channels.forEach(function(chan, idx) {
|
||||
if (idx > 0)
|
||||
nodes.push(', ');
|
||||
nodes.push(E('code', {}, chan));
|
||||
});
|
||||
return E('span', {}, nodes);
|
||||
},
|
||||
|
||||
renderWidget: function(section_id, option_index, cfgvalue) {
|
||||
var isReadonly = (this.readonly != null) ? this.readonly : this.map.readonly;
|
||||
var hidden = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : '', {
|
||||
id: this.cbid(section_id)
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell' }, _('Channel')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell' }, _('Key')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions' }, '')
|
||||
])
|
||||
]);
|
||||
|
||||
function updateValue() {
|
||||
var rows = table.querySelectorAll('tr[data-channel-row]');
|
||||
var chansWithKeys = [];
|
||||
var chansWithoutKeys = [];
|
||||
|
||||
rows.forEach(function(row) {
|
||||
var chanInput = row.querySelector('.chan-input');
|
||||
var keyInput = row.querySelector('.key-input');
|
||||
var chan = (chanInput ? chanInput.value : '').trim();
|
||||
var key = (keyInput ? keyInput.value : '').trim();
|
||||
|
||||
if (!chan)
|
||||
return;
|
||||
|
||||
if (key)
|
||||
chansWithKeys.push({ chan: chan, key: key });
|
||||
else
|
||||
chansWithoutKeys.push(chan);
|
||||
});
|
||||
|
||||
var allChans = chansWithKeys.map(function(item) { return item.chan; }).concat(chansWithoutKeys);
|
||||
var allKeys = chansWithKeys.map(function(item) { return item.key; });
|
||||
var result = allChans.join(',');
|
||||
if (allKeys.length > 0)
|
||||
result += ' ' + allKeys.join(',');
|
||||
|
||||
hidden.setValue(result);
|
||||
}
|
||||
|
||||
function createRow(channel, key) {
|
||||
var chanInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text chan-input',
|
||||
'placeholder': '#openwrt',
|
||||
'value': channel || '',
|
||||
'disabled': isReadonly || null
|
||||
});
|
||||
|
||||
chanInput.addEventListener('input', function() {
|
||||
if (chanInput.value.indexOf(',') !== -1 || /\s/.test(chanInput.value)) {
|
||||
chanInput.classList.add('cbi-input-invalid');
|
||||
chanInput.title = _('Enter a single channel per row without commas or spaces');
|
||||
} else {
|
||||
chanInput.classList.remove('cbi-input-invalid');
|
||||
chanInput.removeAttribute('title');
|
||||
}
|
||||
updateValue();
|
||||
});
|
||||
|
||||
var keyInput = E('input', {
|
||||
'type': 'password',
|
||||
'class': 'cbi-input-text key-input',
|
||||
'placeholder': _('Optional key'),
|
||||
'value': key || '',
|
||||
'disabled': isReadonly || null
|
||||
});
|
||||
|
||||
keyInput.addEventListener('input', function() {
|
||||
if (keyInput.value.indexOf(',') !== -1 || /\s/.test(keyInput.value)) {
|
||||
keyInput.classList.add('cbi-input-invalid');
|
||||
keyInput.title = _('Channel key cannot contain commas or spaces');
|
||||
} else {
|
||||
keyInput.classList.remove('cbi-input-invalid');
|
||||
keyInput.removeAttribute('title');
|
||||
}
|
||||
updateValue();
|
||||
});
|
||||
|
||||
var btnRemove = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'title': _('Remove'),
|
||||
'disabled': isReadonly || null
|
||||
}, '✕');
|
||||
|
||||
var row = E('tr', {
|
||||
'class': 'tr cbi-section-table-row',
|
||||
'data-channel-row': ''
|
||||
}, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, chanInput),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, keyInput),
|
||||
E('td', { 'class': 'td cbi-section-table-cell cbi-section-actions' }, btnRemove)
|
||||
]);
|
||||
|
||||
btnRemove.addEventListener('click', function() {
|
||||
row.remove();
|
||||
updateValue();
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
var parsed = weechatTools.parseAutoJoin(cfgvalue != null ? cfgvalue : '');
|
||||
if (parsed.channels.length > 0) {
|
||||
parsed.channels.forEach(function(chan, idx) {
|
||||
table.appendChild(createRow(chan, parsed.keys[idx] || ''));
|
||||
});
|
||||
}
|
||||
else {
|
||||
table.appendChild(createRow('', ''));
|
||||
}
|
||||
|
||||
var btnAdd = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'btn cbi-button cbi-button-add',
|
||||
'disabled': isReadonly || null
|
||||
}, _('Add channel'));
|
||||
|
||||
btnAdd.addEventListener('click', function() {
|
||||
var newRow = createRow('', '');
|
||||
table.appendChild(newRow);
|
||||
var input = newRow.querySelector('.chan-input');
|
||||
if (input)
|
||||
input.focus();
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-value-field' }, [
|
||||
table,
|
||||
btnAdd,
|
||||
E('div', { 'class': 'alert-message error autojoin-error', 'style': 'display: none' }),
|
||||
hidden.render()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('weechat', _('WeeChat - IRC Servers'),
|
||||
_('Configure IRC server connections, authentication, and auto-join channels.'));
|
||||
|
||||
s = m.section(form.GridSection, 'server', _('IRC Servers'));
|
||||
s.addremove = true;
|
||||
s.anonymous = false;
|
||||
s.addbtntitle = _('Add Server');
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.sectiontitle = function(section_id) {
|
||||
return section_id;
|
||||
};
|
||||
|
||||
/* Define Tabs */
|
||||
s.tab('connection', _('Connection'));
|
||||
s.tab('identity', _('Identity'));
|
||||
s.tab('auth', _('Authentication'));
|
||||
s.tab('channels', _('Channels'));
|
||||
|
||||
/* Connection settings (table columns + modal Connection tab) */
|
||||
o = s.taboption('connection', form.Value, 'address', _('Server Hostname'),
|
||||
_('Hostname or IP address of the IRC server.'));
|
||||
o.placeholder = 'irc.oftc.net';
|
||||
o.datatype = 'host';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('connection', form.Value, 'port', _('Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = '6697';
|
||||
o.placeholder = '6697';
|
||||
|
||||
o = s.taboption('connection', form.Flag, 'ssl', _('Use TLS'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('connection', form.ListValue, 'ipv6', _('IPv6 Support'),
|
||||
_('IPv6 connection mode for this IRC server.'));
|
||||
o.value('auto', _('Auto'));
|
||||
o.value('force', _('Force IPv6'));
|
||||
o.value('disable', _('Disable IPv6'));
|
||||
o.default = 'auto';
|
||||
o.textvalue = function(section_id) {
|
||||
var val = this.cfgvalue(section_id) || this.default;
|
||||
switch (val) {
|
||||
case 'force': return _('Force IPv6');
|
||||
case 'disable': return _('Disabled');
|
||||
case 'auto':
|
||||
default:
|
||||
return _('Auto');
|
||||
}
|
||||
};
|
||||
|
||||
/* Channels (table column + modal Channels tab) */
|
||||
o = s.taboption('channels', CBIAutoJoin, 'autojoin', _('Auto-join Channels'),
|
||||
_('Channels to join automatically after connecting to this server.'));
|
||||
o.rmempty = true;
|
||||
|
||||
/* Auto-connect & Enabled flags (table columns + modal Connection tab) */
|
||||
o = s.taboption('connection', form.Flag, 'autoconnect', _('Auto-connect'),
|
||||
_('Connect automatically when WeeChat starts.'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.taboption('connection', form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
/* Tab 2: Identity */
|
||||
o = s.taboption('identity', form.Value, 'nicks', _('Nicknames'),
|
||||
_('Comma-separated list of nicknames to try.'));
|
||||
o.placeholder = 'weechat,weechat_';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
o = s.taboption('identity', form.Value, 'username', _('Username'));
|
||||
o.placeholder = 'weechat';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
o = s.taboption('identity', form.Value, 'realname', _('Real Name'));
|
||||
o.placeholder = 'WeeChat User';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
/* Tab 3: Authentication */
|
||||
o = s.taboption('auth', form.ListValue, 'auth_method', _('Authentication Method'),
|
||||
_('Select the authentication method required by the IRC network.'));
|
||||
o.value('none', _('None'));
|
||||
o.value('nickserv', _('NickServ (/msg nickserv identify)'));
|
||||
o.value('sasl_plain', _('SASL PLAIN'));
|
||||
o.value('sasl_scram_sha_256', _('SASL SCRAM-SHA-256'));
|
||||
o.value('sasl_scram_sha_512', _('SASL SCRAM-SHA-512'));
|
||||
o.default = 'none';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.taboption('auth', form.Value, 'sasl_username', _('SASL Username'),
|
||||
_('Account username if different from nickname.'));
|
||||
o.depends('auth_method', 'sasl_plain');
|
||||
o.depends('auth_method', 'sasl_scram_sha_256');
|
||||
o.depends('auth_method', 'sasl_scram_sha_512');
|
||||
o.modalonly = true;
|
||||
/* Same default as the init script: the first nickname, then the username */
|
||||
o.validate = function(section_id, value) {
|
||||
var nick = (this.section.formvalue(section_id, 'nicks') || '').split(',')[0];
|
||||
var account = value || nick || this.section.formvalue(section_id, 'username') || '';
|
||||
if (!account.trim())
|
||||
return _('Enter a SASL username or a nickname');
|
||||
return validateNoExpr(section_id, value);
|
||||
};
|
||||
|
||||
o = s.taboption('auth', form.Value, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends('auth_method', 'nickserv');
|
||||
o.depends('auth_method', 'sasl_plain');
|
||||
o.depends('auth_method', 'sasl_scram_sha_256');
|
||||
o.depends('auth_method', 'sasl_scram_sha_512');
|
||||
o.modalonly = true;
|
||||
o.validate = function(section_id, value) {
|
||||
var method = this.section.formvalue(section_id, 'auth_method');
|
||||
|
||||
if (!method || method === 'none')
|
||||
return true;
|
||||
if (!value || !value.trim())
|
||||
return _('A password is required for this authentication method');
|
||||
/* WeeChat replaces these in the NickServ command after decoding the password */
|
||||
if (method === 'nickserv' && /\$(nick|channel|server)/.test(value))
|
||||
return _('A NickServ password cannot contain $nick, $channel or $server');
|
||||
return true;
|
||||
};
|
||||
|
||||
return m.render().then(L.bind(function(nodes) {
|
||||
var target = null;
|
||||
var hashMatch = (window.location.hash || '').match(/#(?:edit|server)=([^&]+)/);
|
||||
if (hashMatch) {
|
||||
target = decodeURIComponent(hashMatch[1]);
|
||||
} else {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
target = params.get('server') || params.get('edit');
|
||||
}
|
||||
|
||||
if (target && (m.data.get('weechat', target) || uci.get('weechat', target))) {
|
||||
if (window.history && window.history.replaceState) {
|
||||
var cleanSearch = window.location.search ? window.location.search.replace(/[?&](?:server|edit)=[^&]*/g, '').replace(/^&/, '?') : '';
|
||||
window.history.replaceState(null, '', window.location.pathname + cleanSearch);
|
||||
}
|
||||
s.renderMoreOptionsModal(target);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, this));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require tools.weechat as weechatTools';
|
||||
|
||||
function renderEnabledState(isEnabled) {
|
||||
return [
|
||||
isEnabled
|
||||
? E('span', { 'class': 'label success', 'style': 'margin-right: 8px;' }, _('Enabled'))
|
||||
: E('span', { 'class': 'label danger', 'style': 'margin-right: 8px;' }, _('Disabled')),
|
||||
E('span', { 'class': 'cbi-value-description' },
|
||||
isEnabled ? _('Service is allowed to start.') : _('Service is disabled in configuration.'))
|
||||
];
|
||||
}
|
||||
|
||||
function getServiceStatus(serviceData) {
|
||||
var isRunning = false,
|
||||
pid = null;
|
||||
|
||||
if (serviceData && serviceData.weechat && serviceData.weechat.instances) {
|
||||
var instances = serviceData.weechat.instances;
|
||||
for (var inst in instances) {
|
||||
if (instances[inst].running) {
|
||||
isRunning = true;
|
||||
pid = instances[inst].pid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { running: isRunning, pid: pid };
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
callServiceList: rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callRcList: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'list',
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callRcInit: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: ['name', 'action']
|
||||
}),
|
||||
|
||||
callDaemonInfo: rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'get_daemon_info',
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callEnableService: rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'enable_service',
|
||||
expect: { success: false }
|
||||
}),
|
||||
|
||||
/*
|
||||
* The init script only starts WeeChat with option enabled '1'. Let the
|
||||
* backend commit just that option, uci.apply() would apply every change
|
||||
* staged in this session, for any config.
|
||||
*/
|
||||
ensureEnabled: function() {
|
||||
if (uci.get('weechat', 'weechat', 'enabled') === '1')
|
||||
return Promise.resolve();
|
||||
|
||||
return this.callEnableService().then(function(success) {
|
||||
if (!success)
|
||||
throw new Error(_('Could not enable the service in /etc/config/weechat'));
|
||||
|
||||
var cell = document.getElementById('weechat-service-enabled');
|
||||
if (cell) {
|
||||
while (cell.firstChild)
|
||||
cell.removeChild(cell.firstChild);
|
||||
renderEnabledState(true).forEach(function(node) { cell.appendChild(node); });
|
||||
}
|
||||
|
||||
uci.unload('weechat');
|
||||
return uci.load('weechat');
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('weechat'),
|
||||
this.callServiceList('weechat'),
|
||||
this.callRcList(),
|
||||
L.resolveDefault(this.callDaemonInfo(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
handleServiceAction: function(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, action) {
|
||||
btnStart.disabled = true;
|
||||
btnRestart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
|
||||
var ensureEnabled = (action === 'start') ? this.ensureEnabled() : Promise.resolve();
|
||||
|
||||
return ensureEnabled.then(L.bind(function() {
|
||||
return this.callRcInit('weechat', action);
|
||||
}, this)).then(L.bind(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
window.setTimeout(resolve, 1000);
|
||||
});
|
||||
}, this)).then(L.bind(function() {
|
||||
return this.updateServiceStatus(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly);
|
||||
}, this)).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/weechat %s": %s').format(action, e.message || e)));
|
||||
});
|
||||
},
|
||||
|
||||
handleToggleAutostart: function(enable, autostartBadge, autostartBtn, isReadonly, ev) {
|
||||
var action = enable ? 'enable' : 'disable';
|
||||
autostartBtn.disabled = true;
|
||||
|
||||
var prepare = enable ? this.ensureEnabled() : Promise.resolve();
|
||||
|
||||
return prepare.then(L.bind(function() {
|
||||
return this.callRcInit('weechat', action);
|
||||
}, this)).then(L.bind(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
|
||||
autostartBadge.className = 'label ' + (enable ? 'success' : '');
|
||||
autostartBadge.textContent = enable ? _('Enabled') : _('Disabled');
|
||||
autostartBtn.textContent = enable ? _('Disable Autostart') : _('Enable Autostart');
|
||||
autostartBtn.disabled = isReadonly ? true : false;
|
||||
autostartBtn.onclick = isReadonly ? null : ui.createHandlerFn(this, 'handleToggleAutostart', !enable, autostartBadge, autostartBtn, isReadonly);
|
||||
}, this)).catch(function(e) {
|
||||
autostartBtn.disabled = false;
|
||||
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/weechat %s": %s').format(action, e.message || e)));
|
||||
});
|
||||
},
|
||||
|
||||
updateServiceStatus: function(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly) {
|
||||
return this.callServiceList('weechat').then(function(res) {
|
||||
var st = getServiceStatus(res);
|
||||
var isRunning = st.running;
|
||||
var pid = st.pid;
|
||||
|
||||
statusBadge.className = 'label ' + (isRunning ? 'success' : 'danger');
|
||||
statusBadge.textContent = isRunning ? _('Running') : _('Stopped');
|
||||
pidLabel.textContent = (isRunning && pid) ? ('PID: ' + pid) : '';
|
||||
|
||||
btnStart.disabled = (isRunning || isReadonly);
|
||||
btnRestart.disabled = (!isRunning || isReadonly);
|
||||
btnStop.disabled = (!isRunning || isReadonly);
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var serviceData = data[1] || {},
|
||||
rcData = data[2] || {},
|
||||
daemonInfo = data[3] || {},
|
||||
isReadonly = !L.hasViewPermission(),
|
||||
st = getServiceStatus(serviceData),
|
||||
isRunning = st.running,
|
||||
pid = st.pid,
|
||||
isAutostart = !!(rcData && rcData.weechat && rcData.weechat.enabled),
|
||||
isEnabled = (uci.get('weechat', 'weechat', 'enabled') === '1'),
|
||||
version = (daemonInfo && daemonInfo.version) ? daemonInfo.version : '-';
|
||||
|
||||
var statusBadge = E('span', {
|
||||
'class': 'label ' + (isRunning ? 'success' : 'danger'),
|
||||
'style': 'padding: 4px 10px; font-size: 90%; font-weight: bold; border-radius: 4px;'
|
||||
}, isRunning ? _('Running') : _('Stopped'));
|
||||
|
||||
var pidLabel = E('span', {
|
||||
'class': 'cbi-value-description',
|
||||
'style': 'margin-left: 8px; font-weight: normal;'
|
||||
}, (isRunning && pid) ? ('PID: ' + pid) : '');
|
||||
|
||||
var autostartBadge = E('span', {
|
||||
'class': 'label ' + (isAutostart ? 'success' : ''),
|
||||
'style': 'margin-right: 12px;'
|
||||
}, isAutostart ? _('Enabled') : _('Disabled'));
|
||||
|
||||
var autostartBtn = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'disabled': isReadonly ? 'disabled' : null
|
||||
}, isAutostart ? _('Disable Autostart') : _('Enable Autostart'));
|
||||
|
||||
if (!isReadonly) {
|
||||
autostartBtn.onclick = ui.createHandlerFn(this, 'handleToggleAutostart', !isAutostart, autostartBadge, autostartBtn, isReadonly);
|
||||
}
|
||||
|
||||
var btnStart = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action weechat-btn-control',
|
||||
'disabled': (isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Start'));
|
||||
|
||||
var btnRestart = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-reload weechat-btn-control',
|
||||
'disabled': (!isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Restart'));
|
||||
|
||||
var btnStop = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative weechat-btn-control',
|
||||
'disabled': (!isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Stop'));
|
||||
|
||||
if (!isReadonly) {
|
||||
btnStart.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'start'));
|
||||
btnRestart.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'restart'));
|
||||
btnStop.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'stop'));
|
||||
}
|
||||
|
||||
var relayEnabled = (uci.get('weechat', 'weechat', 'relay_enabled') === '1'),
|
||||
relayPort = uci.get('weechat', 'weechat', 'relay_port') || '9000',
|
||||
relayTls = (uci.get('weechat', 'weechat', 'relay_tls') === '1');
|
||||
|
||||
var servers = uci.sections('weechat', 'server'),
|
||||
enabledCount = 0;
|
||||
|
||||
servers.forEach(function(srv) {
|
||||
if (srv.enabled !== '0' && srv.enabled !== false && srv.enabled !== 'off')
|
||||
enabledCount++;
|
||||
});
|
||||
|
||||
var relayStatusNode;
|
||||
if (relayEnabled) {
|
||||
var clientBtn = E('a', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'href': 'https://www.glowing-bear.org/',
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'display: inline-block; margin-top: 6px;'
|
||||
}, [ _('Open Web Client (Hosted)'), ' ↗' ]);
|
||||
|
||||
relayStatusNode = E('div', {}, [
|
||||
E('div', {}, [
|
||||
E('span', { 'class': 'label success' }, _('Enabled'))
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value-description', 'style': 'margin-top: 4px;' },
|
||||
_('Port %s · TLS %s').format(relayPort, relayTls ? _('enabled') : _('disabled'))),
|
||||
E('div', { 'style': 'margin-top: 6px;' }, clientBtn)
|
||||
]);
|
||||
} else {
|
||||
relayStatusNode = E('div', {}, [
|
||||
E('span', { 'class': 'label' }, _('Disabled'))
|
||||
]);
|
||||
}
|
||||
|
||||
var ircStatusNode = E('div', {}, [
|
||||
E('span', {},
|
||||
_('%d configured · %d enabled').format(servers.length, enabledCount))
|
||||
]);
|
||||
|
||||
var serverRows = [];
|
||||
if (servers.length === 0) {
|
||||
serverRows.push(E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'colspan': 8, 'class': 'td cbi-section-table-cell center' },
|
||||
_('No IRC servers configured. Go to Servers tab to add one.'))
|
||||
]));
|
||||
} else {
|
||||
servers.forEach(function(s) {
|
||||
var isSrvEnabled = (s.enabled !== '0' && s.enabled !== false && s.enabled !== 'off');
|
||||
var isSrvSsl = (s.ssl !== '0' && s.ssl !== false && s.ssl !== 'off');
|
||||
var parsed = weechatTools.parseAutoJoin(s.autojoin);
|
||||
var chans = parsed.channels;
|
||||
|
||||
var chanNodes = [];
|
||||
if (chans.length > 0) {
|
||||
chans.forEach(function(chan) {
|
||||
chanNodes.push(E('code', {
|
||||
'style': 'margin: 2px 4px 2px 0; display: inline-block; padding: 2px 6px; border-radius: 3px;'
|
||||
}, chan));
|
||||
});
|
||||
} else {
|
||||
chanNodes.push(E('em', { 'class': 'cbi-value-description' }, _('no channels')));
|
||||
}
|
||||
|
||||
serverRows.push(E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell' },
|
||||
isSrvEnabled
|
||||
? E('span', { 'class': 'label success' }, _('Enabled'))
|
||||
: E('span', { 'class': 'label' }, _('Disabled'))),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, E('strong', {}, s['.name'])),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.address || '-'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.port || '6697'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, isSrvSsl ? _('Yes') : _('No')),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.nicks || s.username || '-'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, chanNodes),
|
||||
E('td', { 'class': 'td cbi-section-table-cell cbi-section-actions', 'style': 'text-align: right; white-space: nowrap;' }, [
|
||||
E('a', {
|
||||
'class': 'btn cbi-button cbi-button-edit',
|
||||
'href': L.url('admin/services/weechat/servers') + '?server=' + encodeURIComponent(s['.name']) + '#edit=' + encodeURIComponent(s['.name']),
|
||||
'title': _('Edit server'),
|
||||
'aria-label': _('Edit server')
|
||||
}, '✎')
|
||||
])
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
poll.add(L.bind(function() {
|
||||
return this.updateServiceStatus(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly);
|
||||
}, this), 5);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/weechat/weechat.css') }),
|
||||
|
||||
E('h2', {}, _('WeeChat - Status')),
|
||||
E('div', { 'class': 'cbi-map-descr' },
|
||||
_('WeeChat runs as a headless daemon on your router. It keeps you connected to IRC networks and exposes a secure Relay API for web or mobile clients.')),
|
||||
|
||||
/* Two cards grid */
|
||||
E('div', { 'class': 'weechat-status-grid' }, [
|
||||
/* Card 1: Service Status */
|
||||
E('div', { 'class': 'weechat-panel' }, [
|
||||
E('h3', {}, _('Service Status')),
|
||||
E('table', {}, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'style': 'width: 35%;' }, _('WeeChat Daemon')),
|
||||
E('td', { 'class': 'td left' }, [ statusBadge, pidLabel ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Version')),
|
||||
E('td', { 'class': 'td left' }, E('span', {}, version))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Service enabled')),
|
||||
E('td', { 'class': 'td left', 'id': 'weechat-service-enabled' }, renderEnabledState(isEnabled))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Autostart at boot')),
|
||||
E('td', { 'class': 'td left' }, [ autostartBadge, autostartBtn ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Service control')),
|
||||
E('td', { 'class': 'td left' }, [ btnStart, btnRestart, btnStop ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Card 2: Overview */
|
||||
E('div', { 'class': 'weechat-panel' }, [
|
||||
E('h3', {}, _('Overview')),
|
||||
E('table', {}, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'style': 'width: 30%;' }, _('Relay server')),
|
||||
E('td', { 'class': 'td left' }, relayStatusNode)
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('IRC servers')),
|
||||
E('td', { 'class': 'td left' }, ircStatusNode)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Configured IRC Servers Table */
|
||||
E('div', { 'class': 'weechat-servers-panel' }, [
|
||||
E('h3', {}, _('Configured IRC Servers')),
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 10%; white-space: nowrap;' }, _('Status')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; white-space: nowrap;' }, _('Server')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 20%; white-space: nowrap;' }, _('Hostname')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 7%; white-space: nowrap;' }, _('Port')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 7%; white-space: nowrap;' }, _('TLS')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 13%; white-space: nowrap;' }, _('Nicknames')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 26%;' }, _('Channels')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions', 'style': 'width: 5%; text-align: right;' }, '')
|
||||
])
|
||||
].concat(serverRows))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
.weechat-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.weechat-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.weechat-panel {
|
||||
border: 1px solid rgba(128, 128, 128, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(128, 128, 128, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.weechat-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weechat-panel table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
border-collapse: collapse;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.weechat-panel td {
|
||||
padding: 10px 4px;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.weechat-servers-panel {
|
||||
border: 1px solid rgba(128, 128, 128, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(128, 128, 128, 0.02);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.weechat-servers-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weechat-btn-control {
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
Reference in new issue
Block a user