feat: Add SecuBox Portal - Unified WebUI entry point (Phase 4)
New package: luci-app-secubox-portal v1.0.0 Creates unified entry point for all SecuBox applications with: Portal Features: - Top navigation bar with SecuBox branding - Section-based navigation: Dashboard, Security, Network, Monitoring, System - "Return to Standard LuCI" link for quick access to main LuCI interface - Real-time service status detection for all apps Dashboard Section: - System overview with hostname, model, uptime, memory usage - Quick stats showing running services count - Featured apps grid with quick access cards - Service status indicators (running/stopped) App Registry: - Security: CrowdSec, Client Guardian, Auth Guardian - Network: Bandwidth Manager, Traffic Shaper, WireGuard, Network Modes - Monitoring: Media Flow, nDPId, Netifyd, Netdata - System: System Hub, CDN Cache, SecuBox Settings Styling: - Full dark theme with cyber aesthetic - App cards with icon backgrounds and status dots - Responsive design for mobile devices - Smooth section transitions with animations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e7c53cec1d
commit
a9e5bc0262
20
package/secubox/luci-app-secubox-portal/Makefile
Normal file
20
package/secubox/luci-app-secubox-portal/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# luci-app-secubox-portal - Unified SecuBox WebUI Portal
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=SecuBox Portal - Unified WebUI
|
||||
LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed navigation
|
||||
LUCI_DEPENDS:=+luci-base +luci-theme-secubox
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
||||
|
||||
include ../../luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildance!
|
||||
$(eval $(call BuildPackage,luci-app-secubox-portal))
|
||||
@ -0,0 +1,573 @@
|
||||
/**
|
||||
* SecuBox Portal - Unified WebUI Styles
|
||||
* File: portal.css
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
/* Portal Container */
|
||||
.secubox-portal {
|
||||
font-family: var(--sb-font-family, 'Inter', -apple-system, BlinkMacSystemFont, sans-serif);
|
||||
min-height: 100vh;
|
||||
background: var(--cyber-bg-primary, #0a0a0f);
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
}
|
||||
|
||||
/* Top Header Bar */
|
||||
.sb-portal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
height: 60px;
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border-bottom: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sb-portal-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sb-portal-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--sb-accent-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.sb-portal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
background: var(--sb-accent-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.sb-portal-version {
|
||||
font-size: 0.75rem;
|
||||
color: var(--cyber-text-tertiary, #71717a);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Main Navigation */
|
||||
.sb-portal-nav {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.sb-portal-nav-item {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--cyber-text-secondary, #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-portal-nav-item:hover {
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
background: var(--cyber-bg-tertiary, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.sb-portal-nav-item.active {
|
||||
color: white;
|
||||
background: var(--sb-accent-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.sb-portal-nav-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Header Actions */
|
||||
.sb-portal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sb-luci-return {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
background: var(--cyber-bg-tertiary, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sb-luci-return:hover {
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
background: var(--cyber-bg-elevated, rgba(255, 255, 255, 0.08));
|
||||
border-color: var(--cyber-border-default, rgba(255, 255, 255, 0.15));
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.sb-portal-content {
|
||||
padding: 1.5rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Section Container */
|
||||
.sb-portal-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.sb-portal-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.sb-section-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.sb-section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.sb-section-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Sub-tabs for sections */
|
||||
.sb-section-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sb-section-tab {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sb-section-tab:hover {
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
background: var(--cyber-bg-tertiary, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.sb-section-tab.active {
|
||||
color: white;
|
||||
background: var(--cyber-bg-elevated, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.sb-section-tab-badge {
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
background: var(--sb-accent-primary, #667eea);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Dashboard Grid */
|
||||
.sb-dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Quick Stats */
|
||||
.sb-quick-stat {
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sb-quick-stat:hover {
|
||||
border-color: var(--cyber-border-default, rgba(255, 255, 255, 0.15));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sb-quick-stat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sb-quick-stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.sb-quick-stat-icon.security {
|
||||
background: rgba(0, 212, 170, 0.15);
|
||||
color: #00d4aa;
|
||||
}
|
||||
|
||||
.sb-quick-stat-icon.network {
|
||||
background: rgba(6, 182, 212, 0.15);
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.sb-quick-stat-icon.monitoring {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.sb-quick-stat-icon.system {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.sb-quick-stat-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sb-quick-stat-status.running {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.sb-quick-stat-status.stopped {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.sb-quick-stat-status.warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.sb-quick-stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sb-quick-stat-label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* App Cards */
|
||||
.sb-app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.sb-app-card {
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sb-app-card:hover {
|
||||
border-color: var(--sb-accent-primary, #667eea);
|
||||
background: var(--cyber-bg-elevated, rgba(255, 255, 255, 0.03));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.sb-app-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sb-app-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.sb-app-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sb-app-card-version {
|
||||
font-size: 0.75rem;
|
||||
color: var(--cyber-text-tertiary, #71717a);
|
||||
}
|
||||
|
||||
.sb-app-card-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sb-app-card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.sb-app-card-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sb-app-card-status-dot.running {
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
.sb-app-card-status-dot.stopped {
|
||||
background: #71717a;
|
||||
}
|
||||
|
||||
.sb-app-card-status-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--cyber-text-tertiary, #71717a);
|
||||
}
|
||||
|
||||
/* Recent Events */
|
||||
.sb-events-list {
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sb-events-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.sb-events-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sb-events-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-bottom: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.05));
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.sb-events-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sb-events-item:hover {
|
||||
background: var(--cyber-bg-tertiary, rgba(255, 255, 255, 0.03));
|
||||
}
|
||||
|
||||
.sb-events-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sb-events-icon.alert {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.sb-events-icon.info {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.sb-events-icon.success {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.sb-events-icon.warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.sb-events-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sb-events-message {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--cyber-text-primary, #e4e4e7);
|
||||
margin: 0 0 0.25rem 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sb-events-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--cyber-text-tertiary, #71717a);
|
||||
}
|
||||
|
||||
/* Embedded View Container */
|
||||
.sb-embedded-container {
|
||||
background: var(--cyber-bg-secondary, #141419);
|
||||
border: 1px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.sb-embedded-view {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.sb-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--cyber-text-secondary, #a1a1aa);
|
||||
}
|
||||
|
||||
.sb-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--cyber-border-subtle, rgba(255, 255, 255, 0.08));
|
||||
border-top-color: var(--sb-accent-primary, #667eea);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sb-portal-header {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sb-portal-nav {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.sb-portal-nav-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sb-portal-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sb-dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sb-app-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,300 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
/**
|
||||
* SecuBox Portal Module
|
||||
* Provides unified navigation and app management
|
||||
*/
|
||||
|
||||
return baseclass.extend({
|
||||
version: '1.0.0',
|
||||
|
||||
// SecuBox app registry
|
||||
apps: {
|
||||
// Security Apps
|
||||
'crowdsec': {
|
||||
id: 'crowdsec',
|
||||
name: 'CrowdSec Dashboard',
|
||||
desc: 'Community-driven security with real-time threat detection and crowd-sourced intelligence',
|
||||
icon: '\ud83d\udee1\ufe0f',
|
||||
iconBg: 'rgba(0, 212, 170, 0.15)',
|
||||
iconColor: '#00d4aa',
|
||||
section: 'security',
|
||||
path: 'admin/services/crowdsec-dashboard',
|
||||
service: 'crowdsec',
|
||||
version: '0.7.0'
|
||||
},
|
||||
'client-guardian': {
|
||||
id: 'client-guardian',
|
||||
name: 'Client Guardian',
|
||||
desc: 'Monitor and manage network clients with access control and parental features',
|
||||
icon: '\ud83d\udc65',
|
||||
iconBg: 'rgba(139, 92, 246, 0.15)',
|
||||
iconColor: '#8b5cf6',
|
||||
section: 'security',
|
||||
path: 'admin/services/client-guardian',
|
||||
service: 'client-guardian',
|
||||
version: '0.5.0'
|
||||
},
|
||||
'auth-guardian': {
|
||||
id: 'auth-guardian',
|
||||
name: 'Auth Guardian',
|
||||
desc: 'Two-factor authentication and access control for network services',
|
||||
icon: '\ud83d\udd10',
|
||||
iconBg: 'rgba(239, 68, 68, 0.15)',
|
||||
iconColor: '#ef4444',
|
||||
section: 'security',
|
||||
path: 'admin/services/auth-guardian',
|
||||
service: 'auth-guardian',
|
||||
version: '0.3.0'
|
||||
},
|
||||
|
||||
// Network Apps
|
||||
'bandwidth-manager': {
|
||||
id: 'bandwidth-manager',
|
||||
name: 'Bandwidth Manager',
|
||||
desc: 'Control bandwidth allocation with QoS, quotas, and traffic shaping',
|
||||
icon: '\ud83d\udcc8',
|
||||
iconBg: 'rgba(59, 130, 246, 0.15)',
|
||||
iconColor: '#3b82f6',
|
||||
section: 'network',
|
||||
path: 'admin/services/bandwidth-manager',
|
||||
service: 'bandwidth-manager',
|
||||
version: '0.5.0'
|
||||
},
|
||||
'traffic-shaper': {
|
||||
id: 'traffic-shaper',
|
||||
name: 'Traffic Shaper',
|
||||
desc: 'Advanced traffic shaping with SQM and cake-based queue management',
|
||||
icon: '\ud83c\udf0a',
|
||||
iconBg: 'rgba(20, 184, 166, 0.15)',
|
||||
iconColor: '#14b8a6',
|
||||
section: 'network',
|
||||
path: 'admin/network/sqm',
|
||||
service: null,
|
||||
version: null
|
||||
},
|
||||
'wireguard': {
|
||||
id: 'wireguard',
|
||||
name: 'WireGuard VPN',
|
||||
desc: 'Modern, fast, and secure VPN tunnel management',
|
||||
icon: '\ud83d\udd12',
|
||||
iconBg: 'rgba(239, 68, 68, 0.15)',
|
||||
iconColor: '#ef4444',
|
||||
section: 'network',
|
||||
path: 'admin/network/wireguard',
|
||||
service: 'wgserver',
|
||||
version: null
|
||||
},
|
||||
'network-modes': {
|
||||
id: 'network-modes',
|
||||
name: 'Network Modes',
|
||||
desc: 'Configure router, AP, or bridge mode with one-click setup',
|
||||
icon: '\ud83c\udf10',
|
||||
iconBg: 'rgba(102, 126, 234, 0.15)',
|
||||
iconColor: '#667eea',
|
||||
section: 'network',
|
||||
path: 'admin/services/network-modes',
|
||||
service: null,
|
||||
version: '0.2.0'
|
||||
},
|
||||
|
||||
// Monitoring Apps
|
||||
'media-flow': {
|
||||
id: 'media-flow',
|
||||
name: 'Media Flow',
|
||||
desc: 'Monitor streaming services and media traffic in real-time',
|
||||
icon: '\ud83c\udfac',
|
||||
iconBg: 'rgba(236, 72, 153, 0.15)',
|
||||
iconColor: '#ec4899',
|
||||
section: 'monitoring',
|
||||
path: 'admin/services/media-flow',
|
||||
service: 'media-flow',
|
||||
version: '0.6.0'
|
||||
},
|
||||
'ndpid': {
|
||||
id: 'ndpid',
|
||||
name: 'nDPId Flows',
|
||||
desc: 'Deep packet inspection with application detection and flow analysis',
|
||||
icon: '\ud83d\udd0d',
|
||||
iconBg: 'rgba(6, 182, 212, 0.15)',
|
||||
iconColor: '#06b6d4',
|
||||
section: 'monitoring',
|
||||
path: 'admin/services/ndpid',
|
||||
service: 'ndpid',
|
||||
version: '1.1.0'
|
||||
},
|
||||
'netifyd': {
|
||||
id: 'netifyd',
|
||||
name: 'Netifyd',
|
||||
desc: 'Network intelligence agent for traffic classification',
|
||||
icon: '\ud83d\udce1',
|
||||
iconBg: 'rgba(6, 182, 212, 0.15)',
|
||||
iconColor: '#06b6d4',
|
||||
section: 'monitoring',
|
||||
path: 'admin/services/secubox-netifyd',
|
||||
service: 'netifyd',
|
||||
version: '1.2.0'
|
||||
},
|
||||
'netdata': {
|
||||
id: 'netdata',
|
||||
name: 'Netdata Dashboard',
|
||||
desc: 'Real-time system and network performance monitoring',
|
||||
icon: '\ud83d\udcca',
|
||||
iconBg: 'rgba(34, 197, 94, 0.15)',
|
||||
iconColor: '#22c55e',
|
||||
section: 'monitoring',
|
||||
path: 'admin/services/netdata-dashboard',
|
||||
service: 'netdata',
|
||||
version: '0.4.0'
|
||||
},
|
||||
|
||||
// System Apps
|
||||
'system-hub': {
|
||||
id: 'system-hub',
|
||||
name: 'System Hub',
|
||||
desc: 'Centralized system administration and configuration',
|
||||
icon: '\u2699\ufe0f',
|
||||
iconBg: 'rgba(249, 115, 22, 0.15)',
|
||||
iconColor: '#f97316',
|
||||
section: 'system',
|
||||
path: 'admin/services/system-hub',
|
||||
service: null,
|
||||
version: '0.4.0'
|
||||
},
|
||||
'cdn-cache': {
|
||||
id: 'cdn-cache',
|
||||
name: 'CDN Cache',
|
||||
desc: 'Local content caching for faster repeated downloads',
|
||||
icon: '\ud83d\udce6',
|
||||
iconBg: 'rgba(20, 184, 166, 0.15)',
|
||||
iconColor: '#14b8a6',
|
||||
section: 'system',
|
||||
path: 'admin/services/cdn-cache',
|
||||
service: 'squid',
|
||||
version: '0.3.0'
|
||||
},
|
||||
'secubox-settings': {
|
||||
id: 'secubox-settings',
|
||||
name: 'SecuBox Settings',
|
||||
desc: 'Global SecuBox configuration and preferences',
|
||||
icon: '\ud83d\udd27',
|
||||
iconBg: 'rgba(161, 161, 170, 0.15)',
|
||||
iconColor: '#a1a1aa',
|
||||
section: 'system',
|
||||
path: 'admin/system/secubox',
|
||||
service: null,
|
||||
version: null
|
||||
}
|
||||
},
|
||||
|
||||
// Section definitions
|
||||
sections: {
|
||||
'dashboard': {
|
||||
id: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
icon: '\ud83c\udfe0',
|
||||
order: 1
|
||||
},
|
||||
'security': {
|
||||
id: 'security',
|
||||
name: 'Security',
|
||||
icon: '\ud83d\udee1\ufe0f',
|
||||
order: 2
|
||||
},
|
||||
'network': {
|
||||
id: 'network',
|
||||
name: 'Network',
|
||||
icon: '\ud83c\udf10',
|
||||
order: 3
|
||||
},
|
||||
'monitoring': {
|
||||
id: 'monitoring',
|
||||
name: 'Monitoring',
|
||||
icon: '\ud83d\udcca',
|
||||
order: 4
|
||||
},
|
||||
'system': {
|
||||
id: 'system',
|
||||
name: 'System',
|
||||
icon: '\u2699\ufe0f',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get apps by section
|
||||
*/
|
||||
getAppsBySection: function(sectionId) {
|
||||
var self = this;
|
||||
var apps = [];
|
||||
Object.keys(this.apps).forEach(function(key) {
|
||||
if (self.apps[key].section === sectionId) {
|
||||
apps.push(self.apps[key]);
|
||||
}
|
||||
});
|
||||
return apps;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all sections
|
||||
*/
|
||||
getSections: function() {
|
||||
var self = this;
|
||||
return Object.keys(this.sections)
|
||||
.map(function(key) { return self.sections[key]; })
|
||||
.sort(function(a, b) { return a.order - b.order; });
|
||||
},
|
||||
|
||||
/**
|
||||
* Build LuCI URL
|
||||
*/
|
||||
buildUrl: function(path) {
|
||||
return L.url(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if service is running
|
||||
*/
|
||||
checkServiceStatus: function(serviceName) {
|
||||
if (!serviceName) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return L.resolveDefault(
|
||||
fs.exec('/etc/init.d/' + serviceName, ['status']),
|
||||
{ code: 1 }
|
||||
).then(function(res) {
|
||||
return res.code === 0 ? 'running' : 'stopped';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all app statuses
|
||||
*/
|
||||
getAllAppStatuses: function() {
|
||||
var self = this;
|
||||
var promises = [];
|
||||
var appKeys = Object.keys(this.apps);
|
||||
|
||||
appKeys.forEach(function(key) {
|
||||
var app = self.apps[key];
|
||||
if (app.service) {
|
||||
promises.push(
|
||||
self.checkServiceStatus(app.service).then(function(status) {
|
||||
return { id: key, status: status };
|
||||
})
|
||||
);
|
||||
} else {
|
||||
promises.push(Promise.resolve({ id: key, status: null }));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function(results) {
|
||||
var statuses = {};
|
||||
results.forEach(function(r) {
|
||||
statuses[r.id] = r.status;
|
||||
});
|
||||
return statuses;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,371 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require secubox-portal/portal as portal';
|
||||
|
||||
var callSystemBoard = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'board'
|
||||
});
|
||||
|
||||
var callSystemInfo = rpc.declare({
|
||||
object: 'system',
|
||||
method: 'info'
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
currentSection: 'dashboard',
|
||||
appStatuses: {},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callSystemBoard(),
|
||||
callSystemInfo(),
|
||||
this.loadAppStatuses()
|
||||
]);
|
||||
},
|
||||
|
||||
loadAppStatuses: function() {
|
||||
var self = this;
|
||||
var apps = portal.apps;
|
||||
var promises = [];
|
||||
|
||||
Object.keys(apps).forEach(function(key) {
|
||||
var app = apps[key];
|
||||
if (app.service) {
|
||||
promises.push(
|
||||
fs.exec('/etc/init.d/' + app.service, ['status'])
|
||||
.then(function(res) {
|
||||
return { id: key, status: (res && res.code === 0) ? 'running' : 'stopped' };
|
||||
})
|
||||
.catch(function() {
|
||||
return { id: key, status: 'stopped' };
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function(results) {
|
||||
results.forEach(function(r) {
|
||||
self.appStatuses[r.id] = r.status;
|
||||
});
|
||||
return self.appStatuses;
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var boardInfo = data[0] || {};
|
||||
var sysInfo = data[1] || {};
|
||||
var self = this;
|
||||
|
||||
// Set portal app context
|
||||
document.body.setAttribute('data-secubox-app', 'portal');
|
||||
|
||||
// Inject CSS
|
||||
var cssLink = document.querySelector('link[href*="secubox-portal/portal.css"]');
|
||||
if (!cssLink) {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = L.resource('secubox-portal/portal.css');
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
var container = E('div', { 'class': 'secubox-portal' }, [
|
||||
// Header
|
||||
this.renderHeader(),
|
||||
// Content
|
||||
E('div', { 'class': 'sb-portal-content' }, [
|
||||
this.renderDashboardSection(boardInfo, sysInfo),
|
||||
this.renderSecuritySection(),
|
||||
this.renderNetworkSection(),
|
||||
this.renderMonitoringSection(),
|
||||
this.renderSystemSection()
|
||||
])
|
||||
]);
|
||||
|
||||
// Set initial active section
|
||||
this.switchSection('dashboard');
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
renderHeader: function() {
|
||||
var self = this;
|
||||
var sections = portal.getSections();
|
||||
|
||||
return E('div', { 'class': 'sb-portal-header' }, [
|
||||
// Brand
|
||||
E('div', { 'class': 'sb-portal-brand' }, [
|
||||
E('div', { 'class': 'sb-portal-logo' }, 'S'),
|
||||
E('span', { 'class': 'sb-portal-title' }, 'SecuBox'),
|
||||
E('span', { 'class': 'sb-portal-version' }, 'v0.14.0')
|
||||
]),
|
||||
// Navigation
|
||||
E('nav', { 'class': 'sb-portal-nav' },
|
||||
sections.map(function(section) {
|
||||
return E('button', {
|
||||
'class': 'sb-portal-nav-item' + (section.id === 'dashboard' ? ' active' : ''),
|
||||
'data-section': section.id,
|
||||
'click': function() { self.switchSection(section.id); }
|
||||
}, [
|
||||
E('span', { 'class': 'sb-portal-nav-icon' }, section.icon),
|
||||
section.name
|
||||
]);
|
||||
})
|
||||
),
|
||||
// Actions
|
||||
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'
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
switchSection: function(sectionId) {
|
||||
this.currentSection = sectionId;
|
||||
|
||||
// Update nav active state
|
||||
document.querySelectorAll('.sb-portal-nav-item').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.dataset.section === sectionId);
|
||||
});
|
||||
|
||||
// Update section visibility
|
||||
document.querySelectorAll('.sb-portal-section').forEach(function(section) {
|
||||
section.classList.toggle('active', section.dataset.section === sectionId);
|
||||
});
|
||||
},
|
||||
|
||||
renderDashboardSection: function(boardInfo, sysInfo) {
|
||||
var self = this;
|
||||
var securityApps = portal.getAppsBySection('security');
|
||||
var networkApps = portal.getAppsBySection('network');
|
||||
var monitoringApps = portal.getAppsBySection('monitoring');
|
||||
|
||||
// Count running services
|
||||
var runningCount = Object.values(this.appStatuses).filter(function(s) { return s === 'running'; }).length;
|
||||
var totalServices = Object.keys(this.appStatuses).length;
|
||||
|
||||
return E('div', { 'class': 'sb-portal-section active', 'data-section': 'dashboard' }, [
|
||||
E('div', { 'class': 'sb-section-header' }, [
|
||||
E('h2', { 'class': 'sb-section-title' }, 'SecuBox Dashboard'),
|
||||
E('p', { 'class': 'sb-section-subtitle' },
|
||||
'Welcome to SecuBox - Your unified network security and management platform')
|
||||
]),
|
||||
|
||||
// Quick Stats
|
||||
E('div', { 'class': 'sb-dashboard-grid' }, [
|
||||
// System Status
|
||||
E('div', { 'class': 'sb-quick-stat' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-header' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-icon system' }, '\ud83d\udda5\ufe0f'),
|
||||
E('span', { 'class': 'sb-quick-stat-status running' }, 'Online')
|
||||
]),
|
||||
E('div', { 'class': 'sb-quick-stat-value' }, boardInfo.hostname || 'SecuBox'),
|
||||
E('div', { 'class': 'sb-quick-stat-label' }, boardInfo.model || 'Router')
|
||||
]),
|
||||
|
||||
// Services Status
|
||||
E('div', { 'class': 'sb-quick-stat' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-header' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-icon monitoring' }, '\u2699\ufe0f'),
|
||||
E('span', { 'class': 'sb-quick-stat-status ' + (runningCount > 0 ? 'running' : 'warning') },
|
||||
runningCount > 0 ? 'Active' : 'Idle')
|
||||
]),
|
||||
E('div', { 'class': 'sb-quick-stat-value' }, runningCount + '/' + totalServices),
|
||||
E('div', { 'class': 'sb-quick-stat-label' }, 'Services Running')
|
||||
]),
|
||||
|
||||
// Security Apps
|
||||
E('div', { 'class': 'sb-quick-stat' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-header' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-icon security' }, '\ud83d\udee1\ufe0f'),
|
||||
E('span', { 'class': 'sb-quick-stat-status running' }, 'Protected')
|
||||
]),
|
||||
E('div', { 'class': 'sb-quick-stat-value' }, securityApps.length),
|
||||
E('div', { 'class': 'sb-quick-stat-label' }, 'Security Modules')
|
||||
]),
|
||||
|
||||
// Network Apps
|
||||
E('div', { 'class': 'sb-quick-stat' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-header' }, [
|
||||
E('div', { 'class': 'sb-quick-stat-icon network' }, '\ud83c\udf10'),
|
||||
E('span', { 'class': 'sb-quick-stat-status running' }, 'Configured')
|
||||
]),
|
||||
E('div', { 'class': 'sb-quick-stat-value' }, networkApps.length),
|
||||
E('div', { 'class': 'sb-quick-stat-label' }, 'Network Tools')
|
||||
])
|
||||
]),
|
||||
|
||||
// Featured Apps
|
||||
E('h3', { 'style': 'margin: 1.5rem 0 1rem; color: var(--cyber-text-primary);' }, 'Quick Access'),
|
||||
E('div', { 'class': 'sb-app-grid' },
|
||||
this.renderFeaturedApps(['crowdsec', 'bandwidth-manager', 'media-flow', 'ndpid'])
|
||||
),
|
||||
|
||||
// Recent Events placeholder
|
||||
E('div', { 'class': 'sb-events-list', 'style': 'margin-top: 1.5rem;' }, [
|
||||
E('div', { 'class': 'sb-events-header' }, [
|
||||
E('h4', { 'class': 'sb-events-title' }, 'System Overview')
|
||||
]),
|
||||
E('div', { 'class': 'sb-events-item' }, [
|
||||
E('div', { 'class': 'sb-events-icon info' }, '\u2139\ufe0f'),
|
||||
E('div', { 'class': 'sb-events-content' }, [
|
||||
E('p', { 'class': 'sb-events-message' },
|
||||
'System: ' + (boardInfo.system || 'Unknown') + ' | Kernel: ' + (boardInfo.kernel || 'Unknown')),
|
||||
E('span', { 'class': 'sb-events-meta' }, 'System Information')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sb-events-item' }, [
|
||||
E('div', { 'class': 'sb-events-icon success' }, '\u2705'),
|
||||
E('div', { 'class': 'sb-events-content' }, [
|
||||
E('p', { 'class': 'sb-events-message' },
|
||||
'Uptime: ' + this.formatUptime(sysInfo.uptime || 0)),
|
||||
E('span', { 'class': 'sb-events-meta' }, 'System Status')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sb-events-item' }, [
|
||||
E('div', { 'class': 'sb-events-icon info' }, '\ud83d\udcbe'),
|
||||
E('div', { 'class': 'sb-events-content' }, [
|
||||
E('p', { 'class': 'sb-events-message' },
|
||||
'Memory: ' + this.formatBytes(sysInfo.memory ? sysInfo.memory.total - sysInfo.memory.free : 0) +
|
||||
' / ' + this.formatBytes(sysInfo.memory ? sysInfo.memory.total : 0) + ' used'),
|
||||
E('span', { 'class': 'sb-events-meta' }, 'Resource Usage')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderFeaturedApps: function(appIds) {
|
||||
var self = this;
|
||||
return appIds.map(function(id) {
|
||||
var app = portal.apps[id];
|
||||
if (!app) return null;
|
||||
|
||||
var status = self.appStatuses[id];
|
||||
|
||||
return E('a', {
|
||||
'class': 'sb-app-card',
|
||||
'href': L.url(app.path)
|
||||
}, [
|
||||
E('div', { 'class': 'sb-app-card-header' }, [
|
||||
E('div', {
|
||||
'class': 'sb-app-card-icon',
|
||||
'style': 'background: ' + app.iconBg + '; color: ' + app.iconColor + ';'
|
||||
}, app.icon),
|
||||
E('div', {}, [
|
||||
E('h4', { 'class': 'sb-app-card-title' }, app.name),
|
||||
app.version ? E('span', { 'class': 'sb-app-card-version' }, 'v' + app.version) : null
|
||||
])
|
||||
]),
|
||||
E('p', { 'class': 'sb-app-card-desc' }, app.desc),
|
||||
status ? E('div', { 'class': 'sb-app-card-status' }, [
|
||||
E('span', { 'class': 'sb-app-card-status-dot ' + status }),
|
||||
E('span', { 'class': 'sb-app-card-status-text' },
|
||||
status === 'running' ? 'Service running' : 'Service stopped')
|
||||
]) : null
|
||||
]);
|
||||
}).filter(function(el) { return el !== null; });
|
||||
},
|
||||
|
||||
renderSecuritySection: function() {
|
||||
var apps = portal.getAppsBySection('security');
|
||||
return this.renderAppSection('security', 'Security',
|
||||
'Protect your network with advanced security tools', apps);
|
||||
},
|
||||
|
||||
renderNetworkSection: function() {
|
||||
var apps = portal.getAppsBySection('network');
|
||||
return this.renderAppSection('network', 'Network',
|
||||
'Configure and optimize your network connections', apps);
|
||||
},
|
||||
|
||||
renderMonitoringSection: function() {
|
||||
var apps = portal.getAppsBySection('monitoring');
|
||||
return this.renderAppSection('monitoring', 'Monitoring',
|
||||
'Monitor traffic, applications, and system performance', apps);
|
||||
},
|
||||
|
||||
renderSystemSection: function() {
|
||||
var apps = portal.getAppsBySection('system');
|
||||
return this.renderAppSection('system', 'System',
|
||||
'System administration and configuration tools', apps);
|
||||
},
|
||||
|
||||
renderAppSection: function(sectionId, title, subtitle, apps) {
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'sb-portal-section', 'data-section': sectionId }, [
|
||||
E('div', { 'class': 'sb-section-header' }, [
|
||||
E('h2', { 'class': 'sb-section-title' }, title),
|
||||
E('p', { 'class': 'sb-section-subtitle' }, subtitle)
|
||||
]),
|
||||
E('div', { 'class': 'sb-app-grid' },
|
||||
apps.map(function(app) {
|
||||
var status = self.appStatuses[app.id];
|
||||
|
||||
return E('a', {
|
||||
'class': 'sb-app-card',
|
||||
'href': L.url(app.path)
|
||||
}, [
|
||||
E('div', { 'class': 'sb-app-card-header' }, [
|
||||
E('div', {
|
||||
'class': 'sb-app-card-icon',
|
||||
'style': 'background: ' + app.iconBg + '; color: ' + app.iconColor + ';'
|
||||
}, app.icon),
|
||||
E('div', {}, [
|
||||
E('h4', { 'class': 'sb-app-card-title' }, app.name),
|
||||
app.version ? E('span', { 'class': 'sb-app-card-version' }, 'v' + app.version) : null
|
||||
])
|
||||
]),
|
||||
E('p', { 'class': 'sb-app-card-desc' }, app.desc),
|
||||
status ? E('div', { 'class': 'sb-app-card-status' }, [
|
||||
E('span', { 'class': 'sb-app-card-status-dot ' + status }),
|
||||
E('span', { 'class': 'sb-app-card-status-text' },
|
||||
status === 'running' ? 'Service running' : 'Service stopped')
|
||||
]) : null
|
||||
]);
|
||||
})
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
formatUptime: function(seconds) {
|
||||
if (!seconds) return 'Unknown';
|
||||
var days = Math.floor(seconds / 86400);
|
||||
var hours = Math.floor((seconds % 86400) / 3600);
|
||||
var mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
var parts = [];
|
||||
if (days > 0) parts.push(days + 'd');
|
||||
if (hours > 0) parts.push(hours + 'h');
|
||||
if (mins > 0) parts.push(mins + 'm');
|
||||
|
||||
return parts.join(' ') || '< 1m';
|
||||
},
|
||||
|
||||
formatBytes: function(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = 0;
|
||||
while (bytes >= 1024 && i < units.length - 1) {
|
||||
bytes /= 1024;
|
||||
i++;
|
||||
}
|
||||
return bytes.toFixed(1) + ' ' + units[i];
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
{
|
||||
"admin/secubox": {
|
||||
"title": "SecuBox",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/secubox/portal"
|
||||
}
|
||||
},
|
||||
"admin/secubox/portal": {
|
||||
"title": "Portal",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "secubox-portal/index"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user