Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
399 lines
11 KiB
JavaScript
399 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require poll';
|
|
'require rpc';
|
|
'require ui';
|
|
'require form';
|
|
|
|
var callMeshnameStatus = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'status',
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameList = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'list',
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameAnnounce = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'announce',
|
|
params: ['name', 'port'],
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameRevoke = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'revoke',
|
|
params: ['name'],
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameResolve = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'resolve',
|
|
params: ['domain'],
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameSync = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'sync',
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameGetConfig = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'get_config',
|
|
expect: { }
|
|
});
|
|
|
|
var callMeshnameSetConfig = rpc.declare({
|
|
object: 'luci.meshname',
|
|
method: 'set_config',
|
|
params: ['enabled', 'auto_announce', 'sync_interval', 'gossip_enabled'],
|
|
expect: { }
|
|
});
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callMeshnameStatus(),
|
|
callMeshnameList(),
|
|
callMeshnameGetConfig()
|
|
]);
|
|
},
|
|
|
|
formatIPv6: function(ipv6) {
|
|
if (!ipv6) return '-';
|
|
// Truncate long IPv6 for display
|
|
if (ipv6.length > 25) {
|
|
return ipv6.substring(0, 20) + '...';
|
|
}
|
|
return ipv6;
|
|
},
|
|
|
|
renderStatus: function(status) {
|
|
var isRunning = status.running === '1';
|
|
var isEnabled = status.enabled === '1';
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Meshname DNS Status'),
|
|
E('div', { 'class': 'table' }, [
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, 'Service Status'),
|
|
E('div', { 'class': 'td' }, [
|
|
E('span', {
|
|
'class': isRunning ? 'badge success' : 'badge warning',
|
|
'style': 'padding: 4px 8px; border-radius: 4px; font-size: 12px; ' +
|
|
(isRunning ? 'background: #28a745; color: white;' : 'background: #ffc107; color: black;')
|
|
}, isRunning ? 'Running' : 'Stopped')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, 'Yggdrasil IPv6'),
|
|
E('div', { 'class': 'td' }, [
|
|
E('code', {}, status.ygg_ipv6 || 'Not available')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, 'Local Services'),
|
|
E('div', { 'class': 'td' }, String(status.local_count || 0))
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, 'Known Domains'),
|
|
E('div', { 'class': 'td' }, String(status.domain_count || 0))
|
|
]),
|
|
E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, 'Last Sync'),
|
|
E('div', { 'class': 'td' }, status.last_sync || 'Never')
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderLocalServices: function(domains) {
|
|
var self = this;
|
|
var localServices = (domains.local || []);
|
|
|
|
var rows = localServices.map(function(svc) {
|
|
return E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, [
|
|
E('strong', {}, svc.fqdn)
|
|
]),
|
|
E('div', { 'class': 'td' }, [
|
|
E('code', {}, self.formatIPv6(svc.ipv6))
|
|
]),
|
|
E('div', { 'class': 'td' }, svc.port > 0 ? String(svc.port) : '-'),
|
|
E('div', { 'class': 'td' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-red',
|
|
'click': function() {
|
|
self.handleRevoke(svc.name);
|
|
}
|
|
}, 'Revoke')
|
|
])
|
|
]);
|
|
});
|
|
|
|
if (rows.length === 0) {
|
|
rows.push(E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td', 'colspan': '4', 'style': 'text-align: center; color: #666;' },
|
|
'No local services announced')
|
|
]));
|
|
}
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Local Services'),
|
|
E('div', { 'class': 'cbi-section-node' }, [
|
|
E('div', { 'class': 'table' }, [
|
|
E('div', { 'class': 'tr cbi-section-table-titles' }, [
|
|
E('div', { 'class': 'th' }, 'Domain'),
|
|
E('div', { 'class': 'th' }, 'IPv6'),
|
|
E('div', { 'class': 'th' }, 'Port'),
|
|
E('div', { 'class': 'th' }, 'Actions')
|
|
])
|
|
].concat(rows))
|
|
]),
|
|
E('div', { 'style': 'margin-top: 10px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': function() { self.showAnnounceModal(); }
|
|
}, 'Announce Service')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderRemoteDomains: function(domains) {
|
|
var self = this;
|
|
var remoteDomains = (domains.remote || []);
|
|
|
|
var rows = remoteDomains.map(function(dom) {
|
|
var originShort = dom.origin ? dom.origin.substring(0, 16) + '...' : 'unknown';
|
|
return E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td' }, [
|
|
E('strong', {}, dom.fqdn)
|
|
]),
|
|
E('div', { 'class': 'td' }, [
|
|
E('code', {}, self.formatIPv6(dom.ipv6))
|
|
]),
|
|
E('div', { 'class': 'td' }, dom.port > 0 ? String(dom.port) : '-'),
|
|
E('div', { 'class': 'td', 'title': dom.origin || '' }, originShort)
|
|
]);
|
|
});
|
|
|
|
if (rows.length === 0) {
|
|
rows.push(E('div', { 'class': 'tr' }, [
|
|
E('div', { 'class': 'td', 'colspan': '4', 'style': 'text-align: center; color: #666;' },
|
|
'No remote domains discovered yet')
|
|
]));
|
|
}
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Known Domains (from Mesh)'),
|
|
E('div', { 'class': 'cbi-section-node' }, [
|
|
E('div', { 'class': 'table' }, [
|
|
E('div', { 'class': 'tr cbi-section-table-titles' }, [
|
|
E('div', { 'class': 'th' }, 'Domain'),
|
|
E('div', { 'class': 'th' }, 'IPv6'),
|
|
E('div', { 'class': 'th' }, 'Port'),
|
|
E('div', { 'class': 'th' }, 'Origin')
|
|
])
|
|
].concat(rows))
|
|
]),
|
|
E('div', { 'style': 'margin-top: 10px;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': function() { self.handleSync(); }
|
|
}, 'Force Sync')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderResolveTest: function() {
|
|
var self = this;
|
|
var inputId = 'meshname-resolve-input';
|
|
var resultId = 'meshname-resolve-result';
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Resolve Test'),
|
|
E('div', { 'style': 'display: flex; gap: 10px; align-items: center;' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': inputId,
|
|
'placeholder': 'Enter domain (e.g., myservice.ygg)',
|
|
'style': 'flex: 1; padding: 8px;'
|
|
}),
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-blue',
|
|
'click': function() {
|
|
var domain = document.getElementById(inputId).value;
|
|
if (domain) {
|
|
self.handleResolve(domain, resultId);
|
|
}
|
|
}
|
|
}, 'Resolve')
|
|
]),
|
|
E('div', { 'id': resultId, 'style': 'margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; display: none;' })
|
|
]);
|
|
},
|
|
|
|
showAnnounceModal: function() {
|
|
var self = this;
|
|
|
|
ui.showModal('Announce Service', [
|
|
E('p', {}, 'Announce a local service with a .ygg domain:'),
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, 'Service Name'),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'meshname-announce-name',
|
|
'placeholder': 'myservice',
|
|
'style': 'width: 100%;'
|
|
}),
|
|
E('div', { 'class': 'cbi-value-description' }, 'Will be accessible as <name>.ygg')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-value' }, [
|
|
E('label', { 'class': 'cbi-value-title' }, 'Port (optional)'),
|
|
E('div', { 'class': 'cbi-value-field' }, [
|
|
E('input', {
|
|
'type': 'number',
|
|
'id': 'meshname-announce-port',
|
|
'placeholder': '8080',
|
|
'style': 'width: 100%;'
|
|
})
|
|
])
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
' ',
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'click': function() {
|
|
var name = document.getElementById('meshname-announce-name').value;
|
|
var port = document.getElementById('meshname-announce-port').value || '0';
|
|
if (name) {
|
|
self.handleAnnounce(name, parseInt(port));
|
|
}
|
|
}
|
|
}, 'Announce')
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleAnnounce: function(name, port) {
|
|
var self = this;
|
|
ui.hideModal();
|
|
ui.showModal('Announcing...', [
|
|
E('p', { 'class': 'spinning' }, 'Announcing ' + name + '.ygg...')
|
|
]);
|
|
|
|
callMeshnameAnnounce(name, port).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.status === 'ok') {
|
|
ui.addNotification(null, E('p', {}, 'Successfully announced ' + result.fqdn), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + (result.error || 'Unknown error')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error');
|
|
});
|
|
},
|
|
|
|
handleRevoke: function(name) {
|
|
var self = this;
|
|
|
|
ui.showModal('Confirm Revoke', [
|
|
E('p', {}, 'Are you sure you want to revoke ' + name + '.ygg?'),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
' ',
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-red',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
callMeshnameRevoke(name).then(function(result) {
|
|
if (result.status === 'ok') {
|
|
ui.addNotification(null, E('p', {}, 'Revoked ' + name + '.ygg'), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + (result.error || 'Unknown error')), 'error');
|
|
}
|
|
});
|
|
}
|
|
}, 'Revoke')
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleResolve: function(domain, resultId) {
|
|
var resultDiv = document.getElementById(resultId);
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.innerHTML = '<span class="spinning">Resolving...</span>';
|
|
|
|
callMeshnameResolve(domain).then(function(result) {
|
|
if (result.status === 'ok') {
|
|
resultDiv.innerHTML = '<strong>' + result.domain + '</strong> resolves to: <code>' + result.ipv6 + '</code>';
|
|
resultDiv.style.background = '#d4edda';
|
|
} else {
|
|
resultDiv.innerHTML = 'Cannot resolve: ' + (result.error || 'Unknown error');
|
|
resultDiv.style.background = '#f8d7da';
|
|
}
|
|
}).catch(function(err) {
|
|
resultDiv.innerHTML = 'Error: ' + err.message;
|
|
resultDiv.style.background = '#f8d7da';
|
|
});
|
|
},
|
|
|
|
handleSync: function() {
|
|
ui.showModal('Syncing...', [
|
|
E('p', { 'class': 'spinning' }, 'Synchronizing with mesh peers...')
|
|
]);
|
|
|
|
callMeshnameSync().then(function(result) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Sync initiated'), 'info');
|
|
setTimeout(function() { window.location.reload(); }, 2000);
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error');
|
|
});
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var domains = data[1] || { local: [], remote: [] };
|
|
var config = data[2] || {};
|
|
|
|
return E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, 'Meshname DNS'),
|
|
E('div', { 'class': 'cbi-map-descr' },
|
|
'Decentralized DNS for .ygg domains over Yggdrasil mesh network. ' +
|
|
'Services announce themselves via gossip protocol.'),
|
|
this.renderStatus(status),
|
|
this.renderLocalServices(domains),
|
|
this.renderRemoteDomains(domains),
|
|
this.renderResolveTest()
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|