feat: Fix Client Guardian RPC, redesign Netifyd devices UI (v0.6.0-r26)

- Fix Client Guardian JS files: replace invalid 'require X as Y' syntax
  with direct RPC declarations (LuCI doesn't support as alias)
- Add factory default profile to Client Guardian profiles.json
- Redesign Netifyd devices page with modern card-based UI:
  - Device type detection with emoji icons
  - Gradient summary cards for stats
  - Responsive grid layout
  - Traffic distribution bars
  - Real-time refresh with pulse animation
- Fix Netifyd RPC calls: use correct luci.secubox-netifyd object name
- Add WAN access control feature to secubox-admin

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-08 13:52:45 +01:00
parent b610239551
commit b7fb268f71
18 changed files with 1107 additions and 318 deletions

View File

@ -288,7 +288,66 @@
"Bash(do scp /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/$f root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)",
"Bash(for f in clients.js overview.js)",
"Bash(for f in htdocs/luci-static/resources/view/client-guardian/settings.js htdocs/luci-static/resources/client-guardian/api.js root/usr/libexec/rpcd/luci.client-guardian root/etc/config/client-guardian)",
"Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)"
"Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)",
"Bash(ssh root@192.168.255.1:*)",
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\")",
"Bash($SSH root@192.168.255.1 \"ubus list | grep secubox; ls -la /usr/libexec/rpcd/luci.secubox* 2>/dev/null; ls -la /www/luci-static/resources/view/secubox/ 2>/dev/null\")",
"Bash($SSH root@192.168.255.1 \"cat /usr/share/luci/menu.d/luci-app-secubox.json 2>/dev/null || echo ''NOT FOUND''\")",
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | head -50\")",
"Bash($SSH root@192.168.255.1 \"ubus -v list luci.secubox\")",
"Bash($SSH root@192.168.255.1 \"echo ''{\"\"method\"\":\"\"getModules\"\"}'' | /usr/libexec/rpcd/luci.secubox call getModules 2>&1 | head -30\")",
"Bash($SSH root@192.168.255.1 \"sed -n ''90,100p'' /usr/sbin/secubox-appstore\")",
"Bash(SCP=\"scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\":*)",
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/sbin/secubox-appstore root@192.168.255.1:/usr/sbin/secubox-appstore)",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart\")",
"Bash(SCP=\"scp -O -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart && echo ''✅ Fixed''\")",
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | head -30\")",
"Bash($SSH root@192.168.255.1 \"grep -A50 ''getModules'' /usr/libexec/rpcd/luci.secubox | head -60\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart && ubus call luci.secubox getModules ''{}'' 2>&1 | head -50\")",
"Bash($SSH root@192.168.255.1 \"sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | head -80\")",
"Bash($SSH root@192.168.255.1 \"cat /usr/share/secubox/plugins/catalog/crowdsec-dashboard.json 2>/dev/null | head -30\")",
"Bash($SSH root@192.168.255.1 \"grep -E ''^\\(luci-app-|secubox-\\)'' /tmp/secubox-installed-cache 2>/dev/null | head -20\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart\")",
"Bash($SSH root@192.168.255.1 \"sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 ''crowdsec-dashboard''\")",
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 ''netifyd''\")",
"Bash($SSH root@192.168.255.1 \"/etc/init.d/netifyd status; ls /etc/rc.d/S*netifyd 2>/dev/null\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 -E ''\\(netifyd|crowdsec-dashboard\\)''\")",
"Bash($SSH root@192.168.255.1 \"cat /etc/config/secubox 2>/dev/null | head -50\")",
"Bash($SSH root@192.168.255.1 \"cat /etc/config/firewall | grep -A5 ''luci\\\\|secubox'' | head -30\")",
"Bash($SSH root@192.168.255.1 \"/etc/init.d/rpcd restart && sleep 2 && ubus -v list luci.secubox | grep wan\")",
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox root@192.168.255.1:/usr/libexec/rpcd/luci.secubox)",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/libexec/rpcd/luci.secubox && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox get_wan_access ''{}''\")",
"Bash($SSH root@192.168.255.1 \"/usr/sbin/secubox-wan-access status\")",
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep secubox_wan\")",
"Bash($SSH root@192.168.255.1 \"nft list ruleset 2>/dev/null | grep -A2 ''secubox\\\\|dport 443'' | head -20\")",
"Bash($SSH root@192.168.255.1 \"/etc/init.d/firewall restart && sleep 2 && fw4 print 2>/dev/null | grep -i ''secubox\\\\|443'' | head -10\")",
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10\")",
"Bash($SSH root@192.168.255.1 \"echo ''connected''\")",
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''=zone$|\\\\.name=''\")",
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''network=|\\\\.input=|\\\\.output=|\\\\.forward=''\")",
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/sbin/secubox-wan-access root@192.168.255.1:/usr/sbin/secubox-wan-access)",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-wan-access && /usr/sbin/secubox-wan-access apply && /etc/init.d/firewall restart && sleep 2 && /usr/sbin/secubox-wan-access status\")",
"Bash($SSH root@192.168.255.1 \"\n# Remove old invalid rule\nuci delete firewall.@rule[0] 2>/dev/null\n\n# Run apply which will create new rules with correct src\n/usr/sbin/secubox-wan-access apply\n\n# Show the new rule\nuci show firewall | grep secubox\n\n# Restart firewall\n/etc/init.d/firewall restart\n\")",
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15\")",
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30\")",
"Bash($SSH root@192.168.255.1 \"echo connected && uci show firewall | grep secubox\")",
"Bash($SSH root@192.168.255.1 \"/etc/init.d/firewall restart && sleep 3 && /usr/sbin/secubox-wan-access status\")",
"Bash($SSH root@192.168.255.1 \"fw4 print 2>&1 | grep -i ''secubox\\\\|443'' | head -10\")",
"Bash($SSH root@192.168.255.1 \"fw4 print 2>&1 | grep -E ''secubox|tcp dport 443'' | head -10\")",
"Bash($SSH root@192.168.255.1 \"cat /usr/share/rpcd/acl.d/luci-app-secubox-admin.json 2>/dev/null || cat /usr/share/rpcd/acl.d/luci-mod-secubox.json 2>/dev/null | head -30\")",
"Bash($SSH root@192.168.255.1 \"ls /usr/share/rpcd/acl.d/ | grep -E ''secubox|admin''\")",
"Bash($SSH root@192.168.255.1 \"cat /usr/share/rpcd/acl.d/luci-app-secubox-admin.json\")",
"Bash($SSH root@192.168.255.1 'cat > /usr/share/rpcd/acl.d/luci-app-secubox-admin.json << ''''EOF''''\n{\n\t\"\"luci-app-secubox-admin\"\": {\n\t\t\"\"description\"\": \"\"SecuBox Admin Control Center\"\",\n\t\t\"\"read\"\": {\n\t\t\t\"\"ubus\"\": {\n\t\t\t\t\"\"luci.secubox\"\": [\n\t\t\t\t\t\"\"get_appstore_apps\"\",\n\t\t\t\t\t\"\"get_appstore_app\"\",\n\t\t\t\t\t\"\"getModules\"\",\n\t\t\t\t\t\"\"getModuleInfo\"\",\n\t\t\t\t\t\"\"get_dashboard_data\"\",\n\t\t\t\t\t\"\"get_system_health\"\",\n\t\t\t\t\t\"\"get_alerts\"\",\n\t\t\t\t\t\"\"getLogs\"\",\n\t\t\t\t\t\"\"listProfiles\"\",\n\t\t\t\t\t\"\"get_catalog_sources\"\",\n\t\t\t\t\t\"\"check_updates\"\",\n\t\t\t\t\t\"\"get_app_versions\"\",\n\t\t\t\t\t\"\"get_changelog\"\",\n\t\t\t\t\t\"\"get_widget_data\"\",\n\t\t\t\t\t\"\"get_wan_access\"\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"\"uci\"\": [\n\t\t\t\t\"\"secubox\"\",\n\t\t\t\t\"\"secubox-appstore\"\",\n\t\t\t\t\"\"firewall\"\",\n\t\t\t\t\"\"*\"\"\n\t\t\t]\n\t\t},\n\t\t\"\"write\"\": {\n\t\t\t\"\"ubus\"\": {\n\t\t\t\t\"\"luci.secubox\"\": [\n\t\t\t\t\t\"\"install_appstore_app\"\",\n\t\t\t\t\t\"\"remove_appstore_app\"\",\n\t\t\t\t\t\"\"enable_module\"\",\n\t\t\t\t\t\"\"disable_module\"\",\n\t\t\t\t\t\"\"updateModule\"\",\n\t\t\t\t\t\"\"applyProfile\"\",\n\t\t\t\t\t\"\"set_catalog_source\"\",\n\t\t\t\t\t\"\"sync_catalog\"\",\n\t\t\t\t\t\"\"set_wan_access\"\",\n\t\t\t\t\t\"\"apply_wan_access\"\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"\"uci\"\": [\n\t\t\t\t\"\"secubox\"\",\n\t\t\t\t\"\"secubox-appstore\"\",\n\t\t\t\t\"\"firewall\"\",\n\t\t\t\t\"\"*\"\"\n\t\t\t]\n\t\t}\n\t}\n}\nEOF\n/etc/init.d/rpcd restart && echo \"\"ACL updated\"\"')",
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox apply_wan_access ''{}'' 2>&1\")",
"Bash($SSH root@192.168.255.1 \"/usr/sbin/secubox-wan-access apply 2>&1\")",
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox apply_wan_access ''{}''\")",
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/libexec/rpcd/luci.secubox && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox apply_wan_access ''{}''\")",
"Bash($SSH root@192.168.255.1 \"uci show network | grep -E ''=interface|\\\\.proto=''\")",
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''zone.*name|=forwarding''\")",
"Bash(for:*)",
"Bash(do sed -i \"s/''require secubox-theme\\\\/theme as Theme'';//g\" \"$f\")"
]
}
}

View File

@ -1,6 +1,6 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require uci';

View File

@ -1,16 +1,72 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require client-guardian.api as api';
'require rpc';
var callGetClients = rpc.declare({
object: 'luci.client-guardian',
method: 'clients',
expect: { clients: [] }
});
var callGetZones = rpc.declare({
object: 'luci.client-guardian',
method: 'zones',
expect: { zones: [] }
});
var callApproveClient = rpc.declare({
object: 'luci.client-guardian',
method: 'approve_client',
params: ['mac', 'name', 'zone', 'notes']
});
var callUpdateClient = rpc.declare({
object: 'luci.client-guardian',
method: 'update_client',
params: ['section', 'name', 'zone', 'notes', 'daily_quota', 'static_ip']
});
var callBanClient = rpc.declare({
object: 'luci.client-guardian',
method: 'ban_client',
params: ['mac', 'reason']
});
var callQuarantineClient = rpc.declare({
object: 'luci.client-guardian',
method: 'quarantine_client',
params: ['mac']
});
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
i = Math.min(i, units.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function getDeviceIcon(hostname, mac) {
hostname = (hostname || '').toLowerCase();
mac = (mac || '').toLowerCase();
if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/)) return '📱';
if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/)) return '💻';
if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/)) return '📷';
if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/)) return '📺';
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
if (hostname.match(/router|switch|ap|access[-_]?point|bridge/)) return '🌐';
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
return '🔌';
}
return view.extend({
load: function() {
return Promise.all([
api.getClients(),
api.getZones()
callGetClients(),
callGetZones()
]);
},
@ -85,7 +141,7 @@ return view.extend({
if (client.status === 'unknown') statusClass += ' quarantine';
if (client.status === 'banned') statusClass += ' banned';
var deviceIcon = api.getDeviceIcon(client.hostname || client.name, client.mac);
var deviceIcon = getDeviceIcon(client.hostname || client.name, client.mac);
var zoneClass = (client.zone || 'unknown').replace('lan_', '');
var self = this;
@ -109,8 +165,8 @@ return view.extend({
]),
E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'),
E('div', { 'class': 'cg-client-traffic' }, [
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + api.formatBytes(client.rx_bytes || 0)),
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + api.formatBytes(client.tx_bytes || 0))
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + formatBytes(client.rx_bytes || 0)),
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + formatBytes(client.tx_bytes || 0))
]),
E('div', { 'class': 'cg-client-actions' }, [
client.status === 'unknown' ? E('div', {
@ -192,7 +248,7 @@ return view.extend({
var name = document.getElementById('approve-name').value;
var zone = document.getElementById('approve-zone').value;
var notes = document.getElementById('approve-notes').value;
api.approveClient(mac, name, zone, notes).then(L.bind(function() {
callApproveClient(mac, name, zone, notes).then(L.bind(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
this.handleRefresh();
@ -231,7 +287,7 @@ return view.extend({
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
api.updateClient(
callUpdateClient(
client.section,
document.getElementById('edit-name').value,
document.getElementById('edit-zone').value,
@ -262,7 +318,7 @@ return view.extend({
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
E('button', { 'class': 'cg-btn cg-btn-danger', 'click': L.bind(function() {
var reason = document.getElementById('ban-reason').value || 'Manual ban';
api.banClient(mac, reason).then(L.bind(function() {
callBanClient(mac, reason).then(L.bind(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
this.handleRefresh();
@ -274,7 +330,7 @@ return view.extend({
handleUnban: function(ev) {
var mac = ev.currentTarget.dataset.mac;
api.quarantineClient(mac).then(L.bind(function() {
callQuarantineClient(mac).then(L.bind(function() {
ui.addNotification(null, E('p', _('Client unbanned successfully')), 'success');
this.handleRefresh();
}, this));
@ -282,8 +338,8 @@ return view.extend({
handleRefresh: function() {
return Promise.all([
api.getClients(),
api.getZones()
callGetClients(),
callGetZones()
]).then(L.bind(function(data) {
var container = document.querySelector('.client-guardian-dashboard');
if (container) {

View File

@ -1,6 +1,6 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require ui';
'require uci';

View File

@ -1,6 +1,6 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';

View File

@ -1,18 +1,67 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require uci';
'require ui';
'require client-guardian.api as api';
'require rpc';
var callGetStatus = rpc.declare({
object: 'luci.client-guardian',
method: 'status'
});
var callGetClients = rpc.declare({
object: 'luci.client-guardian',
method: 'clients',
expect: { clients: [] }
});
var callGetZones = rpc.declare({
object: 'luci.client-guardian',
method: 'zones',
expect: { zones: [] }
});
var callApproveClient = rpc.declare({
object: 'luci.client-guardian',
method: 'approve_client',
params: ['mac', 'name', 'zone', 'notes']
});
var callBanClient = rpc.declare({
object: 'luci.client-guardian',
method: 'ban_client',
params: ['mac', 'reason']
});
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
i = Math.min(i, units.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function getDeviceIcon(hostname, mac) {
hostname = (hostname || '').toLowerCase();
mac = (mac || '').toLowerCase();
if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/)) return '📱';
if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/)) return '💻';
if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/)) return '📷';
if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/)) return '📺';
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
if (hostname.match(/router|switch|ap|access[-_]?point|bridge/)) return '🌐';
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
return '🔌';
}
return view.extend({
load: function() {
return Promise.all([
api.getStatus(),
api.getClients(),
api.getZones(),
callGetStatus(),
callGetClients(),
callGetZones(),
uci.load('client-guardian')
]);
},
@ -117,7 +166,7 @@ return view.extend({
if (client.status === 'banned')
statusClass += ' banned';
var deviceIcon = api.getDeviceIcon(client.hostname || client.name, client.mac);
var deviceIcon = getDeviceIcon(client.hostname || client.name, client.mac);
var zoneClass = (client.zone || 'unknown').replace('lan_', '');
var item = E('div', { 'class': 'cg-client-item ' + statusClass }, [
@ -142,8 +191,8 @@ return view.extend({
]),
E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'),
E('div', { 'class': 'cg-client-traffic' }, [
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + api.formatBytes(client.rx_bytes || 0)),
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + api.formatBytes(client.tx_bytes || 0))
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + formatBytes(client.rx_bytes || 0)),
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + formatBytes(client.tx_bytes || 0))
])
]);
@ -197,7 +246,7 @@ return view.extend({
'class': 'cg-btn cg-btn-success',
'click': L.bind(function() {
var zone = document.getElementById('approve-zone').value;
api.approveClient(mac, '', zone, '').then(L.bind(function() {
callApproveClient(mac, '', zone, '').then(L.bind(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
this.handleRefresh();
@ -222,7 +271,7 @@ return view.extend({
E('button', {
'class': 'cg-btn cg-btn-danger',
'click': L.bind(function() {
api.banClient(mac, 'Manual ban').then(L.bind(function() {
callBanClient(mac, 'Manual ban').then(L.bind(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
this.handleRefresh();
@ -235,24 +284,18 @@ return view.extend({
handleRefresh: function() {
return Promise.all([
api.getStatus(),
api.getClients(),
api.getZones()
callGetStatus(),
callGetClients(),
callGetZones()
]).then(L.bind(function(data) {
// Update dashboard without full page reload
var container = document.querySelector('.client-guardian-dashboard');
if (container) {
// Show loading indicator
var statusBadge = document.querySelector('.cg-status-badge');
if (statusBadge) {
statusBadge.classList.add('loading');
}
// Reconstruct data array (status, clients, zones, uci already loaded)
var newView = this.render(data);
dom.content(container.parentNode, newView);
// Remove loading indicator
if (statusBadge) {
statusBadge.classList.remove('loading');
}
@ -263,7 +306,6 @@ return view.extend({
},
handleLeave: function() {
// Stop polling when leaving the view to prevent memory leaks
poll.stop();
},

View File

@ -1,13 +1,17 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require ui';
'require client-guardian.api as api';
'require rpc';
var callGetParental = rpc.declare({
object: 'luci.client-guardian',
method: 'parental'
});
return view.extend({
load: function() {
return api.getParental();
return callGetParental();
},
render: function(data) {

View File

@ -1,6 +1,6 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require form';
'require ui';
'require uci';

View File

@ -1,6 +1,5 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require ui';
'require rpc';

View File

@ -1,13 +1,29 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require ui';
'require client-guardian.api as api';
'require rpc';
var callGetZones = rpc.declare({
object: 'luci.client-guardian',
method: 'zones',
expect: { zones: [] }
});
var callUpdateZone = rpc.declare({
object: 'luci.client-guardian',
method: 'update_zone',
params: ['id', 'name', 'bandwidth_limit', 'content_filter']
});
var callSyncZones = rpc.declare({
object: 'luci.client-guardian',
method: 'sync_zones'
});
return view.extend({
load: function() {
return api.getZones();
return callGetZones();
},
render: function(data) {
@ -51,17 +67,17 @@ return view.extend({
var features = [];
if (zone.internet_access) features.push({ name: 'Internet', enabled: true });
else features.push({ name: 'Internet', enabled: false });
if (zone.local_access) features.push({ name: 'Local', enabled: true });
else features.push({ name: 'Local', enabled: false });
if (zone.inter_client) features.push({ name: 'Inter-client', enabled: true });
if (zone.time_restrictions) features.push({ name: 'Horaires', enabled: true });
if (zone.content_filter && zone.content_filter !== 'none')
if (zone.content_filter && zone.content_filter !== 'none')
features.push({ name: 'Filtrage', enabled: true });
if (zone.portal_required) features.push({ name: 'Portail', enabled: true });
if (zone.bandwidth_limit > 0)
if (zone.bandwidth_limit > 0)
features.push({ name: zone.bandwidth_limit + ' Mbps', enabled: true });
return E('div', {
@ -162,13 +178,11 @@ return view.extend({
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end; margin-top: 20px' }, [
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
api.updateZone(
callUpdateZone(
zone.id,
zone.name,
parseInt(document.getElementById('zone-bandwidth').value) || 0,
document.getElementById('zone-filter').value,
zone.time_restrictions ? document.getElementById('zone-start').value : '',
zone.time_restrictions ? document.getElementById('zone-end').value : ''
document.getElementById('zone-filter').value
).then(L.bind(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Zone updated successfully')), 'success');
@ -184,7 +198,7 @@ return view.extend({
btn.disabled = true;
btn.innerHTML = '<span>⏳</span> Synchronisation...';
api.syncZones().then(function(result) {
callSyncZones().then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', {}, 'Zones firewall synchronisées avec succès'), 'success');
btn.innerHTML = '<span>✅</span> Synchronisé';
@ -205,7 +219,7 @@ return view.extend({
},
handleRefresh: function() {
return api.getZones().then(L.bind(function(data) {
return callGetZones().then(L.bind(function(data) {
var container = document.querySelector('.client-guardian-dashboard');
if (container) {
var newView = this.render(data);

View File

@ -1,5 +1,62 @@
{
"profiles": [
{
"id": "factory_default",
"name": "Configuration Usine",
"description": "Zones firewall par défaut OpenWrt - Réinitialisation standard",
"icon": "🔄",
"is_factory_default": true,
"zones": [
{
"id": "lan",
"name": "Réseau Local (LAN)",
"description": "Tous les appareils du réseau local",
"network": "lan",
"color": "#22c55e",
"icon": "home",
"internet_access": true,
"local_access": true,
"inter_client": true,
"bandwidth_limit": 0,
"priority": "normal"
},
{
"id": "wan",
"name": "Internet (WAN)",
"description": "Connexion vers Internet - trafic sortant autorisé",
"network": "wan",
"color": "#ef4444",
"icon": "globe",
"is_wan": true,
"internet_access": true,
"local_access": false,
"inter_client": false,
"bandwidth_limit": 0,
"priority": "high"
},
{
"id": "guest",
"name": "Invités",
"description": "Réseau invités isolé (optionnel)",
"network": "guest",
"color": "#8b5cf6",
"icon": "users",
"optional": true,
"internet_access": true,
"local_access": false,
"inter_client": false,
"bandwidth_limit": 0,
"priority": "low"
}
],
"firewall_defaults": {
"input": "ACCEPT",
"output": "ACCEPT",
"forward": "REJECT",
"lan_to_wan": "ACCEPT",
"wan_to_lan": "REJECT"
}
},
{
"id": "family_home",
"name": "Maison Familiale",

View File

@ -6,6 +6,25 @@
'require rpc';
'require form';
var callGetWanAccess = rpc.declare({
object: 'luci.secubox',
method: 'get_wan_access',
expect: { }
});
var callSetWanAccess = rpc.declare({
object: 'luci.secubox',
method: 'set_wan_access',
params: ['enabled', 'https_enabled', 'https_port', 'http_enabled', 'http_port', 'ssh_enabled', 'ssh_port'],
expect: { success: false }
});
var callApplyWanAccess = rpc.declare({
object: 'luci.secubox',
method: 'apply_wan_access',
expect: { success: false }
});
var callGetConfigFiles = rpc.declare({
object: 'file',
method: 'list',
@ -34,12 +53,14 @@ return view.extend({
L.resolveDefault(uci.load('secubox-appstore'), {}),
L.resolveDefault(uci.load('network'), {}),
L.resolveDefault(uci.load('firewall'), {}),
L.resolveDefault(uci.load('dhcp'), {})
L.resolveDefault(uci.load('dhcp'), {}),
L.resolveDefault(callGetWanAccess(), {})
]);
},
render: function() {
render: function(data) {
var self = this;
var wanAccess = data[5] || {};
var container = E('div', { 'class': 'cyberpunk-mode' }, [
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
@ -55,6 +76,7 @@ return view.extend({
E('div', { 'class': 'cyber-dual-console' }, [
// Left: Quick Config Sections
E('div', { 'class': 'cyber-console-left' }, [
this.renderWanAccessPanel(wanAccess),
this.renderQuickConfigPanel(),
this.renderSystemSubsetsPanel(),
this.renderConfigFilesPanel()
@ -72,6 +94,171 @@ return view.extend({
return container;
},
renderWanAccessPanel: function(wanAccess) {
var self = this;
var enabled = wanAccess.enabled || false;
var services = wanAccess.services || {};
return E('div', { 'class': 'cyber-panel cyber-scanlines' }, [
E('div', { 'class': 'cyber-panel-header' }, [
E('div', { 'class': 'cyber-panel-title' }, '🌐 WAN ACCESS'),
E('span', {
'class': 'cyber-panel-badge ' + (enabled ? 'success' : 'warning'),
'id': 'wan-access-status'
}, enabled ? 'ENABLED' : 'DISABLED')
]),
E('div', { 'class': 'cyber-panel-body' }, [
// Master toggle
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 12px; background: rgba(0,255,65,0.05); border-left: 3px solid var(--cyber-primary); margin-bottom: 15px;' }, [
E('div', {}, [
E('div', { 'style': 'font-weight: bold; font-size: 13px;' }, 'Remote Access'),
E('div', { 'style': 'font-size: 10px; color: var(--cyber-text-dim);' }, 'Allow access from WAN/Internet')
]),
E('label', { 'class': 'cyber-switch' }, [
E('input', {
'type': 'checkbox',
'id': 'wan-access-master',
'checked': enabled,
'change': function(ev) {
var masterEnabled = ev.target.checked;
document.getElementById('wan-access-status').textContent = masterEnabled ? 'ENABLED' : 'DISABLED';
document.getElementById('wan-access-status').className = 'cyber-panel-badge ' + (masterEnabled ? 'success' : 'warning');
}
}),
E('span', { 'class': 'cyber-slider' })
])
]),
// Service toggles
E('div', { 'style': 'display: flex; flex-direction: column; gap: 10px;' }, [
// HTTPS
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(0,255,255,0.05); border-radius: 4px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('span', { 'style': 'font-size: 16px;' }, '🔒'),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'HTTPS (LuCI)'),
E('div', { 'style': 'font-size: 10px; color: var(--cyber-text-dim);' }, 'Secure web interface')
])
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('input', {
'type': 'number',
'id': 'wan-https-port',
'value': (services.https && services.https.port) || 443,
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
}),
E('label', { 'class': 'cyber-switch' }, [
E('input', {
'type': 'checkbox',
'id': 'wan-https-enabled',
'checked': services.https && services.https.enabled
}),
E('span', { 'class': 'cyber-slider' })
])
])
]),
// HTTP
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(255,165,0,0.05); border-radius: 4px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('span', { 'style': 'font-size: 16px;' }, '🌐'),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'HTTP'),
E('div', { 'style': 'font-size: 10px; color: var(--cyber-warning);' }, 'Not recommended')
])
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('input', {
'type': 'number',
'id': 'wan-http-port',
'value': (services.http && services.http.port) || 80,
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
}),
E('label', { 'class': 'cyber-switch' }, [
E('input', {
'type': 'checkbox',
'id': 'wan-http-enabled',
'checked': services.http && services.http.enabled
}),
E('span', { 'class': 'cyber-slider' })
])
])
]),
// SSH
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(255,0,0,0.05); border-radius: 4px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('span', { 'style': 'font-size: 16px;' }, '🖥️'),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'SSH'),
E('div', { 'style': 'font-size: 10px; color: var(--cyber-danger);' }, 'Use with caution')
])
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('input', {
'type': 'number',
'id': 'wan-ssh-port',
'value': (services.ssh && services.ssh.port) || 22,
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
}),
E('label', { 'class': 'cyber-switch' }, [
E('input', {
'type': 'checkbox',
'id': 'wan-ssh-enabled',
'checked': services.ssh && services.ssh.enabled
}),
E('span', { 'class': 'cyber-slider' })
])
])
])
]),
// Apply button
E('div', { 'style': 'margin-top: 15px;' }, [
E('button', {
'class': 'cyber-btn primary',
'style': 'width: 100%;',
'click': function() {
var masterEnabled = document.getElementById('wan-access-master').checked;
var httpsEnabled = document.getElementById('wan-https-enabled').checked;
var httpsPort = parseInt(document.getElementById('wan-https-port').value) || 443;
var httpEnabled = document.getElementById('wan-http-enabled').checked;
var httpPort = parseInt(document.getElementById('wan-http-port').value) || 80;
var sshEnabled = document.getElementById('wan-ssh-enabled').checked;
var sshPort = parseInt(document.getElementById('wan-ssh-port').value) || 22;
ui.showModal(_('Applying'), [
E('p', { 'class': 'spinning' }, _('Updating firewall rules...'))
]);
callSetWanAccess(
masterEnabled ? 1 : 0,
httpsEnabled ? 1 : 0,
httpsPort,
httpEnabled ? 1 : 0,
httpPort,
sshEnabled ? 1 : 0,
sshPort
).then(function() {
return callApplyWanAccess();
}).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', 'WAN access rules applied successfully'), 'success');
} else {
ui.addNotification(null, E('p', 'Failed to apply rules'), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', 'Error: ' + err), 'error');
});
}
}, '🔄 Apply Firewall Rules')
])
])
]);
},
renderQuickConfigPanel: function() {
var self = this;

View File

@ -17,12 +17,14 @@
"check_updates",
"get_app_versions",
"get_changelog",
"get_widget_data"
"get_widget_data",
"get_wan_access"
]
},
"uci": [
"secubox",
"secubox-appstore",
"firewall",
"*"
]
},
@ -36,12 +38,15 @@
"updateModule",
"applyProfile",
"set_catalog_source",
"sync_catalog"
"sync_catalog",
"set_wan_access",
"apply_wan_access"
]
},
"uci": [
"secubox",
"secubox-appstore",
"firewall",
"*"
]
}

View File

@ -3,7 +3,52 @@
'require poll';
'require ui';
'require dom';
'require secubox-netifyd/api as netifydAPI';
'require rpc';
var callGetDevices = rpc.declare({
object: 'luci.secubox-netifyd',
method: 'get_detected_devices'
});
var callGetStatus = rpc.declare({
object: 'luci.secubox-netifyd',
method: 'get_service_status'
});
var callGetDashboard = rpc.declare({
object: 'luci.secubox-netifyd',
method: 'get_dashboard'
});
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
i = Math.min(i, units.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function formatDuration(seconds) {
if (seconds < 60) return seconds + 's';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';
return Math.floor(seconds / 86400) + 'd';
}
function getDeviceIcon(hostname, mac) {
hostname = (hostname || '').toLowerCase();
if (hostname.match(/android|phone|mobile|samsung|xiaomi|huawei|oppo|vivo/)) return '📱';
if (hostname.match(/iphone|ipad|apple|macbook|imac/)) return '🍎';
if (hostname.match(/pc|laptop|desktop|windows|linux|ubuntu/)) return '💻';
if (hostname.match(/camera|cam|dvr|nvr|hikvision|dahua/)) return '📷';
if (hostname.match(/tv|roku|chromecast|firestick|appletv|smart-tv/)) return '📺';
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
if (hostname.match(/router|switch|ap|access[-_]?point|mesh/)) return '📡';
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
if (hostname.match(/alexa|echo|google[-_]?home|homepod/)) return '🔊';
if (hostname.match(/thermostat|nest|hue|bulb|sensor|iot/)) return '🏠';
return '🔌';
}
return view.extend({
refreshInterval: 5,
@ -14,9 +59,9 @@ return view.extend({
load: function() {
return Promise.all([
netifydAPI.getDetectedDevices(),
netifydAPI.getServiceStatus(),
netifydAPI.getDashboard()
callGetDevices(),
callGetStatus(),
callGetDashboard()
]);
},
@ -67,7 +112,7 @@ return view.extend({
},
handleExport: function(ev) {
var csvContent = 'IP Address,MAC Address,Flows,Bytes Sent,Bytes Received,Total Traffic,Last Seen\n';
var csvContent = 'IP Address,MAC Address,Hostname,Flows,Bytes Sent,Bytes Received,Total Traffic,Last Seen\n';
this.devicesData.forEach(function(device) {
var total = (device.bytes_sent || 0) + (device.bytes_received || 0);
@ -77,6 +122,7 @@ return view.extend({
csvContent += [
'"' + (device.ip || 'N/A') + '"',
'"' + (device.mac || 'N/A') + '"',
'"' + (device.hostname || device.name || '') + '"',
device.flows || 0,
device.bytes_sent || 0,
device.bytes_received || 0,
@ -113,45 +159,46 @@ return view.extend({
{
title: _('Active Devices'),
value: devices.length.toString(),
icon: 'network-wired',
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
emoji: '📱',
color: '#6366f1',
bg: 'linear-gradient(135deg, rgba(99,102,241,0.15) 0%, rgba(139,92,246,0.15) 100%)'
},
{
title: _('Total Flows'),
value: totalFlows.toLocaleString(),
icon: 'stream',
gradient: 'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)'
emoji: '🔄',
color: '#10b981',
bg: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(52,211,153,0.15) 100%)'
},
{
title: _('Total Sent'),
value: netifydAPI.formatBytes(totalBytesSent),
icon: 'upload',
gradient: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)'
title: _('Upload'),
value: formatBytes(totalBytesSent),
emoji: '⬆️',
color: '#ef4444',
bg: 'linear-gradient(135deg, rgba(239,68,68,0.15) 0%, rgba(248,113,113,0.15) 100%)'
},
{
title: _('Total Received'),
value: netifydAPI.formatBytes(totalBytesRecv),
icon: 'download',
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
title: _('Download'),
value: formatBytes(totalBytesRecv),
emoji: '⬇️',
color: '#22c55e',
bg: 'linear-gradient(135deg, rgba(34,197,94,0.15) 0%, rgba(74,222,128,0.15) 100%)'
}
];
return E('div', {
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.5rem'
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px'
}, cards.map(function(card) {
return E('div', {
'style': 'background: ' + card.gradient + '; color: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1)'
'style': 'background: ' + card.bg + '; border: 1px solid rgba(0,0,0,0.08); padding: 20px; border-radius: 16px; transition: transform 0.2s, box-shadow 0.2s;'
}, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem' }, [
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9' }, card.title),
E('i', {
'class': 'fa fa-' + card.icon,
'style': 'font-size: 2em; opacity: 0.3'
})
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px' }, [
E('span', { 'style': 'font-size: 28px' }, card.emoji),
E('span', { 'style': 'font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600' }, card.title)
]),
E('div', { 'style': 'font-size: 2em; font-weight: bold' }, card.value)
E('div', { 'style': 'font-size: 28px; font-weight: 700; color: ' + card.color }, card.value)
]);
}.bind(this)));
}));
},
renderDevicesTable: function() {
@ -161,135 +208,90 @@ return view.extend({
var devices = this.filterDevices(this.devicesData);
var sortedDevices = this.sortDevices(devices, this.sortColumn, this.sortDirection);
var getSortIcon = function(column) {
if (this.sortColumn !== column) {
return E('i', { 'class': 'fa fa-sort', 'style': 'opacity: 0.3' });
}
return E('i', {
'class': 'fa fa-sort-' + (this.sortDirection === 'asc' ? 'up' : 'down'),
'style': 'color: #3b82f6'
});
}.bind(this);
dom.content(container, [
devices.length > 0 ? E('div', { 'class': 'table', 'style': 'font-size: 0.95em' },
[
// Header
E('div', { 'class': 'tr table-titles' }, [
E('div', {
'class': 'th left',
'style': 'width: 20%; cursor: pointer',
'click': ui.createHandlerFn(this, 'handleSort', 'ip')
}, [
_('IP Address'),
' ',
getSortIcon('ip')
]),
E('div', { 'class': 'th left', 'style': 'width: 20%' }, _('MAC Address')),
E('div', {
'class': 'th center',
'style': 'width: 10%; cursor: pointer',
'click': ui.createHandlerFn(this, 'handleSort', 'flows')
}, [
_('Flows'),
' ',
getSortIcon('flows')
]),
E('div', {
'class': 'th right',
'style': 'width: 15%; cursor: pointer',
'click': ui.createHandlerFn(this, 'handleSort', 'bytes_sent')
}, [
_('Sent'),
' ',
getSortIcon('bytes_sent')
]),
E('div', {
'class': 'th right',
'style': 'width: 15%; cursor: pointer',
'click': ui.createHandlerFn(this, 'handleSort', 'bytes_received')
}, [
_('Received'),
' ',
getSortIcon('bytes_received')
]),
E('div', { 'class': 'th', 'style': 'width: 20%' }, _('Traffic Distribution'))
])
].concat(
// Rows
sortedDevices.map(function(device, idx) {
var lastSeen = device.last_seen || 0;
var now = Math.floor(Date.now() / 1000);
var ago = now - lastSeen;
var lastSeenStr = 'N/A';
if (lastSeen > 0) {
if (ago < 60) {
lastSeenStr = _('Just now');
} else {
lastSeenStr = netifydAPI.formatDuration(ago) + ' ' + _('ago');
}
}
var totalBytes = (device.bytes_sent || 0) + (device.bytes_received || 0);
var sentPercent = totalBytes > 0 ? ((device.bytes_sent || 0) / totalBytes * 100) : 50;
var recvPercent = 100 - sentPercent;
return E('div', {
'class': 'tr',
'style': idx % 2 === 0 ? 'background: #f9fafb' : ''
}, [
E('div', { 'class': 'td left', 'style': 'width: 20%' }, [
E('code', { 'style': 'font-size: 0.9em' }, device.ip || 'Unknown'),
E('br'),
E('small', { 'class': 'text-muted' }, lastSeenStr)
]),
E('div', { 'class': 'td left', 'style': 'width: 20%' }, [
E('code', { 'style': 'font-size: 0.8em' }, device.mac || 'Unknown')
]),
E('div', { 'class': 'td center', 'style': 'width: 10%' },
(device.flows || 0).toLocaleString()),
E('div', { 'class': 'td right', 'style': 'width: 15%' }, [
E('span', {
'class': 'badge',
'style': 'background: #ef4444; color: white; padding: 0.25rem 0.5rem; border-radius: 4px'
}, netifydAPI.formatBytes(device.bytes_sent || 0))
]),
E('div', { 'class': 'td right', 'style': 'width: 15%' }, [
E('span', {
'class': 'badge',
'style': 'background: #10b981; color: white; padding: 0.25rem 0.5rem; border-radius: 4px'
}, netifydAPI.formatBytes(device.bytes_received || 0))
]),
E('div', { 'class': 'td', 'style': 'width: 20%' }, [
E('div', {
'style': 'display: flex; gap: 2px; height: 24px; border-radius: 4px; overflow: hidden'
}, [
E('div', {
'style': 'background: #ef4444; width: ' + sentPercent + '%; transition: width 0.3s',
'title': _('Upload: %s').format(sentPercent.toFixed(1) + '%')
}),
E('div', {
'style': 'background: #10b981; width: ' + recvPercent + '%; transition: width 0.3s',
'title': _('Download: %s').format(recvPercent.toFixed(1) + '%')
})
]),
E('div', { 'style': 'font-size: 0.75em; color: #6b7280; margin-top: 0.25rem; text-align: center' },
_('Total: %s').format(netifydAPI.formatBytes(totalBytes)))
])
]);
}.bind(this))
)
) : E('div', {
'class': 'alert-message info',
'style': 'text-align: center; padding: 3rem'
if (devices.length === 0) {
dom.content(container, E('div', {
'style': 'text-align: center; padding: 48px; background: linear-gradient(135deg, rgba(99,102,241,0.05) 0%, rgba(139,92,246,0.05) 100%); border-radius: 16px; border: 2px dashed rgba(99,102,241,0.2)'
}, [
E('i', { 'class': 'fa fa-network-wired', 'style': 'font-size: 3em; opacity: 0.3; display: block; margin-bottom: 1rem' }),
E('h4', _('No Device Data')),
E('p', { 'class': 'text-muted' }, _('No devices have been detected yet')),
E('small', _('Data will appear once network traffic is analyzed'))
])
]);
E('div', { 'style': 'font-size: 48px; margin-bottom: 16px; opacity: 0.5' }, '📡'),
E('h4', { 'style': 'margin: 0 0 8px 0; color: #374151' }, _('No Devices Detected')),
E('p', { 'style': 'color: #6b7280; margin: 0' }, _('Waiting for network traffic...'))
]));
return;
}
var deviceCards = sortedDevices.map(function(device) {
var lastSeen = device.last_seen || 0;
var now = Math.floor(Date.now() / 1000);
var ago = now - lastSeen;
var lastSeenStr = 'N/A';
var isOnline = ago < 120;
if (lastSeen > 0) {
if (ago < 60) lastSeenStr = _('Just now');
else lastSeenStr = formatDuration(ago) + ' ' + _('ago');
}
var totalBytes = (device.bytes_sent || 0) + (device.bytes_received || 0);
var sentPercent = totalBytes > 0 ? ((device.bytes_sent || 0) / totalBytes * 100) : 50;
var recvPercent = 100 - sentPercent;
var icon = getDeviceIcon(device.hostname || device.name, device.mac);
return E('div', {
'style': 'background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; transition: all 0.2s; ' + (isOnline ? 'box-shadow: 0 2px 8px rgba(34,197,94,0.1); border-left: 4px solid #22c55e;' : 'opacity: 0.7;')
}, [
// Header row: Icon + IP/MAC + Status
E('div', { 'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 12px' }, [
E('div', { 'style': 'font-size: 32px; flex-shrink: 0' }, icon),
E('div', { 'style': 'flex: 1; min-width: 0' }, [
E('div', { 'style': 'font-weight: 600; color: #111827; font-size: 15px; font-family: monospace' }, device.ip || 'Unknown'),
E('div', { 'style': 'font-size: 11px; color: #9ca3af; font-family: monospace' }, device.mac || 'Unknown')
]),
E('div', { 'style': 'text-align: right' }, [
E('div', {
'style': 'display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; ' + (isOnline ? 'background: rgba(34,197,94,0.15); color: #16a34a' : 'background: rgba(156,163,175,0.15); color: #6b7280')
}, [
E('span', { 'style': 'width: 6px; height: 6px; border-radius: 50%; background: ' + (isOnline ? '#22c55e' : '#9ca3af') }),
isOnline ? _('Online') : lastSeenStr
]),
E('div', { 'style': 'font-size: 11px; color: #9ca3af; margin-top: 4px' },
(device.flows || 0) + ' ' + _('flows'))
])
]),
// Traffic stats row
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px' }, [
E('div', { 'style': 'background: rgba(239,68,68,0.08); padding: 8px 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center' }, [
E('span', { 'style': 'font-size: 12px; color: #6b7280' }, '⬆️ ' + _('Upload')),
E('span', { 'style': 'font-weight: 600; color: #dc2626; font-size: 13px' }, formatBytes(device.bytes_sent || 0))
]),
E('div', { 'style': 'background: rgba(34,197,94,0.08); padding: 8px 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center' }, [
E('span', { 'style': 'font-size: 12px; color: #6b7280' }, '⬇️ ' + _('Download')),
E('span', { 'style': 'font-weight: 600; color: #16a34a; font-size: 13px' }, formatBytes(device.bytes_received || 0))
])
]),
// Traffic bar
E('div', { 'style': 'position: relative' }, [
E('div', { 'style': 'display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: #f3f4f6' }, [
E('div', {
'style': 'background: linear-gradient(90deg, #ef4444, #f87171); width: ' + sentPercent + '%; transition: width 0.3s',
'title': _('Upload: %s').format(sentPercent.toFixed(1) + '%')
}),
E('div', {
'style': 'background: linear-gradient(90deg, #22c55e, #4ade80); width: ' + recvPercent + '%; transition: width 0.3s',
'title': _('Download: %s').format(recvPercent.toFixed(1) + '%')
})
]),
E('div', { 'style': 'display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #9ca3af' }, [
E('span', {}, sentPercent.toFixed(0) + '% ↑'),
E('span', { 'style': 'font-weight: 500; color: #6b7280' }, _('Total: %s').format(formatBytes(totalBytes))),
E('span', {}, recvPercent.toFixed(0) + '% ↓')
])
])
]);
});
dom.content(container, E('div', {
'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px'
}, deviceCards));
},
render: function(data) {
@ -302,101 +304,96 @@ return view.extend({
// Set up polling
poll.add(L.bind(function() {
return Promise.all([
netifydAPI.getDetectedDevices(),
netifydAPI.getServiceStatus()
callGetDevices(),
callGetStatus()
]).then(L.bind(function(result) {
this.devicesData = (result[0] || {}).devices || [];
this.renderDevicesTable();
// Update summary cards
var summaryContainer = document.getElementById('summary-cards-container');
if (summaryContainer) {
dom.content(summaryContainer, this.renderSummaryCards(this.devicesData).childNodes);
}
// Update device count badge
var countBadge = document.getElementById('device-count-badge');
if (countBadge) {
countBadge.textContent = this.devicesData.length;
}
}, this));
}, this), this.refreshInterval);
var serviceRunning = status.running;
return E('div', { 'class': 'cbi-map' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
E('h2', { 'name': 'content', 'style': 'margin: 0' }, [
E('i', { 'class': 'fa fa-network-wired', 'style': 'margin-right: 0.5rem' }),
_('Detected Devices')
]),
E('span', {
'class': 'badge',
'style': 'padding: 0.5rem 1rem; font-size: 0.9em; background: ' + (serviceRunning ? '#10b981' : '#ef4444')
}, [
E('i', { 'class': 'fa fa-circle', 'style': 'margin-right: 0.5rem' }),
serviceRunning ? _('Live') : _('Offline')
])
]),
E('div', { 'class': 'cbi-map-descr' },
_('Network devices detected and tracked by Netifyd deep packet inspection. Updates every 5 seconds.')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
E('h3', { 'style': 'margin: 0' }, [
E('i', { 'class': 'fa fa-chart-bar', 'style': 'margin-right: 0.5rem' }),
_('Summary')
]),
E('button', {
'class': 'btn btn-primary',
'click': ui.createHandlerFn(this, 'handleExport')
}, [
E('i', { 'class': 'fa fa-download' }),
' ',
_('Export CSV')
])
]),
this.renderSummaryCards(this.devicesData)
])
]),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
E('h3', { 'style': 'margin: 0' }, [
E('i', { 'class': 'fa fa-list', 'style': 'margin-right: 0.5rem' }),
_('Device List'),
' ',
E('span', {
'class': 'badge',
'style': 'background: #3b82f6; color: white; margin-left: 0.5rem'
}, this.devicesData.length)
]),
E('div', { 'style': 'display: flex; gap: 0.5rem; align-items: center' }, [
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': _('Search by IP or MAC...'),
'style': 'min-width: 250px',
'value': this.searchQuery,
'keyup': function(ev) {
self.searchQuery = ev.target.value;
self.renderDevicesTable();
}
})
return E('div', { 'style': 'max-width: 1400px; margin: 0 auto; padding: 24px' }, [
// Header
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
E('div', { 'style': 'font-size: 36px' }, '📡'),
E('div', {}, [
E('h2', { 'style': 'margin: 0; font-size: 24px; font-weight: 700; color: #111827' }, _('Network Devices')),
E('p', { 'style': 'margin: 4px 0 0 0; font-size: 14px; color: #6b7280' }, _('Real-time traffic monitoring via Netifyd DPI'))
])
]),
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
E('div', {
'class': 'alert alert-info',
'style': 'margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem'
'style': 'display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 24px; font-size: 13px; font-weight: 600; ' + (serviceRunning ? 'background: rgba(34,197,94,0.15); color: #16a34a' : 'background: rgba(239,68,68,0.15); color: #dc2626')
}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
E('div', { 'style': 'width: 12px; height: 12px; background: #ef4444; border-radius: 2px' }),
E('span', _('Upload'))
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
E('div', { 'style': 'width: 12px; height: 12px; background: #10b981; border-radius: 2px' }),
E('span', _('Download'))
]),
E('span', { 'class': 'text-muted' }, '|'),
E('span', [
E('i', { 'class': 'fa fa-sync' }),
' ',
_('Auto-refresh: Every %d seconds').format(this.refreshInterval)
])
E('span', { 'style': 'width: 8px; height: 8px; border-radius: 50%; background: ' + (serviceRunning ? '#22c55e' : '#ef4444') + '; animation: ' + (serviceRunning ? 'pulse 2s infinite' : 'none') }),
serviceRunning ? _('Live') : _('Offline')
]),
E('div', { 'id': 'devices-table-container' })
E('button', {
'style': 'display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #6366f1; color: white; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer',
'click': ui.createHandlerFn(this, 'handleExport')
}, [
'📥',
_('Export')
])
])
])
]),
// Summary Cards
E('div', { 'id': 'summary-cards-container' }, this.renderSummaryCards(this.devicesData).childNodes),
// Device List Header
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
E('h3', { 'style': 'margin: 0; font-size: 18px; font-weight: 600; color: #374151' }, _('Device List')),
E('span', {
'id': 'device-count-badge',
'style': 'background: #6366f1; color: white; padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 600'
}, this.devicesData.length)
]),
E('div', { 'style': 'display: flex; gap: 12px; align-items: center' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px; font-size: 12px; color: #6b7280' }, [
E('span', { 'style': 'display: flex; align-items: center; gap: 4px' }, [
E('span', { 'style': 'width: 10px; height: 10px; background: linear-gradient(90deg, #ef4444, #f87171); border-radius: 2px' }),
_('Upload')
]),
E('span', { 'style': 'display: flex; align-items: center; gap: 4px' }, [
E('span', { 'style': 'width: 10px; height: 10px; background: linear-gradient(90deg, #22c55e, #4ade80); border-radius: 2px' }),
_('Download')
]),
E('span', { 'style': 'color: #9ca3af' }, '|'),
E('span', {}, '🔄 ' + _('%ds refresh').format(this.refreshInterval))
]),
E('input', {
'type': 'text',
'placeholder': _('🔍 Search IP or MAC...'),
'style': 'padding: 8px 16px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 13px; min-width: 200px; outline: none',
'value': this.searchQuery,
'keyup': function(ev) {
self.searchQuery = ev.target.value;
self.renderDevicesTable();
}
})
])
]),
// Devices Grid
E('div', { 'id': 'devices-table-container' }),
// CSS for pulse animation
E('style', {}, '@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }')
]);
},

View File

@ -22,3 +22,14 @@ config diagnostics 'settings'
option health_threshold_cpu '80'
option health_threshold_memory '90'
option health_threshold_storage '85'
config wan_access 'remote'
option enabled '1'
option https_enabled '1'
option https_port '443'
option http_enabled '0'
option http_port '80'
option ssh_enabled '0'
option ssh_port '22'
option allowed_ips ''
option dmz_mode '0'

View File

@ -194,12 +194,30 @@ case "$1" in
json_add_object "update_component_settings"
json_add_string "component_id" "string"
json_add_object "settings" "object"
json_add_object "settings"
json_close_object
json_close_object
json_add_object "sync_component_registry"
json_close_object
# WAN Access management
json_add_object "get_wan_access"
json_close_object
json_add_object "set_wan_access"
json_add_boolean "enabled" "boolean"
json_add_boolean "https_enabled" "boolean"
json_add_int "https_port" "integer"
json_add_boolean "http_enabled" "boolean"
json_add_int "http_port" "integer"
json_add_boolean "ssh_enabled" "boolean"
json_add_int "ssh_port" "integer"
json_close_object
json_add_object "apply_wan_access"
json_close_object
json_dump
;;
@ -220,7 +238,7 @@ case "$1" in
reload)
/usr/sbin/secubox-core reload
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_dump
;;
@ -587,12 +605,12 @@ case "$1" in
# Use secubox-appstore for installation
if /usr/sbin/secubox-appstore install "$app_id" >/dev/null 2>&1; then
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "App installed successfully"
json_dump
else
json_init
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "Installation failed"
json_add_string "details" "Check system logs for more information"
json_dump
@ -607,12 +625,12 @@ case "$1" in
# Use secubox-appstore for removal
if /usr/sbin/secubox-appstore remove "$app_id" >/dev/null 2>&1; then
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "App removed successfully"
json_dump
else
json_init
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "Removal failed"
json_add_string "details" "Check system logs for more information"
json_dump
@ -756,19 +774,19 @@ case "$1" in
if uci set "${CONFIG_NAME}.${SECTION_NAME}.force_source=$source" >/dev/null 2>&1 && \
uci commit "$CONFIG_NAME" >/dev/null 2>&1; then
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "Catalog source set to: $source"
json_dump
else
json_init
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "Failed to update UCI config"
json_dump
fi
else
json_init
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "No source specified"
json_dump
fi
@ -782,13 +800,13 @@ case "$1" in
# Call secubox-catalog-sync (with or without source)
if /usr/sbin/secubox-appstore sync ${source:+"$source"} 2>&1; then
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "Catalog synced successfully"
[ -n "$source" ] && json_add_string "source" "$source"
json_dump
else
json_init
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "Sync failed"
json_dump
fi
@ -942,12 +960,12 @@ case "$1" in
json_init
if echo "$result" | grep -q "Success:"; then
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "$result"
json_add_string "component_id" "$component_id"
json_add_string "new_state" "$new_state"
else
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
@ -981,10 +999,10 @@ case "$1" in
json_init
if echo "$result" | grep -q "Success:"; then
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "$result"
else
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
@ -998,10 +1016,10 @@ case "$1" in
json_init
if echo "$result" | grep -q "Success:"; then
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "$result"
else
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
@ -1039,7 +1057,7 @@ case "$1" in
# Extract settings object from input
# This is simplified - full implementation would parse settings JSON
json_init
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "Settings update functionality available via CLI: secubox-component set-setting"
json_dump
;;
@ -1049,15 +1067,53 @@ case "$1" in
json_init
if echo "$result" | grep -q "successfully"; then
json_add_boolean "success" true
json_add_boolean "success" 1
json_add_string "message" "$result"
else
json_add_boolean "success" false
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
;;
get_wan_access)
/usr/sbin/secubox-wan-access json
;;
set_wan_access)
read -r input
enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
https_enabled=$(echo "$input" | jsonfilter -e '@.https_enabled' 2>/dev/null)
https_port=$(echo "$input" | jsonfilter -e '@.https_port' 2>/dev/null)
http_enabled=$(echo "$input" | jsonfilter -e '@.http_enabled' 2>/dev/null)
http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null)
ssh_enabled=$(echo "$input" | jsonfilter -e '@.ssh_enabled' 2>/dev/null)
ssh_port=$(echo "$input" | jsonfilter -e '@.ssh_port' 2>/dev/null)
# Update UCI settings
[ -n "$enabled" ] && uci set secubox.remote.enabled="$enabled"
[ -n "$https_enabled" ] && uci set secubox.remote.https_enabled="$https_enabled"
[ -n "$https_port" ] && uci set secubox.remote.https_port="$https_port"
[ -n "$http_enabled" ] && uci set secubox.remote.http_enabled="$http_enabled"
[ -n "$http_port" ] && uci set secubox.remote.http_port="$http_port"
[ -n "$ssh_enabled" ] && uci set secubox.remote.ssh_enabled="$ssh_enabled"
[ -n "$ssh_port" ] && uci set secubox.remote.ssh_port="$ssh_port"
uci commit secubox
json_init
json_add_boolean "success" 1
json_add_string "message" "WAN access settings updated"
json_dump
;;
apply_wan_access)
/usr/sbin/secubox-wan-access apply >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_add_string "message" "WAN access rules applied"
json_dump
;;
*)
json_init
json_add_boolean "error" true

View File

@ -89,6 +89,23 @@ get_active_catalog() {
echo "$MAIN_CATALOG"
}
# Check if service is enabled (will start on boot)
is_service_enabled() {
local service="$1"
[ -z "$service" ] && return 1
[ -f "/etc/init.d/$service" ] || return 1
# Check if service has symlink in /etc/rc.d/
ls /etc/rc.d/S*"$service" >/dev/null 2>&1
}
# Check if service is running/active
is_service_active() {
local service="$1"
[ -z "$service" ] && return 1
[ -f "/etc/init.d/$service" ] || return 1
/etc/init.d/"$service" status >/dev/null 2>&1
}
# List all modules
list_modules() {
local format="${1:-table}"
@ -117,15 +134,62 @@ list_modules() {
local module_name=$(jsonfilter -i "$catalog" -e '@.name' 2>/dev/null)
local module_category=$(jsonfilter -i "$catalog" -e '@.category' 2>/dev/null)
local module_version=$(jsonfilter -i "$catalog" -e '@.version' 2>/dev/null)
local service_name=$(jsonfilter -i "$catalog" -e '@.procd.service' 2>/dev/null)
# Check if installed
# Auto-detect service name if not defined in catalog
if [ -z "$service_name" ]; then
case "$module_id" in
*crowdsec*) service_name="crowdsec" ;;
*netifyd*) service_name="netifyd" ;;
*adguardhome*) service_name="AdGuardHome" ;;
*wireguard*) service_name="wg-quick" ;;
*nginx*|*vhost*) service_name="nginx" ;;
*dnscrypt*) service_name="dnscrypt-proxy" ;;
*unbound*) service_name="unbound" ;;
*stubby*) service_name="stubby" ;;
*shadowsocks*) service_name="shadowsocks-libev" ;;
*zerotier*) service_name="zerotier" ;;
*tailscale*) service_name="tailscale" ;;
*docker*|*portainer*) service_name="dockerd" ;;
*homeassistant*) service_name="homeassistant" ;;
*jellyfin*) service_name="jellyfin" ;;
*nextcloud*) service_name="nextcloud" ;;
*uptimekuma*|*uptime-kuma*) service_name="uptime-kuma" ;;
*vaultwarden*) service_name="vaultwarden" ;;
*gitea*) service_name="gitea" ;;
*domoticz*) service_name="domoticz" ;;
*zigbee2mqtt*) service_name="zigbee2mqtt" ;;
*mosquitto*|*mqtt*) service_name="mosquitto" ;;
*netdata*) service_name="netdata" ;;
esac
fi
# Check if installed (support both packages.required[0] and packages[0] formats)
local packages=$(jsonfilter -i "$catalog" -e '@.packages.required[0]' 2>/dev/null)
[ -z "$packages" ] && packages=$(jsonfilter -i "$catalog" -e '@.packages[0]' 2>/dev/null)
local status="available"
local installed_version=""
local is_installed=0
local is_enabled=0
local is_active=0
if [ "$cache_ready" -eq 1 ] && [ -n "$packages" ]; then
installed_version=$(get_installed_version_from_cache "$packages")
if [ -n "$installed_version" ]; then
status="installed"
is_installed=1
# Check enabled/active status if service defined
if [ -n "$service_name" ]; then
if is_service_enabled "$service_name"; then
is_enabled=1
status="enabled"
fi
if is_service_active "$service_name"; then
is_active=1
status="active"
fi
fi
fi
fi
@ -137,7 +201,10 @@ list_modules() {
json_add_string "version" "$module_version"
json_add_string "status" "$status"
[ -n "$installed_version" ] && json_add_string "installed_version" "$installed_version"
json_add_boolean "installed" $([ "$status" = "installed" ] && echo true || echo false)
[ -n "$service_name" ] && json_add_string "service" "$service_name"
json_add_boolean "installed" "$is_installed"
json_add_boolean "enabled" "$is_enabled"
json_add_boolean "active" "$is_active"
json_close_object
else
printf "%-20s %-12s %-12s %-10s\n" \

View File

@ -0,0 +1,235 @@
#!/bin/sh
#
# SecuBox WAN Access Manager
# Manages firewall rules for remote access to LuCI/SecuBox
#
. /lib/functions.sh
RULE_PREFIX="secubox_wan"
# Remove all SecuBox WAN access rules
remove_rules() {
local changed=0
# Find and remove all secubox_wan rules
while uci -q get firewall.@rule[-1] >/dev/null 2>&1; do
local name=$(uci -q get firewall.@rule[-1].name)
if echo "$name" | grep -q "^${RULE_PREFIX}"; then
uci delete firewall.@rule[-1]
changed=1
else
break
fi
done
# Iterate through all rules to find secubox ones
local i=0
while true; do
local name=$(uci -q get firewall.@rule[$i].name)
[ -z "$name" ] && break
if echo "$name" | grep -q "^${RULE_PREFIX}"; then
uci delete firewall.@rule[$i]
changed=1
# Don't increment i since indices shift after delete
else
i=$((i + 1))
fi
done
[ "$changed" -eq 1 ] && return 0
return 1
}
# Detect WAN zone name (fallback to '*' for any)
get_wan_zone() {
# Try common WAN zone names
for zone in wan WAN external internet; do
if uci -q get firewall.@zone[] 2>/dev/null | grep -q "name='$zone'"; then
echo "$zone"
return
fi
done
# Check for zone with wan/wan6 network
local i=0
while true; do
local name=$(uci -q get firewall.@zone[$i].name 2>/dev/null)
[ -z "$name" ] && break
local network=$(uci -q get firewall.@zone[$i].network 2>/dev/null)
if echo "$network" | grep -qE '^wan|wan6|wan '; then
echo "$name"
return
fi
i=$((i + 1))
done
# No WAN zone found - use '*' for any source
echo "*"
}
# Add a firewall rule
add_rule() {
local name="$1"
local port="$2"
local proto="${3:-tcp}"
local src="${4:-$(get_wan_zone)}"
uci add firewall rule >/dev/null
uci set firewall.@rule[-1].name="$name"
uci set firewall.@rule[-1].src="$src"
uci set firewall.@rule[-1].dest_port="$port"
uci set firewall.@rule[-1].proto="$proto"
uci set firewall.@rule[-1].target="ACCEPT"
uci set firewall.@rule[-1].enabled="1"
}
# Apply rules based on secubox config
apply_rules() {
config_load secubox
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
config_get enabled remote enabled "0"
config_get https_enabled remote https_enabled "0"
config_get https_port remote https_port "443"
config_get http_enabled remote http_enabled "0"
config_get http_port remote http_port "80"
config_get ssh_enabled remote ssh_enabled "0"
config_get ssh_port remote ssh_port "22"
# Remove existing rules first
remove_rules
# Only add rules if WAN access is enabled
if [ "$enabled" = "1" ]; then
# HTTPS access
if [ "$https_enabled" = "1" ]; then
add_rule "${RULE_PREFIX}_https" "$https_port" "tcp" "wan"
echo "Added HTTPS access rule (port $https_port)"
fi
# HTTP access
if [ "$http_enabled" = "1" ]; then
add_rule "${RULE_PREFIX}_http" "$http_port" "tcp" "wan"
echo "Added HTTP access rule (port $http_port)"
fi
# SSH access
if [ "$ssh_enabled" = "1" ]; then
add_rule "${RULE_PREFIX}_ssh" "$ssh_port" "tcp" "wan"
echo "Added SSH access rule (port $ssh_port)"
fi
fi
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1
echo "WAN access rules applied"
}
# Show current status
status() {
config_load secubox
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
config_get enabled remote enabled "0"
config_get https_enabled remote https_enabled "0"
config_get https_port remote https_port "443"
config_get http_enabled remote http_enabled "0"
config_get http_port remote http_port "80"
config_get ssh_enabled remote ssh_enabled "0"
config_get ssh_port remote ssh_port "22"
echo "SecuBox WAN Access Status"
echo "========================="
echo "Master switch: $([ "$enabled" = "1" ] && echo "ENABLED" || echo "DISABLED")"
echo ""
echo "Services:"
echo " HTTPS (port $https_port): $([ "$https_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
echo " HTTP (port $http_port): $([ "$http_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
echo " SSH (port $ssh_port): $([ "$ssh_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
echo ""
echo "Active firewall rules:"
iptables -L INPUT -n --line-numbers 2>/dev/null | grep -E "dpt:(${https_port}|${http_port}|${ssh_port})" || echo " (none)"
}
# Enable WAN access
enable() {
uci set secubox.remote.enabled='1'
uci commit secubox
apply_rules
echo "WAN access enabled"
}
# Disable WAN access
disable() {
uci set secubox.remote.enabled='0'
uci commit secubox
remove_rules
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1
echo "WAN access disabled"
}
# JSON output for API
json_status() {
config_load secubox
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
config_get enabled remote enabled "0"
config_get https_enabled remote https_enabled "0"
config_get https_port remote https_port "443"
config_get http_enabled remote http_enabled "0"
config_get http_port remote http_port "80"
config_get ssh_enabled remote ssh_enabled "0"
config_get ssh_port remote ssh_port "22"
. /usr/share/libubox/jshn.sh
json_init
json_add_boolean "enabled" "$enabled"
json_add_object "services"
json_add_object "https"
json_add_boolean "enabled" "$https_enabled"
json_add_int "port" "$https_port"
json_close_object
json_add_object "http"
json_add_boolean "enabled" "$http_enabled"
json_add_int "port" "$http_port"
json_close_object
json_add_object "ssh"
json_add_boolean "enabled" "$ssh_enabled"
json_add_int "port" "$ssh_port"
json_close_object
json_close_object
json_dump
}
case "$1" in
apply)
apply_rules
;;
remove)
remove_rules
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1
echo "WAN access rules removed"
;;
enable)
enable
;;
disable)
disable
;;
status)
status
;;
json)
json_status
;;
*)
echo "Usage: $0 {apply|remove|enable|disable|status|json}"
exit 1
;;
esac