secubox-openwrt/package/secubox/luci-app-wireguard-dashboard/htdocs/luci-static/resources/view/wireguard-dashboard/uplinks.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
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>
2026-03-12 11:09:34 +01:00

614 lines
21 KiB
JavaScript

'use strict';
'require view';
'require poll';
'require dom';
'require ui';
'require wireguard-dashboard/api as API';
'require secubox/kiss-theme';
return view.extend({
title: _('WireGuard Uplinks'),
pollInterval: 10,
pollActive: true,
load: function() {
return Promise.all([
API.getUplinkStatus(),
API.getUplinks(),
API.getPeers()
]);
},
// Handle offer uplink
handleOfferUplink: function(ev) {
var self = this;
ui.showModal(_('Offer Uplink'), [
E('p', {}, _('Offer your internet connection to mesh peers as a backup uplink.')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Bandwidth (Mbps)')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'offer-bandwidth',
'class': 'cbi-input-text',
'value': '100',
'min': '1',
'max': '10000'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Latency (ms)')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'offer-latency',
'class': 'cbi-input-text',
'value': '10',
'min': '1',
'max': '1000'
})
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'click': function() {
var bandwidth = document.getElementById('offer-bandwidth').value;
var latency = document.getElementById('offer-latency').value;
ui.hideModal();
ui.showModal(_('Offering Uplink'), [
E('p', { 'class': 'spinning' }, _('Advertising uplink to mesh...'))
]);
API.offerUplink(bandwidth, latency).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Uplink offered successfully')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to offer uplink')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
}
}, _('Offer Uplink'))
])
]);
},
// Handle withdraw uplink
handleWithdrawUplink: function(ev) {
ui.showModal(_('Withdrawing Uplink'), [
E('p', { 'class': 'spinning' }, _('Withdrawing uplink offer from mesh...'))
]);
API.withdrawUplink().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Uplink withdrawn')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to withdraw uplink')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
// Handle add uplink from peer offer
handleAddUplink: function(offer, ev) {
var self = this;
ui.showModal(_('Add Uplink'), [
E('p', {}, _('Use this mesh peer as a backup internet uplink.')),
E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [
E('div', {}, [
E('strong', {}, _('Node: ')),
E('span', {}, offer.node_id || 'Unknown')
]),
E('div', {}, [
E('strong', {}, _('Bandwidth: ')),
E('span', {}, (offer.bandwidth || '?') + ' Mbps')
]),
E('div', {}, [
E('strong', {}, _('Latency: ')),
E('span', {}, (offer.latency || '?') + ' ms')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Priority')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'uplink-priority',
'class': 'cbi-input-text',
'value': '10',
'min': '1',
'max': '100'
}),
E('div', { 'class': 'cbi-value-description' }, _('Lower = higher priority for failover'))
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'click': function() {
var priority = document.getElementById('uplink-priority').value;
ui.hideModal();
ui.showModal(_('Adding Uplink'), [
E('p', { 'class': 'spinning' }, _('Creating uplink interface...'))
]);
API.addUplink(
offer.public_key,
offer.endpoint,
'', // local_pubkey (auto-generated)
priority,
'1', // weight
offer.node_id
).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Uplink added successfully')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to add uplink')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
}
}, _('Add Uplink'))
])
]);
},
// Handle remove uplink
handleRemoveUplink: function(uplink, ev) {
var self = this;
ui.showModal(_('Remove Uplink'), [
E('p', {}, _('Are you sure you want to remove this uplink?')),
E('div', { 'style': 'background: #f8f9fa; padding: 1em; border-radius: 4px; margin: 1em 0;' }, [
E('strong', {}, _('Interface: ')),
E('code', {}, uplink.interface)
]),
E('p', { 'style': 'color: #dc3545;' }, _('This will disconnect the backup uplink.')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': function() {
ui.hideModal();
ui.showModal(_('Removing Uplink'), [
E('p', { 'class': 'spinning' }, _('Removing uplink interface...'))
]);
API.removeUplink(uplink.interface).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Uplink removed')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to remove uplink')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
}
}, _('Remove'))
])
]);
},
// Handle test uplink
handleTestUplink: function(uplink, ev) {
ui.showModal(_('Testing Uplink'), [
E('p', { 'class': 'spinning' }, _('Testing connectivity via %s...').format(uplink.interface))
]);
API.testUplink(uplink.interface, '8.8.8.8').then(function(result) {
ui.hideModal();
if (result.reachable) {
ui.showModal(_('Uplink Test Result'), [
E('div', { 'style': 'text-align: center; padding: 1em;' }, [
E('div', { 'style': 'font-size: 4em; color: #28a745;' }, '✓'),
E('h3', { 'style': 'color: #28a745;' }, _('Uplink Working')),
E('p', {}, _('Latency: %s ms').format(result.latency_ms || '?'))
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
} else {
ui.showModal(_('Uplink Test Result'), [
E('div', { 'style': 'text-align: center; padding: 1em;' }, [
E('div', { 'style': 'font-size: 4em; color: #dc3545;' }, '✗'),
E('h3', { 'style': 'color: #dc3545;' }, _('Uplink Unreachable')),
E('p', {}, result.error || _('Target not reachable through this uplink'))
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
])
]);
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
// Handle set priority
handleSetPriority: function(uplink, ev) {
var self = this;
ui.showModal(_('Set Priority'), [
E('p', {}, _('Set failover priority for this uplink.')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Priority')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'set-priority',
'class': 'cbi-input-text',
'value': uplink.priority || '10',
'min': '1',
'max': '100'
}),
E('div', { 'class': 'cbi-value-description' }, _('Lower = higher priority'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Weight')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'set-weight',
'class': 'cbi-input-text',
'value': uplink.weight || '1',
'min': '1',
'max': '100'
}),
E('div', { 'class': 'cbi-value-description' }, _('Load balancing weight'))
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'click': function() {
var priority = document.getElementById('set-priority').value;
var weight = document.getElementById('set-weight').value;
ui.hideModal();
API.setUplinkPriority(uplink.interface, priority, weight).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', _('Priority updated')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to update priority')), 'error');
}
}).catch(function(err) {
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
}
}, _('Save'))
])
]);
},
// Toggle failover
handleToggleFailover: function(enabled, ev) {
API.setUplinkFailover(enabled ? '1' : '0').then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', enabled ? _('Auto-failover enabled') : _('Auto-failover disabled')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to update failover setting')), 'error');
}
}).catch(function(err) {
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return Promise.all([
API.getUplinkStatus(),
API.getUplinks()
]).then(L.bind(function(results) {
var status = results[0] || {};
var uplinksData = results[1] || [];
var uplinks = Array.isArray(uplinksData) ? uplinksData : (uplinksData.uplinks || []);
// Update status badges
var enabledBadge = document.querySelector('.uplink-enabled-badge');
if (enabledBadge) {
var enabled = status.enabled === '1' || status.enabled === 1;
enabledBadge.className = 'badge uplink-enabled-badge ' + (enabled ? 'badge-success' : 'badge-secondary');
enabledBadge.textContent = enabled ? 'Enabled' : 'Disabled';
}
var countBadge = document.querySelector('.uplink-count');
if (countBadge) {
countBadge.textContent = status.uplink_count || 0;
}
var offerBadge = document.querySelector('.offer-count');
if (offerBadge) {
var offers = status.peer_offers || [];
offerBadge.textContent = offers.length;
}
}, this));
}, this), this.pollInterval);
},
render: function(data) {
var self = this;
var status = data[0] || {};
var uplinksData = data[1] || [];
var peersData = data[2] || [];
var uplinks = Array.isArray(uplinksData) ? uplinksData : (uplinksData.uplinks || []);
var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []);
var enabled = status.enabled === '1' || status.enabled === 1;
var offering = status.offering === '1' || status.offering === 1;
var autoFailover = status.auto_failover === '1' || status.auto_failover === 1;
var peerOffers = status.peer_offers || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('h2', {}, _('WireGuard Mesh Uplinks')),
E('div', { 'class': 'cbi-map-descr' },
_('Use WireGuard mesh peers as backup internet uplinks with automatic failover via MWAN3.')),
// Status Cards
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1em; margin-bottom: 1em;' }, [
// Uplink Status
E('div', { 'style': 'background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 1.5em; border-radius: 12px;' }, [
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Uplink Status')),
E('div', { 'style': 'font-size: 2em; font-weight: bold;' }, [
E('span', { 'class': 'badge uplink-enabled-badge ' + (enabled ? 'badge-success' : 'badge-secondary'), 'style': 'font-size: 0.5em;' },
enabled ? 'Enabled' : 'Disabled')
]),
E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' },
autoFailover ? '✓ Auto-failover active' : '○ Manual mode')
]),
// Active Uplinks
E('div', { 'style': 'background: linear-gradient(135deg, #11998e, #38ef7d); color: white; padding: 1.5em; border-radius: 12px;' }, [
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Active Uplinks')),
E('div', { 'style': 'font-size: 2.5em; font-weight: bold;' }, [
E('span', { 'class': 'uplink-count' }, status.uplink_count || 0)
]),
E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' },
_('Configured backup routes'))
]),
// Peer Offers
E('div', { 'style': 'background: linear-gradient(135deg, #f093fb, #f5576c); color: white; padding: 1.5em; border-radius: 12px;' }, [
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Mesh Offers')),
E('div', { 'style': 'font-size: 2.5em; font-weight: bold;' }, [
E('span', { 'class': 'offer-count' }, peerOffers.length)
]),
E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' },
_('Available from peers'))
]),
// Provider Status
E('div', { 'style': 'background: linear-gradient(135deg, #4facfe, #00f2fe); color: white; padding: 1.5em; border-radius: 12px;' }, [
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9;' }, _('Provider Mode')),
E('div', { 'style': 'font-size: 2em; font-weight: bold;' }, [
E('span', {}, offering ? '📡 Offering' : '📴 Not Offering')
]),
E('div', { 'style': 'font-size: 0.85em; margin-top: 0.5em;' },
offering ? _('Sharing uplink with mesh') : _('Not sharing uplink'))
])
])
]),
// Quick Actions
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.5em; margin-bottom: 1em;' }, [
offering ?
E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': L.bind(this.handleWithdrawUplink, this)
}, '📴 ' + _('Stop Offering')) :
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'click': L.bind(this.handleOfferUplink, this)
}, '📡 ' + _('Offer My Uplink')),
autoFailover ?
E('button', {
'class': 'kiss-btn',
'click': L.bind(this.handleToggleFailover, this, false)
}, '⏹ ' + _('Disable Auto-Failover')) :
E('button', {
'class': 'kiss-btn kiss-btn-cyan',
'click': L.bind(this.handleToggleFailover, this, true)
}, '▶ ' + _('Enable Auto-Failover'))
])
]),
// Active Uplinks Table
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Configured Uplinks')),
uplinks.length > 0 ?
E('div', { 'class': 'table-wrapper' }, [
E('table', { 'class': 'table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, _('Interface')),
E('th', {}, _('Peer')),
E('th', {}, _('Endpoint')),
E('th', {}, _('Priority')),
E('th', {}, _('Status')),
E('th', {}, _('Actions'))
])
]),
E('tbody', {},
uplinks.map(function(uplink) {
var statusColor = uplink.status === 'active' ? '#28a745' :
uplink.status === 'testing' ? '#ffc107' : '#6c757d';
var statusIcon = uplink.status === 'active' ? '✓' :
uplink.status === 'testing' ? '~' : '?';
return E('tr', {}, [
E('td', {}, [
E('code', {}, uplink.interface || 'wgup?')
]),
E('td', {}, [
E('code', { 'style': 'font-size: 0.85em;' },
API.shortenKey(uplink.peer_pubkey, 12))
]),
E('td', {}, uplink.endpoint || '-'),
E('td', {}, [
E('span', { 'class': 'badge', 'style': 'background: #6c757d; color: white;' },
'P' + (uplink.priority || 10) + ' W' + (uplink.weight || 1))
]),
E('td', {}, [
E('span', {
'class': 'badge',
'style': 'background: ' + statusColor + '; color: white;'
}, statusIcon + ' ' + (uplink.status || 'unknown'))
]),
E('td', {}, [
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'margin: 2px; padding: 4px 8px;',
'click': L.bind(self.handleTestUplink, self, uplink)
}, '🔍 ' + _('Test')),
E('button', {
'class': 'kiss-btn',
'style': 'margin: 2px; padding: 4px 8px;',
'click': L.bind(self.handleSetPriority, self, uplink)
}, '⚙ ' + _('Priority')),
E('button', {
'class': 'kiss-btn kiss-btn-red',
'style': 'margin: 2px; padding: 4px 8px;',
'click': L.bind(self.handleRemoveUplink, self, uplink)
}, '✗ ' + _('Remove'))
])
]);
})
)
])
]) :
E('div', { 'style': 'text-align: center; padding: 2em; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '🔗'),
E('h4', {}, _('No Uplinks Configured')),
E('p', { 'style': 'color: #666;' },
_('Add uplinks from mesh peer offers below, or wait for peers to advertise their uplinks.'))
])
]),
// Available Peer Offers
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Available Peer Offers')),
peerOffers.length > 0 ?
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1em;' },
peerOffers.map(function(offer) {
return E('div', {
'style': 'background: white; border: 1px solid #ddd; border-radius: 12px; padding: 1.5em; ' +
'box-shadow: 0 2px 4px rgba(0,0,0,0.05);'
}, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [
E('div', { 'style': 'font-weight: bold; font-size: 1.1em;' }, [
'🌐 ',
offer.node_id || 'Mesh Peer'
]),
E('span', {
'class': 'badge',
'style': 'background: #28a745; color: white;'
}, 'Available')
]),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 0.5em; margin-bottom: 1em;' }, [
E('div', {}, [
E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('Bandwidth')),
E('div', { 'style': 'font-weight: bold;' }, (offer.bandwidth || '?') + ' Mbps')
]),
E('div', {}, [
E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('Latency')),
E('div', { 'style': 'font-weight: bold;' }, (offer.latency || '?') + ' ms')
])
]),
E('div', { 'style': 'font-size: 0.85em; color: #666; margin-bottom: 1em;' }, [
E('code', {}, API.shortenKey(offer.public_key, 16) || 'N/A')
]),
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'width: 100%;',
'click': L.bind(self.handleAddUplink, self, offer)
}, '+ ' + _('Use as Uplink'))
]);
})
) :
E('div', { 'style': 'text-align: center; padding: 2em; background: #f8f9fa; border-radius: 8px;' }, [
E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '📡'),
E('h4', {}, _('No Peer Offers Available')),
E('p', { 'style': 'color: #666;' },
_('Mesh peers can offer their internet connection as backup uplinks. ' +
'Offers will appear here when peers advertise via gossip protocol.'))
])
]),
// Help Section
E('div', { 'class': 'cbi-section', 'style': 'background: #e7f3ff; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [
E('h4', { 'style': 'margin-top: 0;' }, '💡 ' + _('How Mesh Uplinks Work')),
E('ul', { 'style': 'margin: 0; padding-left: 1.5em;' }, [
E('li', {}, _('Mesh peers can share their internet connection as backup uplinks')),
E('li', {}, _('Traffic is routed through WireGuard tunnels to the offering peer')),
E('li', {}, _('MWAN3 handles automatic failover when primary WAN fails')),
E('li', {}, _('Offers are advertised via the P2P gossip protocol')),
E('li', {}, _('Use priority settings to control failover order'))
])
])
]);
// Start polling
this.startPolling();
return KissTheme.wrap([view], 'admin/services/wireguard/uplinks');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});