secubox-openwrt/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js
2025-12-29 09:03:49 +01:00

403 lines
13 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require ui';
'require network-modes.api as api';
'require network-modes.helpers as helpers';
'require secubox/help as Help';
'require secubox-theme/theme as Theme';
// Initialize global theme respecting LuCI preferences
var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: nmLang });
return view.extend({
title: _('Network Modes'),
load: function() {
return api.getAllData();
},
handleModeSwitch: function(mode) {
var self = this;
ui.showModal(_('Switch Mode'), [
E('p', {}, _('Are you sure you want to switch to ') + mode + _(' mode?')),
E('p', { 'class': 'nm-alert nm-alert-warning' }, [
E('span', { 'class': 'nm-alert-icon' }, '⚠️'),
E('div', {}, [
E('div', { 'class': 'nm-alert-title' }, _('Warning')),
E('div', { 'class': 'nm-alert-text' }, _('This will change network configuration. A backup will be created.'))
])
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-positive',
'click': function() {
ui.hideModal();
return api.applyMode(mode).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', {}, result.message), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Error: ' + result.error), 'error');
}
});
}
}, _('Switch Mode'))
])
]);
},
render: function(data) {
var self = this;
var status = data.status || {};
var modesData = (data.modes || {}).modes || [];
var currentMode = status.current_mode || 'router';
// Build a full mode map using backend data + fallbacks
var baseOrder = ['router', 'doublenat', 'multiwan', 'vpnrelay', 'bridge', 'accesspoint', 'relay', 'travel', 'sniffer'];
var modeInfos = {};
// Prime with RPC payload so description/icon/features stay in sync
modesData.forEach(function(mode) {
var fallback = api.getModeInfo(mode.id || '');
modeInfos[mode.id] = Object.assign({}, fallback, {
id: mode.id,
name: mode.name || (fallback && fallback.name) || mode.id,
icon: mode.icon || (fallback && fallback.icon) || '🌐',
description: mode.description || (fallback && fallback.description) || '',
features: Array.isArray(mode.features) && mode.features.length
? mode.features
: (fallback && fallback.features) || []
});
});
// Ensure every known mode has a definition, even if RPC omitted it
baseOrder.concat(['sniffer']).forEach(function(mode) {
if (!modeInfos[mode]) {
modeInfos[mode] = api.getModeInfo(mode);
}
});
// Preserve RPC ordering but guarantee canonical fallback + sniffer tab
var modeOrder = modesData.map(function(mode) { return mode.id; });
baseOrder.forEach(function(mode) {
if (modeOrder.indexOf(mode) === -1)
modeOrder.push(mode);
});
if (modeOrder.indexOf('sniffer') === -1)
modeOrder.push('sniffer');
var currentModeInfo = modeInfos[currentMode] || api.getModeInfo(currentMode);
var view = E('div', { 'class': 'network-modes-dashboard' }, [
// Load global theme CSS
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/help.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }),
helpers.createNavigationTabs('overview'),
this.renderHeader(status, currentModeInfo),
// Current Mode Display Card
E('div', { 'class': 'nm-current-mode-card' }, [
E('div', { 'class': 'nm-current-mode-header' }, [
E('div', { 'class': 'nm-current-mode-icon' }, currentModeInfo ? currentModeInfo.icon : '🌐'),
E('div', { 'class': 'nm-current-mode-info' }, [
E('div', { 'class': 'nm-current-mode-label' }, 'Current Network Mode'),
E('h2', { 'class': 'nm-current-mode-name' }, currentModeInfo ? currentModeInfo.name : currentMode)
])
]),
E('div', { 'class': 'nm-current-mode-description' },
currentModeInfo ? currentModeInfo.description : 'Unknown mode'),
E('div', { 'class': 'nm-current-mode-config' }, [
E('div', { 'class': 'nm-config-item' }, [
E('span', { 'class': 'nm-config-label' }, 'WAN IP:'),
E('span', { 'class': 'nm-config-value' }, status.wan_ip || 'N/A')
]),
E('div', { 'class': 'nm-config-item' }, [
E('span', { 'class': 'nm-config-label' }, 'LAN IP:'),
E('span', { 'class': 'nm-config-value' }, status.lan_ip || 'N/A')
]),
E('div', { 'class': 'nm-config-item' }, [
E('span', { 'class': 'nm-config-label' }, 'DHCP Server:'),
E('span', { 'class': 'nm-config-value' }, status.dhcp_enabled ? 'Enabled' : 'Disabled')
])
]),
E('button', {
'class': 'nm-change-mode-btn',
'click': function() {
window.location.hash = '#admin/secubox/network/network-modes/wizard';
}
}, '🔄 Change Mode')
]),
// Mode Comparison Table
E('div', { 'class': 'nm-comparison-card' }, [
E('h3', { 'class': 'nm-comparison-title' }, 'Mode Comparison Table'),
E('div', { 'class': 'nm-comparison-table-wrapper' }, [
E('table', { 'class': 'nm-comparison-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Feature')
].concat(baseOrder.map(function(modeId) {
var info = modeInfos[modeId] || api.getModeInfo(modeId);
return E('th', {
'class': currentMode === modeId ? 'active-mode' : ''
}, (info.icon || '') + ' ' + (info.name || modeId));
})))
]),
E('tbody', {}, (function() {
var comparisonRows = [
{
label: 'Use Case',
values: {
router: 'Home/Office',
doublenat: 'Behind ISP box',
multiwan: 'Dual uplinks',
vpnrelay: 'VPN gateway',
bridge: 'Layer 2 passthrough',
accesspoint: 'WiFi Hotspot',
relay: 'WiFi Extender',
travel: 'Hotel / Travel kit',
sniffer: 'Packet capture / TAP'
}
},
{
label: 'WAN Ports',
values: {
router: '1 port',
doublenat: 'WAN DHCP',
multiwan: '2 uplinks',
vpnrelay: 'VPN tunnel',
bridge: 'All bridged',
accesspoint: '1 uplink',
relay: 'WiFi uplink',
travel: 'WiFi or USB',
sniffer: 'Monitor source port'
}
},
{
label: 'LAN Ports',
values: {
router: 'Multiple',
doublenat: 'LAN + Guest',
multiwan: 'All ports',
vpnrelay: 'Policy-based',
bridge: 'All ports',
accesspoint: 'All ports',
relay: 'All ports',
travel: 'All ports',
sniffer: 'Mirror to capture'
}
},
{
label: 'WiFi Role',
values: {
router: 'Access Point',
doublenat: 'Router',
multiwan: 'Router',
vpnrelay: 'Router',
bridge: 'Optional AP',
accesspoint: 'AP only',
relay: 'Client + AP',
travel: 'Client + AP',
sniffer: 'Monitor mode'
}
},
{
label: 'DHCP Server',
values: {
router: 'Yes',
doublenat: 'Yes',
multiwan: 'Yes',
vpnrelay: 'Optional',
bridge: 'No',
accesspoint: 'No',
relay: 'Yes',
travel: 'Yes',
sniffer: 'No'
}
},
{
label: 'NAT',
values: {
router: 'Enabled',
doublenat: 'Double layer',
multiwan: 'Enabled',
vpnrelay: 'VPN NAT',
bridge: 'Disabled',
accesspoint: 'Disabled',
relay: 'Enabled',
travel: 'Enabled',
sniffer: 'Disabled'
}
}
];
return comparisonRows.map(function(row) {
return E('tr', {}, [
E('td', { 'class': 'feature-label' }, row.label)
].concat(baseOrder.map(function(modeId) {
return E('td', {
'class': currentMode === modeId ? 'active-mode' : ''
}, row.values[modeId] || '—');
})));
});
})())
])
])
]),
// Mode Selection Grid
E('div', { 'class': 'nm-modes-grid' },
modeOrder.map(function(modeId) {
var info = modeInfos[modeId];
if (!info)
return null;
var isActive = modeId === currentMode;
return E('div', {
'class': 'nm-mode-card ' + modeId + (isActive ? ' active' : ''),
'click': function() {
if (!isActive) {
self.handleModeSwitch(modeId);
}
}
}, [
isActive ? E('div', { 'class': 'nm-mode-active-indicator' }, 'Active') : '',
E('div', { 'class': 'nm-mode-header' }, [
E('div', { 'class': 'nm-mode-icon' }, info.icon),
E('div', { 'class': 'nm-mode-title' }, [
E('h3', {}, info.name),
E('p', {}, modeId.charAt(0).toUpperCase() + modeId.slice(1) + ' Mode')
])
]),
E('div', { 'class': 'nm-mode-description' }, info.description),
E('div', { 'class': 'nm-mode-features' },
(info.features || []).map(function(f) {
return E('span', { 'class': 'nm-mode-feature' }, [
E('span', { 'class': 'nm-mode-feature-icon' }, '✓'),
f
]);
})
)
]);
}).filter(function(card) { return !!card; })
),
// Interfaces Status
E('div', { 'class': 'nm-card' }, [
E('div', { 'class': 'nm-card-header' }, [
E('div', { 'class': 'nm-card-title' }, [
E('span', { 'class': 'nm-card-title-icon' }, '🔌'),
'Network Interfaces'
]),
E('div', { 'class': 'nm-card-badge' }, (status.interfaces || []).length + ' interfaces')
]),
E('div', { 'class': 'nm-card-body' }, [
E('div', { 'class': 'nm-interfaces-grid' },
(status.interfaces || []).map(function(iface) {
var icon = '🔌';
if (iface.name.startsWith('wlan') || iface.name.startsWith('wl')) icon = '📶';
else if (iface.name.startsWith('wg')) icon = '🔐';
else if (iface.name.startsWith('br')) icon = '🌉';
else if (iface.name.startsWith('eth')) icon = '🔗';
return E('div', { 'class': 'nm-interface-card' }, [
E('div', { 'class': 'nm-interface-icon' }, icon),
E('div', { 'class': 'nm-interface-info' }, [
E('div', { 'class': 'nm-interface-name' }, iface.name),
E('div', { 'class': 'nm-interface-ip' }, iface.ip || 'No IP')
]),
E('div', { 'class': 'nm-interface-status ' + iface.state })
]);
})
)
])
]),
// Services Status
E('div', { 'class': 'nm-card' }, [
E('div', { 'class': 'nm-card-header' }, [
E('div', { 'class': 'nm-card-title' }, [
E('span', { 'class': 'nm-card-title-icon' }, '🔧'),
'Services Status'
])
]),
E('div', { 'class': 'nm-card-body' }, [
E('div', { 'class': 'nm-interfaces-grid' },
[
{ name: 'Firewall', key: 'firewall', icon: '🛡️' },
{ name: 'DHCP/DNS', key: 'dnsmasq', icon: '📡' },
{ name: 'Netifyd', key: 'netifyd', icon: '🔍' },
{ name: 'Nginx', key: 'nginx', icon: '🌐' },
{ name: 'Squid', key: 'squid', icon: '🦑' }
].map(function(svc) {
var running = status.services && status.services[svc.key];
return E('div', { 'class': 'nm-interface-card' }, [
E('div', { 'class': 'nm-interface-icon' }, svc.icon),
E('div', { 'class': 'nm-interface-info' }, [
E('div', { 'class': 'nm-interface-name' }, svc.name),
E('div', { 'class': 'nm-interface-ip' }, running ? 'Running' : 'Stopped')
]),
E('div', { 'class': 'nm-interface-status ' + (running ? 'up' : 'down') })
]);
})
)
])
])
]);
// Include CSS
var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') });
document.head.appendChild(cssLink);
return view;
},
renderHeader: function(status, currentModeInfo) {
var modeName = currentModeInfo ? currentModeInfo.name : (status.current_mode || 'router');
var stats = [
{ label: _('Version'), value: status.version || _('Unknown'), icon: '🏷️' },
{ label: _('Mode'), value: modeName, icon: '🧭' },
{ label: _('WAN IP'), value: status.wan_ip || _('Unknown'), icon: '🌍' },
{ label: _('LAN IP'), value: status.lan_ip || _('Unknown'), icon: '🏠' }
];
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🌐'),
_('Network Configuration')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Switch between curated router, bridge, relay, and travel modes.'))
]),
E('div', { 'class': 'sh-header-meta' }, stats.map(this.renderHeaderChip, this))
]);
},
renderHeaderChip: function(stat) {
return E('div', { 'class': 'sh-header-chip' }, [
E('span', { 'class': 'sh-chip-icon' }, stat.icon || '•'),
E('div', { 'class': 'sh-chip-text' }, [
E('span', { 'class': 'sh-chip-label' }, stat.label),
E('strong', {}, stat.value || '-')
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});