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:
parent
fb9722ccd6
commit
d9511420d3
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
;;
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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')
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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')
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user