diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index 1708c3a3..ec112083 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -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, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js index ee79e4b7..1723f155 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js @@ -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; } diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js index 6416607b..d0c83aef 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/wizard.js @@ -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 diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index a887ad4e..e10f7e72 100755 --- a/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/package/secubox/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -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 ;; diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js index 47244f9a..2656fd55 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js @@ -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 }); diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js index 07204249..9be19c3d 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js @@ -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, diff --git a/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow b/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow index d261de83..1ba9af3e 100755 --- a/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow +++ b/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow @@ -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"} diff --git a/package/secubox/luci-app-media-flow/root/usr/share/rpcd/acl.d/luci-app-media-flow.json b/package/secubox/luci-app-media-flow/root/usr/share/rpcd/acl.d/luci-app-media-flow.json index 95e63c5c..3590a44d 100644 --- a/package/secubox/luci-app-media-flow/root/usr/share/rpcd/acl.d/luci-app-media-flow.json +++ b/package/secubox/luci-app-media-flow/root/usr/share/rpcd/acl.d/luci-app-media-flow.json @@ -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"] diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/header.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/header.js new file mode 100644 index 00000000..8b78a8f0 --- /dev/null +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/header.js @@ -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') + ]) + ]) + ]); + } +}); diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.css b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.css index d3d76831..d9aca7a1 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.css +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.css @@ -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; + } } diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js index 5bba9d78..df6ccb38 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js @@ -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 } diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js index c46af8a8..1f1cb71f 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js @@ -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') ]) ]) ]); diff --git a/package/secubox/luci-app-secubox-portal/root/usr/share/luci/menu.d/luci-app-secubox-portal.json b/package/secubox/luci-app-secubox-portal/root/usr/share/luci/menu.d/luci-app-secubox-portal.json index 98d1b5c0..fad02a62 100644 --- a/package/secubox/luci-app-secubox-portal/root/usr/share/luci/menu.d/luci-app-secubox-portal.json +++ b/package/secubox/luci-app-secubox-portal/root/usr/share/luci/menu.d/luci-app-secubox-portal.json @@ -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" + } } }