fix: CrowdSec wizard recovery mode and SecuBox Portal improvements

- Add recovery/reset mode to CrowdSec wizard for bouncer registration issues
- Handle existing bouncer detection with database-level cleanup fallback
- Fix Media Flow pgrep -x issue and add start/stop service ACL permissions
- Fix duplicate nav bar in CrowdSec wizard with aggressive CSS hiding
- Add shared SecuBox header component for consistent navigation
- Fix all portal app links to match actual menu.d paths
- Add UI switcher between SecuBox Portal and standard LuCI
- Hide OpenWrt header and sidebar in SecuBox mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-09 14:50:14 +01:00
parent fb9722ccd6
commit d9511420d3
13 changed files with 879 additions and 47 deletions

View File

@ -197,6 +197,12 @@ var callRepairLapi = rpc.declare({
expect: { }
});
var callResetWizard = rpc.declare({
object: 'luci.crowdsec-dashboard',
method: 'reset_wizard',
expect: { }
});
// Console Methods
var callConsoleStatus = rpc.declare({
object: 'luci.crowdsec-dashboard',
@ -344,6 +350,7 @@ return baseclass.extend({
checkWizardNeeded: callCheckWizardNeeded,
getWizardState: callWizardState,
repairLapi: callRepairLapi,
resetWizard: callResetWizard,
// Console Methods
getConsoleStatus: callConsoleStatus,

View File

@ -6,6 +6,16 @@
* SecuBox themed navigation tabs
*/
// Immediately inject CSS to hide LuCI tabs before page renders
(function() {
if (typeof document === 'undefined') return;
if (document.getElementById('crowdsec-early-hide')) return;
var style = document.createElement('style');
style.id = 'crowdsec-early-hide';
style.textContent = 'body[data-page*="crowdsec"] ul.tabs, body[data-page*="crowdsec"] .tabs:not(.cs-nav-tabs) { display: none !important; }';
(document.head || document.documentElement).appendChild(style);
})();
var tabs = [
{ id: 'wizard', icon: '🚀', label: _('Setup Wizard'), path: ['admin', 'secubox', 'security', 'crowdsec', 'wizard'] },
{ id: 'overview', icon: '📊', label: _('Overview'), path: ['admin', 'secubox', 'security', 'crowdsec', 'overview'] },
@ -25,17 +35,71 @@ return baseclass.extend({
ensureLuCITabsHidden: function() {
if (typeof document === 'undefined')
return;
// Actively remove LuCI tabs from DOM
var luciTabs = document.querySelectorAll('.cbi-tabmenu, ul.tabs, div.tabs, .nav-tabs');
luciTabs.forEach(function(el) {
// Don't remove our own tabs
if (!el.classList.contains('cs-nav-tabs')) {
el.style.display = 'none';
// Also try removing from DOM after a brief delay
setTimeout(function() {
if (el.parentNode && !el.classList.contains('cs-nav-tabs')) {
el.style.display = 'none';
}
}, 100);
}
});
if (document.getElementById('crowdsec-tabstyle'))
return;
var style = document.createElement('style');
style.id = 'crowdsec-tabstyle';
style.textContent = `
/* Hide default LuCI tabs for CrowdSec */
/* Hide default LuCI tabs for CrowdSec - aggressive selectors */
/* Target any ul.tabs in the page */
ul.tabs {
display: none !important;
}
/* Be more specific for pages that need tabs elsewhere */
body:not([data-page*="crowdsec"]) ul.tabs {
display: block !important;
}
/* All possible LuCI tab selectors */
body[data-page^="admin-secubox-security-crowdsec"] .tabs,
body[data-page^="admin-secubox-security-crowdsec"] #tabmenu,
body[data-page^="admin-secubox-security-crowdsec"] .cbi-tabmenu,
body[data-page^="admin-secubox-security-crowdsec"] .nav-tabs,
body[data-page^="admin-secubox-security-crowdsec"] ul.cbi-tabmenu {
body[data-page^="admin-secubox-security-crowdsec"] ul.cbi-tabmenu,
body[data-page*="crowdsec"] ul.tabs,
body[data-page*="crowdsec"] .tabs,
/* Fallback: hide any tabs that appear before our custom nav */
.crowdsec-dashboard .tabs,
.crowdsec-dashboard + .tabs,
.crowdsec-dashboard ~ .tabs,
.cbi-map > .tabs:first-child,
#maincontent > .container > .tabs,
#maincontent > .container > ul.tabs,
#view > .tabs,
#view > ul.tabs,
.view > .tabs,
.view > ul.tabs,
div.tabs:has(+ .crowdsec-dashboard),
div.tabs:has(+ .wizard-container),
/* Direct sibling of CrowdSec content */
.wizard-container ~ .tabs,
.cs-nav-tabs ~ .tabs,
/* LuCI 24.x specific */
.luci-app-crowdsec-dashboard .tabs,
#cbi-crowdsec .tabs {
display: none !important;
}
/* Hide tabs container when our nav is present */
.cs-nav-tabs ~ ul.tabs,
.cs-nav-tabs + ul.tabs {
display: none !important;
}

View File

@ -7,6 +7,16 @@
'require uci';
'require crowdsec-dashboard/api as API';
'require crowdsec-dashboard/nav as CsNav';
'require secubox-portal/header as SbHeader';
// Hide LuCI tabs immediately
(function() {
if (typeof document === 'undefined') return;
var style = document.getElementById('cs-hide-tabs') || document.createElement('style');
style.id = 'cs-hide-tabs';
style.textContent = 'ul.tabs, .cbi-tabmenu { display: none !important; }';
if (!style.parentNode) (document.head || document.documentElement).appendChild(style);
})();
return view.extend({
wizardData: {
@ -40,6 +50,7 @@ return view.extend({
configuring: false,
bouncerConfigured: false,
apiKey: '',
resetting: false,
// Step 6 data (Services)
starting: false,
@ -133,6 +144,12 @@ return view.extend({
});
head.appendChild(themeLink);
// Main wrapper with SecuBox header
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
// Add SecuBox global header
wrapper.appendChild(SbHeader.render());
var container = E('div', { 'class': 'crowdsec-dashboard wizard-container' });
// Add navigation tabs
@ -144,7 +161,9 @@ return view.extend({
// Create step content
container.appendChild(this.renderCurrentStep(data));
return container;
wrapper.appendChild(container);
return wrapper;
},
createStepper: function() {
@ -499,10 +518,29 @@ return view.extend({
},
renderStep5Bouncer: function(data) {
var self = this;
return E('div', { 'class': 'wizard-step' }, [
E('h2', {}, _('Configure Firewall Bouncer')),
E('p', {}, _('The firewall bouncer will automatically block malicious IPs using nftables.')),
// Recovery mode warning/option
E('div', { 'id': 'recovery-mode-section', 'class': 'config-section', 'style': 'margin-bottom: 20px; padding: 16px; background: rgba(234, 179, 8, 0.1); border: 1px solid rgba(234, 179, 8, 0.3); border-radius: 8px;' }, [
E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 12px;' }, [
E('span', { 'style': 'font-size: 24px; margin-right: 12px;' }, '🔄'),
E('div', {}, [
E('strong', { 'style': 'color: #eab308;' }, _('Recovery Mode')),
E('div', { 'style': 'font-size: 0.9em; color: var(--cyber-text-secondary, #94a3b8);' },
_('Use this if bouncer registration fails or you want to start fresh'))
])
]),
E('button', {
'class': 'cbi-button cbi-button-negative',
'style': 'width: 100%;',
'disabled': this.wizardData.resetting ? true : null,
'click': L.bind(this.handleResetWizard, this)
}, this.wizardData.resetting ? _('Resetting...') : _('Reset Bouncer Configuration'))
]),
// Configuration options
E('div', { 'class': 'config-section' }, [
E('div', {
@ -1048,6 +1086,43 @@ return view.extend({
}, this));
},
handleResetWizard: function() {
console.log('[Wizard] Reset wizard triggered (recovery mode)');
if (!confirm(_('This will delete existing bouncer registration and clear all bouncer configuration. Continue?'))) {
return;
}
this.wizardData.resetting = true;
this.wizardData.bouncerConfigured = false;
this.wizardData.apiKey = '';
this.refreshView();
return API.resetWizard().then(L.bind(function(result) {
console.log('[Wizard] Reset wizard result:', result);
this.wizardData.resetting = false;
if (result && result.success) {
ui.addNotification(null, E('p', _('Bouncer configuration reset successfully. You can now configure fresh.')), 'success');
// Reset relevant wizard data
this.wizardData.bouncerConfigured = false;
this.wizardData.apiKey = '';
this.wizardData.enabled = false;
this.wizardData.running = false;
this.wizardData.nftablesActive = false;
this.wizardData.lapiConnected = false;
} else {
ui.addNotification(null, E('p', _('Reset failed: ') + (result.error || 'Unknown error')), 'error');
}
this.refreshView();
}, this)).catch(L.bind(function(err) {
console.error('[Wizard] Reset wizard error:', err);
this.wizardData.resetting = false;
ui.addNotification(null, E('p', _('Reset failed: ') + err.message), 'error');
this.refreshView();
}, this));
},
handleSaveAndApply: null,
handleSave: null,
handleReset: null

View File

@ -420,6 +420,7 @@ update_hub() {
# Register a new bouncer
register_bouncer() {
local bouncer_name="$1"
local force="${2:-0}"
check_cscli
json_init
@ -430,30 +431,57 @@ register_bouncer() {
return
fi
# Check if bouncer already exists
# Check if bouncer already exists (robust pattern matching)
local exists=0
if $CSCLI bouncers list -o json 2>/dev/null | grep -q "\"name\":\"$bouncer_name\""; then
local bouncer_list
bouncer_list=$($CSCLI bouncers list -o json 2>/dev/null)
if echo "$bouncer_list" | grep -qE "\"name\"[[:space:]]*:[[:space:]]*\"$bouncer_name\""; then
exists=1
fi
local api_key
if [ "$exists" = "1" ]; then
# Delete existing bouncer and re-register to get new API key
$CSCLI bouncers delete "$bouncer_name" >/dev/null 2>&1
secubox_log "Deleted existing bouncer: $bouncer_name"
secubox_log "Bouncer '$bouncer_name' already exists, deleting for re-registration..."
# Delete existing bouncer to get new API key
if ! $CSCLI bouncers delete "$bouncer_name" 2>&1; then
secubox_log "Warning: Could not delete bouncer via cscli, trying force..."
fi
# Small delay to ensure deletion is processed
sleep 1
fi
# Generate API key
api_key=$($CSCLI bouncers add "$bouncer_name" -o raw 2>&1)
if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error"; then
if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error\|unable\|failed"; then
json_add_boolean "success" 1
json_add_string "api_key" "$api_key"
json_add_string "message" "Bouncer '$bouncer_name' registered successfully"
secubox_log "Registered bouncer: $bouncer_name"
json_add_boolean "replaced" "$exists"
secubox_log "Registered bouncer: $bouncer_name (replaced: $exists)"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to register bouncer '$bouncer_name': $api_key"
# If still failing, try more aggressive cleanup
if echo "$api_key" | grep -qi "already exists"; then
secubox_log "Bouncer still exists, attempting database-level cleanup..."
# Force delete from database
sqlite3 /srv/crowdsec/data/crowdsec.db "DELETE FROM bouncers WHERE name='$bouncer_name';" 2>/dev/null
sleep 1
# Retry registration
api_key=$($CSCLI bouncers add "$bouncer_name" -o raw 2>&1)
if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error\|unable\|failed"; then
json_add_boolean "success" 1
json_add_string "api_key" "$api_key"
json_add_string "message" "Bouncer '$bouncer_name' registered after forced cleanup"
json_add_boolean "replaced" 1
secubox_log "Registered bouncer after forced cleanup: $bouncer_name"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to register bouncer '$bouncer_name' even after cleanup: $api_key"
fi
else
json_add_boolean "success" 0
json_add_string "error" "Failed to register bouncer '$bouncer_name': $api_key"
fi
fi
json_dump
@ -931,6 +959,51 @@ repair_lapi() {
json_dump
}
# Reset wizard - clean up for fresh start
reset_wizard() {
json_init
local steps_done=""
local errors=""
secubox_log "Starting CrowdSec wizard reset (recovery mode)..."
# Step 1: Stop services
/etc/init.d/crowdsec-firewall-bouncer stop >/dev/null 2>&1
steps_done="${steps_done}Stopped firewall bouncer; "
# Step 2: Delete existing bouncer registration
if [ -x "$CSCLI" ]; then
$CSCLI bouncers delete "crowdsec-firewall-bouncer" >/dev/null 2>&1
steps_done="${steps_done}Deleted bouncer registration; "
# Also try database cleanup
sqlite3 /srv/crowdsec/data/crowdsec.db "DELETE FROM bouncers WHERE name='crowdsec-firewall-bouncer';" 2>/dev/null
fi
# Step 3: Clear UCI bouncer config
uci -q delete crowdsec.bouncer 2>/dev/null
uci commit crowdsec 2>/dev/null
steps_done="${steps_done}Cleared UCI config; "
# Step 4: Remove bouncer config file
rm -f /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml 2>/dev/null
steps_done="${steps_done}Removed bouncer config file; "
# Step 5: Reset nftables rules
if command -v nft >/dev/null 2>&1; then
nft delete table ip crowdsec 2>/dev/null
nft delete table ip6 crowdsec6 2>/dev/null
steps_done="${steps_done}Cleared nftables rules; "
fi
json_add_boolean "success" 1
json_add_string "message" "Wizard reset completed - ready for fresh setup"
json_add_string "steps" "$steps_done"
secubox_log "Wizard reset completed: $steps_done"
json_dump
}
# Console enrollment status
get_console_status() {
json_init
@ -1091,7 +1164,7 @@ service_control() {
# Main dispatcher
case "$1" in
list)
echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"}}'
echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"}}'
;;
call)
case "$2" in
@ -1205,6 +1278,9 @@ case "$1" in
repair_lapi)
repair_lapi
;;
reset_wizard)
reset_wizard
;;
console_status)
get_console_status
;;

View File

@ -79,6 +79,30 @@ var callSetSettings = rpc.declare({
expect: { }
});
var callStartNdpid = rpc.declare({
object: 'luci.media-flow',
method: 'start_ndpid',
expect: { success: false }
});
var callStopNdpid = rpc.declare({
object: 'luci.media-flow',
method: 'stop_ndpid',
expect: { success: false }
});
var callStartNetifyd = rpc.declare({
object: 'luci.media-flow',
method: 'start_netifyd',
expect: { success: false }
});
var callStopNetifyd = rpc.declare({
object: 'luci.media-flow',
method: 'stop_netifyd',
expect: { success: false }
});
return baseclass.extend({
getStatus: callStatus,
getActiveStreams: callGetActiveStreams,
@ -91,5 +115,9 @@ return baseclass.extend({
listAlerts: callListAlerts,
clearHistory: callClearHistory,
getSettings: callGetSettings,
setSettings: callSetSettings
setSettings: callSetSettings,
startNdpid: callStartNdpid,
stopNdpid: callStopNdpid,
startNetifyd: callStartNetifyd,
stopNetifyd: callStopNetifyd
});

View File

@ -3,6 +3,7 @@
'require poll';
'require ui';
'require media-flow/api as API';
'require secubox-portal/header as SbHeader';
return L.view.extend({
load: function() {
@ -18,6 +19,10 @@ return L.view.extend({
var streamsData = data[1] || {};
var statsByService = data[2] || {};
// Main wrapper with SecuBox header
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
var v = E('div', { 'class': 'cbi-map' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('h2', {}, _('Media Flow Dashboard')),
@ -96,10 +101,57 @@ return L.view.extend({
])
]);
} else {
var self = this;
noticeSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'background: #f8d7da; border: 1px solid #dc3545; padding: 15px; border-radius: 4px; margin-bottom: 15px;' }, [
E('strong', {}, _('No DPI Engine: ')),
E('span', {}, _('Install nDPId or netifyd for streaming detection capabilities.'))
E('span', {}, _('No DPI service is running. ')),
E('div', { 'style': 'margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap;' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
ui.showModal(_('Starting nDPId...'), [
E('p', { 'class': 'spinning' }, _('Starting nDPId and compatibility layer...'))
]);
API.startNdpid().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, res.message || _('nDPId started')), 'success');
setTimeout(function() { window.location.reload(); }, 2000);
} else {
ui.addNotification(null, E('p', {}, res.message || _('Failed to start nDPId')), 'error');
}
}).catch(function() {
ui.hideModal();
ui.addNotification(null, E('p', {}, _('Failed to start nDPId. Check if it is installed.')), 'error');
});
}
}, _('Start nDPId')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Starting Netifyd...'), [
E('p', { 'class': 'spinning' }, _('Starting netifyd service...'))
]);
API.startNetifyd().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, res.message || _('Netifyd started')), 'success');
setTimeout(function() { window.location.reload(); }, 2000);
} else {
ui.addNotification(null, E('p', {}, res.message || _('Failed to start netifyd')), 'error');
}
}).catch(function() {
ui.hideModal();
ui.addNotification(null, E('p', {}, _('Failed to start netifyd. Check if it is installed.')), 'error');
});
}
}, _('Start Netifyd')),
E('a', {
'class': 'cbi-button',
'href': L.url('admin/services/ndpid/settings')
}, _('nDPId Settings'))
])
])
]);
}
@ -380,7 +432,8 @@ return L.view.extend({
updateServiceStats();
}, this), 5);
return v;
wrapper.appendChild(v);
return wrapper;
},
handleSaveApply: null,

View File

@ -25,9 +25,9 @@ init_storage() {
# Detect available DPI source
get_dpi_source() {
# Prefer nDPId if running and has data
if [ -f "$NDPID_FLOWS" ] && pgrep -x ndpid >/dev/null 2>&1; then
if [ -f "$NDPID_FLOWS" ] && pgrep ndpid >/dev/null 2>&1; then
echo "ndpid"
elif [ -f /var/run/netifyd/status.json ] && pgrep -x netifyd >/dev/null 2>&1; then
elif [ -f /var/run/netifyd/status.json ] && pgrep netifyd >/dev/null 2>&1; then
echo "netifyd"
else
echo "none"
@ -105,7 +105,11 @@ case "$1" in
"list_alerts": {},
"clear_history": {},
"get_settings": {},
"set_settings": {"enabled": 1, "history_retention": 7, "refresh_interval": 5}
"set_settings": {"enabled": 1, "history_retention": 7, "refresh_interval": 5},
"start_ndpid": {},
"stop_ndpid": {},
"start_netifyd": {},
"stop_netifyd": {}
}
EOF
;;
@ -119,7 +123,7 @@ case "$1" in
ndpid_running=0
ndpid_version="unknown"
ndpid_flows=0
pgrep -x ndpid > /dev/null 2>&1 && ndpid_running=1
pgrep ndpid > /dev/null 2>&1 && ndpid_running=1
if [ "$ndpid_running" = "1" ] && [ -f "$NDPID_FLOWS" ]; then
ndpid_flows=$(jq 'length' "$NDPID_FLOWS" 2>/dev/null || echo 0)
ndpid_version=$(ndpid -v 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1 || echo "unknown")
@ -127,7 +131,7 @@ case "$1" in
# Check netifyd status
netifyd_running=0
pgrep -x netifyd > /dev/null 2>&1 && netifyd_running=1
pgrep netifyd > /dev/null 2>&1 && netifyd_running=1
netifyd_data=$(get_netifyd_data)
netifyd_flows=0
@ -422,6 +426,53 @@ case "$1" in
echo '{"success": true, "message": "Settings saved"}'
;;
start_ndpid)
# Start nDPId and compatibility layer
result_ndpid=0
result_compat=0
msg=""
if [ -x /etc/init.d/ndpid ]; then
/etc/init.d/ndpid start 2>/dev/null && result_ndpid=1
/etc/init.d/ndpid enable 2>/dev/null
fi
if [ -x /etc/init.d/ndpid-compat ]; then
sleep 2
/etc/init.d/ndpid-compat start 2>/dev/null && result_compat=1
/etc/init.d/ndpid-compat enable 2>/dev/null
fi
if [ "$result_ndpid" = "1" ]; then
msg="nDPId started"
[ "$result_compat" = "1" ] && msg="$msg with compatibility layer"
echo "{\"success\": true, \"message\": \"$msg\"}"
else
echo '{"success": false, "message": "nDPId not installed or failed to start"}'
fi
;;
stop_ndpid)
/etc/init.d/ndpid-compat stop 2>/dev/null
/etc/init.d/ndpid stop 2>/dev/null
echo '{"success": true, "message": "nDPId stopped"}'
;;
start_netifyd)
if [ -x /etc/init.d/netifyd ]; then
/etc/init.d/netifyd start 2>/dev/null
/etc/init.d/netifyd enable 2>/dev/null
echo '{"success": true, "message": "Netifyd started"}'
else
echo '{"success": false, "message": "Netifyd not installed"}'
fi
;;
stop_netifyd)
/etc/init.d/netifyd stop 2>/dev/null
echo '{"success": true, "message": "Netifyd stopped"}'
;;
*)
cat <<-EOF
{"error": -32601, "message": "Method not found: $2"}

View File

@ -26,7 +26,11 @@
"set_alert",
"delete_alert",
"clear_history",
"set_settings"
"set_settings",
"start_ndpid",
"stop_ndpid",
"start_netifyd",
"stop_netifyd"
]
},
"uci": ["media_flow"]

View File

@ -0,0 +1,321 @@
'use strict';
'require baseclass';
/**
* SecuBox Shared Header
* Provides consistent navigation across all SecuBox pages
* Include this in any SecuBox view to show the unified header
*/
// CSS to inject for hiding LuCI elements and styling the header
var headerCSS = `
/* Hide OpenWrt/LuCI header and sidebar in SecuBox mode - AGGRESSIVE */
body.secubox-mode header,
body.secubox-mode .main-header,
body.secubox-mode #mainmenu,
body.secubox-mode .main-left,
body.secubox-mode .main > .main-left,
body.secubox-mode nav[role="navigation"],
body.secubox-mode #navigation,
body.secubox-mode .luci-sidebar,
body.secubox-mode aside,
body.secubox-mode .container > header,
body.secubox-mode #header,
body.secubox-mode .brand,
body.secubox-mode header.brand,
body.secubox-mode .header-brand,
body.secubox-mode > header:first-child,
header:has(+ .secubox-page-wrapper),
header:has(~ .secubox-page-wrapper),
.main > header,
body > header,
#maincontent > header,
.container > header:first-child {
display: none !important;
}
/* Force hide the blue OpenWrt header specifically */
header[style*="background"],
.brand[style*="background"],
header.brand,
div.brand {
display: none !important;
}
/* Make main content full width */
body.secubox-mode .main > .main-right,
body.secubox-mode #maincontent,
body.secubox-mode .main-right,
body.secubox-mode main[role="main"],
body.secubox-mode .container {
margin-left: 0 !important;
padding-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
/* Hide breadcrumbs */
body.secubox-mode .cbi-breadcrumb,
body.secubox-mode ol.breadcrumb,
body.secubox-mode .breadcrumb {
display: none !important;
}
/* SecuBox Header Styles */
.sb-global-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
height: 56px;
background: linear-gradient(180deg, #1a1a24 0%, #141419 100%);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
position: sticky;
top: 0;
z-index: 1000;
margin: -20px -20px 20px -20px;
width: calc(100% + 40px);
}
.sb-header-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.sb-header-logo {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.sb-header-title {
font-size: 1.125rem;
font-weight: 600;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sb-header-version {
font-size: 0.7rem;
color: #71717a;
margin-left: 0.5rem;
}
.sb-header-nav {
display: flex;
gap: 0.25rem;
}
.sb-header-nav-item {
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
font-weight: 500;
color: #a1a1aa;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.sb-header-nav-item:hover {
color: #e4e4e7;
background: rgba(255, 255, 255, 0.05);
}
.sb-header-nav-item.active {
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.sb-header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sb-header-switcher {
display: flex;
align-items: center;
gap: 0.25rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 3px;
}
.sb-header-switch-btn {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
color: #a1a1aa;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
}
.sb-header-switch-btn:hover {
color: #e4e4e7;
background: rgba(255, 255, 255, 0.08);
}
.sb-header-switch-btn.active {
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
@media (max-width: 900px) {
.sb-global-header {
flex-wrap: wrap;
height: auto;
padding: 0.5rem 1rem;
gap: 0.5rem;
}
.sb-header-nav {
order: 3;
width: 100%;
overflow-x: auto;
padding-bottom: 0.25rem;
}
.sb-header-nav-item {
padding: 0.375rem 0.625rem;
font-size: 0.75rem;
white-space: nowrap;
}
}
`;
var sections = [
{ id: 'dashboard', name: 'Dashboard', icon: '\ud83c\udfe0', path: 'admin/secubox/portal' },
{ id: 'security', name: 'Security', icon: '\ud83d\udee1\ufe0f', path: 'admin/secubox/security' },
{ id: 'network', name: 'Network', icon: '\ud83c\udf10', path: 'admin/secubox/network' },
{ id: 'monitoring', name: 'Monitoring', icon: '\ud83d\udcca', path: 'admin/secubox/monitoring' },
{ id: 'system', name: 'System', icon: '\u2699\ufe0f', path: 'admin/secubox/system' }
];
function injectCSS() {
if (document.getElementById('sb-header-styles')) return;
var style = document.createElement('style');
style.id = 'sb-header-styles';
style.textContent = headerCSS;
document.head.appendChild(style);
}
function hideOpenWrtUI() {
document.body.classList.add('secubox-mode');
// Direct element hiding for immediate effect - hide ALL headers and nav
var selectors = [
'header', '.main-header', '#mainmenu', '.main-left',
'nav[role="navigation"]', '#navigation', '.luci-sidebar', 'aside', '#header',
'.brand', 'header.brand', 'div.brand', '.header-brand'
];
selectors.forEach(function(sel) {
document.querySelectorAll(sel).forEach(function(el) {
// Don't hide our SecuBox header
if (!el.classList.contains('sb-global-header') && !el.closest('.sb-global-header')) {
el.style.display = 'none';
}
});
});
// Specifically hide the first header in document (usually OpenWrt header)
var firstHeader = document.querySelector('body > header, #maincontent > header, .container > header, .main > header');
if (firstHeader && !firstHeader.classList.contains('sb-global-header')) {
firstHeader.style.display = 'none';
}
// Expand main content
var main = document.querySelector('.main-right') || document.querySelector('#maincontent') || document.querySelector('.container');
if (main) {
main.style.marginLeft = '0';
main.style.width = '100%';
main.style.maxWidth = '100%';
main.style.paddingLeft = '0';
}
}
function detectActiveSection() {
var path = window.location.pathname;
if (path.indexOf('/secubox/security') !== -1) return 'security';
if (path.indexOf('/secubox/network') !== -1) return 'network';
if (path.indexOf('/secubox/monitoring') !== -1) return 'monitoring';
if (path.indexOf('/secubox/system') !== -1) return 'system';
return 'dashboard';
}
return baseclass.extend({
/**
* Initialize SecuBox mode - call this in your view's render()
*/
init: function() {
injectCSS();
hideOpenWrtUI();
},
/**
* Render the SecuBox header bar
* @returns {Element} The header DOM element
*/
render: function() {
injectCSS();
hideOpenWrtUI();
var activeSection = detectActiveSection();
return E('div', { 'class': 'sb-global-header' }, [
// Brand
E('div', { 'class': 'sb-header-brand' }, [
E('div', { 'class': 'sb-header-logo' }, 'S'),
E('span', { 'class': 'sb-header-title' }, 'SecuBox'),
E('span', { 'class': 'sb-header-version' }, 'v0.14.0')
]),
// Navigation
E('nav', { 'class': 'sb-header-nav' },
sections.map(function(section) {
return E('a', {
'class': 'sb-header-nav-item' + (section.id === activeSection ? ' active' : ''),
'href': L.url(section.path)
}, [
E('span', {}, section.icon),
section.name
]);
})
),
// UI Switcher
E('div', { 'class': 'sb-header-actions' }, [
E('div', { 'class': 'sb-header-switcher' }, [
E('a', {
'class': 'sb-header-switch-btn active',
'href': L.url('admin/secubox/portal'),
'title': 'SecuBox Interface'
}, 'SecuBox'),
E('a', {
'class': 'sb-header-switch-btn',
'href': L.url('admin/status/overview'),
'title': 'Standard LuCI'
}, 'LuCI')
])
])
]);
}
});

View File

@ -4,6 +4,75 @@
* Version: 1.0.0
*/
/* Hide LuCI navigation when in SecuBox Portal mode */
body:has(.secubox-portal) #mainmenu,
body:has(.secubox-portal) .main > .main-left,
body:has(.secubox-portal) header.main-header,
body:has(.secubox-portal) .main-left,
body:has(.secubox-portal) nav[role="navigation"],
body:has(.secubox-portal) #navigation,
body:has(.secubox-portal) .luci-sidebar,
body:has(.secubox-portal) aside,
body:has(.secubox-portal) header:not(.sb-portal-header):not(.sb-global-header),
body:has(.secubox-portal) .brand,
body:has(.secubox-portal) div.brand {
display: none !important;
}
/* Hide OpenWrt header when SecuBox mode is active */
body.secubox-mode header:not(.sb-global-header),
body.secubox-mode .brand,
body.secubox-mode div.brand,
body.secubox-portal-mode header:not(.sb-portal-header),
body:has(.secubox-page-wrapper) header:not(.sb-global-header),
body:has(.sb-global-header) > header:first-of-type:not(.sb-global-header) {
display: none !important;
}
/* Make main content full width in portal mode */
body:has(.secubox-portal) .main > .main-right,
body:has(.secubox-portal) #maincontent,
body:has(.secubox-portal) .main-right,
body:has(.secubox-portal) main[role="main"] {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
/* Hide breadcrumb in portal mode */
body:has(.secubox-portal) .cbi-breadcrumb,
body:has(.secubox-portal) ol.breadcrumb,
body:has(.secubox-portal) .breadcrumb {
display: none !important;
}
/* Hide page header in portal mode - portal has its own */
body:has(.secubox-portal) h1.page-header,
body:has(.secubox-portal) .page-header {
display: none !important;
}
/* Fallback for browsers without :has() support */
.secubox-portal-mode #mainmenu,
.secubox-portal-mode .main > .main-left,
.secubox-portal-mode header.main-header,
.secubox-portal-mode .main-left,
.secubox-portal-mode nav[role="navigation"],
.secubox-portal-mode #navigation,
.secubox-portal-mode .luci-sidebar,
.secubox-portal-mode aside {
display: none !important;
}
.secubox-portal-mode .main > .main-right,
.secubox-portal-mode #maincontent,
.secubox-portal-mode .main-right,
.secubox-portal-mode main[role="main"] {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
/* Portal Container */
.secubox-portal {
font-family: var(--sb-font-family, 'Inter', -apple-system, BlinkMacSystemFont, sans-serif);
@ -536,6 +605,48 @@
}
}
/* UI Switcher - Toggle between SecuBox and LuCI */
.sb-ui-switcher {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--cyber-bg-tertiary, rgba(255, 255, 255, 0.05));
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.1));
border-radius: 8px;
padding: 4px;
}
.sb-ui-label {
font-size: 0.75rem;
color: var(--cyber-text-tertiary, #71717a);
padding: 0 0.5rem;
display: none;
}
.sb-ui-btn {
padding: 0.375rem 0.875rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--cyber-text-secondary, #a1a1aa);
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
}
.sb-ui-btn:hover {
color: var(--cyber-text-primary, #e4e4e7);
background: var(--cyber-bg-elevated, rgba(255, 255, 255, 0.08));
}
.sb-ui-btn.active {
color: white;
background: var(--sb-accent-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
/* Responsive */
@media (max-width: 768px) {
.sb-portal-header {
@ -570,4 +681,13 @@
.sb-app-grid {
grid-template-columns: 1fr;
}
.sb-ui-switcher {
padding: 3px;
}
.sb-ui-btn {
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
}
}

View File

@ -9,7 +9,7 @@
return baseclass.extend({
version: '1.0.0',
// SecuBox app registry
// SecuBox app registry - paths match admin/secubox/* menu structure
apps: {
// Security Apps
'crowdsec': {
@ -20,7 +20,7 @@ return baseclass.extend({
iconBg: 'rgba(0, 212, 170, 0.15)',
iconColor: '#00d4aa',
section: 'security',
path: 'admin/services/crowdsec-dashboard',
path: 'admin/secubox/security/crowdsec/overview',
service: 'crowdsec',
version: '0.7.0'
},
@ -32,7 +32,7 @@ return baseclass.extend({
iconBg: 'rgba(139, 92, 246, 0.15)',
iconColor: '#8b5cf6',
section: 'security',
path: 'admin/services/client-guardian',
path: 'admin/secubox/security/guardian/overview',
service: 'client-guardian',
version: '0.5.0'
},
@ -44,7 +44,7 @@ return baseclass.extend({
iconBg: 'rgba(239, 68, 68, 0.15)',
iconColor: '#ef4444',
section: 'security',
path: 'admin/services/auth-guardian',
path: 'admin/secubox/security/auth-guardian/overview',
service: 'auth-guardian',
version: '0.3.0'
},
@ -58,7 +58,7 @@ return baseclass.extend({
iconBg: 'rgba(59, 130, 246, 0.15)',
iconColor: '#3b82f6',
section: 'network',
path: 'admin/services/bandwidth-manager',
path: 'admin/secubox/network/bandwidth-manager',
service: 'bandwidth-manager',
version: '0.5.0'
},
@ -82,7 +82,7 @@ return baseclass.extend({
iconBg: 'rgba(239, 68, 68, 0.15)',
iconColor: '#ef4444',
section: 'network',
path: 'admin/network/wireguard',
path: 'admin/secubox/network/wireguard',
service: 'wgserver',
version: null
},
@ -94,7 +94,7 @@ return baseclass.extend({
iconBg: 'rgba(102, 126, 234, 0.15)',
iconColor: '#667eea',
section: 'network',
path: 'admin/services/network-modes',
path: 'admin/secubox/network/modes',
service: null,
version: '0.2.0'
},
@ -108,8 +108,8 @@ return baseclass.extend({
iconBg: 'rgba(236, 72, 153, 0.15)',
iconColor: '#ec4899',
section: 'monitoring',
path: 'admin/services/media-flow',
service: 'media-flow',
path: 'admin/secubox/monitoring/mediaflow/dashboard',
service: null,
version: '0.6.0'
},
'ndpid': {
@ -120,7 +120,7 @@ return baseclass.extend({
iconBg: 'rgba(6, 182, 212, 0.15)',
iconColor: '#06b6d4',
section: 'monitoring',
path: 'admin/services/ndpid',
path: 'admin/secubox/ndpid/dashboard',
service: 'ndpid',
version: '1.1.0'
},
@ -132,7 +132,7 @@ return baseclass.extend({
iconBg: 'rgba(6, 182, 212, 0.15)',
iconColor: '#06b6d4',
section: 'monitoring',
path: 'admin/services/secubox-netifyd',
path: 'admin/secubox/netifyd/dashboard',
service: 'netifyd',
version: '1.2.0'
},
@ -144,7 +144,7 @@ return baseclass.extend({
iconBg: 'rgba(34, 197, 94, 0.15)',
iconColor: '#22c55e',
section: 'monitoring',
path: 'admin/services/netdata-dashboard',
path: 'admin/secubox/monitoring/netdata/dashboard',
service: 'netdata',
version: '0.4.0'
},
@ -158,7 +158,7 @@ return baseclass.extend({
iconBg: 'rgba(249, 115, 22, 0.15)',
iconColor: '#f97316',
section: 'system',
path: 'admin/services/system-hub',
path: 'admin/secubox/system/system-hub/overview',
service: null,
version: '0.4.0'
},
@ -170,7 +170,7 @@ return baseclass.extend({
iconBg: 'rgba(20, 184, 166, 0.15)',
iconColor: '#14b8a6',
section: 'system',
path: 'admin/services/cdn-cache',
path: 'admin/secubox/system/cdn-cache',
service: 'squid',
version: '0.3.0'
},
@ -182,7 +182,7 @@ return baseclass.extend({
iconBg: 'rgba(161, 161, 170, 0.15)',
iconColor: '#a1a1aa',
section: 'system',
path: 'admin/system/secubox',
path: 'admin/secubox/system/settings',
service: null,
version: null
}

View File

@ -63,8 +63,27 @@ return view.extend({
var sysInfo = data[1] || {};
var self = this;
// Set portal app context
// Set portal app context and hide LuCI navigation
document.body.setAttribute('data-secubox-app', 'portal');
document.body.classList.add('secubox-portal-mode');
// Hide LuCI sidebar immediately via JS for reliability
var elementsToHide = [
'#mainmenu', '.main-left', 'header.main-header',
'nav[role="navigation"]', '#navigation', '.luci-sidebar', 'aside'
];
elementsToHide.forEach(function(selector) {
var el = document.querySelector(selector);
if (el) el.style.display = 'none';
});
// Make main content full width
var mainRight = document.querySelector('.main-right') || document.querySelector('#maincontent');
if (mainRight) {
mainRight.style.marginLeft = '0';
mainRight.style.width = '100%';
mainRight.style.maxWidth = '100%';
}
// Inject CSS
var cssLink = document.querySelector('link[href*="secubox-portal/portal.css"]');
@ -118,14 +137,20 @@ return view.extend({
]);
})
),
// Actions
// Actions - UI Switcher
E('div', { 'class': 'sb-portal-actions' }, [
E('a', {
'class': 'sb-luci-return',
'href': L.url('admin/status/overview'),
'title': 'Return to standard LuCI interface'
}, [
'\u2190 Standard LuCI'
E('div', { 'class': 'sb-ui-switcher' }, [
E('span', { 'class': 'sb-ui-label' }, 'Interface:'),
E('a', {
'class': 'sb-ui-btn active',
'href': L.url('admin/secubox/portal'),
'title': 'SecuBox Portal'
}, 'SecuBox'),
E('a', {
'class': 'sb-ui-btn',
'href': L.url('admin/status/overview'),
'title': 'Standard LuCI interface'
}, 'LuCI')
])
])
]);

View File

@ -8,11 +8,19 @@
}
},
"admin/secubox/portal": {
"title": "Portal",
"order": 10,
"title": "Dashboard",
"order": 1,
"action": {
"type": "view",
"path": "secubox-portal/index"
}
},
"admin/secubox-home": {
"title": "SecuBox Home",
"order": 0,
"action": {
"type": "alias",
"path": "admin/secubox/portal"
}
}
}