From 3ac0a31ceaf71f21f43084b9ed6afc8e779099e8 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 11 Feb 2026 10:07:22 +0100 Subject: [PATCH] feat(portal): C3BOX async progressive rendering with public ACL - Async progressive cache: instant render from localStorage, async RPC updates - Public ACL: unauthenticated access for secubox-public/portal route - Progressive DOM updates via updateText() helpers - No blocking Promise.all - each fetch updates its section on completion Co-Authored-By: Claude Opus 4.5 --- .../resources/view/secubox-portal/index.js | 1536 ++++++++++++----- .../luci/menu.d/luci-app-secubox-portal.json | 11 +- .../rpcd/acl.d/luci-app-secubox-portal.json | 27 +- 3 files changed, 1107 insertions(+), 467 deletions(-) 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 439fe374..7ca30bf7 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 @@ -8,154 +8,668 @@ 'require ui'; /** - * SecuBox Portal - KISS Edition - * Simple, categorized service dashboard + * SecuBox C3BOX Portal - Generative KISS Theme System + * + * Architecture: + * - CSS Variables for theming (supports light/dark/custom) + * - Component functions return DOM elements (generative-friendly) + * - Multi-screen navigation with sidebar + * - Dual-mode: LuCI addon or standalone sandbox + * - Double-buffered cache for instant rendering + * + * Theme tokens: --bg, --card, --line, --text, --muted, --green, --red, --blue, --cyan */ -var callSystemBoard = rpc.declare({ object: 'system', method: 'board' }); -var callSystemInfo = rpc.declare({ object: 'system', method: 'info' }); +// ========== ASYNC PROGRESSIVE CACHE SYSTEM ========== +var C3_CACHE = { + STORAGE_KEY: 'c3box_cache', + data: null, -var callGetServices = rpc.declare({ - object: 'luci.secubox', - method: 'get_services', - expect: { services: [] } -}); + // Load from localStorage instantly + init: function() { + try { + var stored = localStorage.getItem(this.STORAGE_KEY); + if (stored) { + this.data = JSON.parse(stored); + return true; + } + } catch (e) {} + this.data = this.getDefaults(); + return false; + }, -var callDashboardData = rpc.declare({ - object: 'luci.secubox', - method: 'get_dashboard_data', - expect: { counts: {} } -}); + // Get cached value or default + get: function(key) { + return (this.data && this.data[key]) || this.getDefaults()[key]; + }, -// Service Categories - KISS mapping -var SERVICE_CATEGORIES = { - 'web': { - name: 'Web/Proxy', - icon: '🌐', - color: '#3b82f6', - services: ['haproxy', 'nginx', 'uhttpd', 'squid', 'cdn-cache', 'vhost-manager'] + // Update single key and persist + set: function(key, value) { + if (!this.data) this.data = this.getDefaults(); + this.data[key] = value; + this.data._ts = Date.now(); + try { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.data)); + } catch (e) {} }, - 'security': { - name: 'Security', - icon: '🛡️', - color: '#ef4444', - services: ['crowdsec', 'crowdsec-firewall-bouncer', 'firewall', 'adguardhome', 'mitmproxy', 'tor', 'tor-shield'] - }, - 'ai': { - name: 'AI/ML', - icon: '🤖', - color: '#8b5cf6', - services: ['localai', 'ollama', 'streamlit'] - }, - 'containers': { - name: 'Containers', - icon: '📦', - color: '#06b6d4', - services: ['lxc', 'docker', 'podman', 'containerd'] - }, - 'media': { - name: 'Media', - icon: '🎵', - color: '#ec4899', - services: ['jellyfin', 'lyrion', 'plex', 'emby'] - }, - 'apps': { - name: 'Applications', - icon: '⚡', - color: '#22c55e', - services: [] // Catch-all for running services with ports + + // Get default/fallback data structure + getDefaults: function() { + return { + boardInfo: { hostname: 'SecuBox', model: 'C3BOX' }, + sysInfo: { uptime: 0, load: [0, 0, 0], memory: { total: 8000000000, free: 4000000000 } }, + services: [], + health: {}, + modules: { modules: [] }, + initServices: {}, + _ts: 0 + }; } }; +// ========== DOM UPDATE HELPERS ========== +function updateText(selector, text) { + var el = document.querySelector(selector); + if (el && el.textContent !== text) el.textContent = text; +} + +function updateAttr(selector, attr, value) { + var el = document.querySelector(selector); + if (el) el.setAttribute(attr, value); +} + +// ========== RPC DECLARATIONS ========== +var callSystemBoard = rpc.declare({ object: 'system', method: 'board' }); +var callSystemInfo = rpc.declare({ object: 'system', method: 'info' }); +var callGetServices = rpc.declare({ object: 'luci.secubox', method: 'get_services', expect: {} }); +var callGetHealth = rpc.declare({ object: 'luci.system-hub', method: 'get_health', expect: {} }); +var callGetModules = rpc.declare({ object: 'luci.secubox', method: 'getModules', expect: {} }); + +// ========== THEME SYSTEM ========== +var C3_THEME = { + // Core palette + bg: '#0a0e17', + bg2: '#111827', + card: '#161e2e', + cardHover: '#1c2640', + line: '#1e293b', + text: '#e2e8f0', + muted: '#94a3b8', + // Accent colors + green: '#00C853', + greenGlow: '#00E676', + red: '#FF1744', + blue: '#2979FF', + blueGlow: '#448AFF', + cyan: '#22d3ee', + purple: '#a78bfa', + orange: '#fb923c', + pink: '#f472b6', + yellow: '#fbbf24', + dark: '#37474F', + // Layout + sidebarW: '240px', + topbarH: '56px', + // Fonts (fallback-safe) + fontDisplay: "'Orbitron', 'Segoe UI', sans-serif", + fontMono: "'JetBrains Mono', 'Consolas', monospace", + fontBody: "'Outfit', 'Segoe UI', sans-serif" +}; + +// ========== NAVIGATION CONFIG ========== +var NAV_ITEMS = [ + { id: 'hero', icon: '\uD83C\uDFE0', label: 'Home', section: 'Main' }, + { id: 'dashboard', icon: '\uD83D\uDCCA', label: 'System Hub', section: 'Main' }, + { id: 'security', icon: '\uD83D\uDEE1\uFE0F', label: 'DNS Guard', section: 'Security' }, + { id: 'services', icon: '\uD83D\uDCE6', label: 'Services', section: 'Security' }, + { id: 'cloning', icon: '\uD83D\uDCE5', label: 'Cloning Station', section: 'Deploy' }, + { id: 'mesh', icon: '\uD83D\uDCE1', label: 'Mesh MaaS', section: 'Deploy' } +]; + +var SCREEN_TITLES = { + hero: '\uD83C\uDFE0 SecuBox Home', + dashboard: '\uD83D\uDCCA System Control Center', + security: '\uD83D\uDEE1\uFE0F DNS Guard', + services: '\uD83D\uDCE6 Services Stack', + cloning: '\uD83D\uDCE5 Cloning Station', + mesh: '\uD83D\uDCE1 Mesh Federation' +}; + +// ========== CSS GENERATOR ========== +function generateCSS(theme) { + var t = theme || C3_THEME; + return ` +/* C3BOX Portal - Generated Theme */ +:root { + --bg: ${t.bg}; --bg2: ${t.bg2}; --card: ${t.card}; --card-hover: ${t.cardHover}; + --line: ${t.line}; --text: ${t.text}; --muted: ${t.muted}; + --green: ${t.green}; --green-glow: ${t.greenGlow}; --red: ${t.red}; + --blue: ${t.blue}; --blue-glow: ${t.blueGlow}; --cyan: ${t.cyan}; + --purple: ${t.purple}; --orange: ${t.orange}; --pink: ${t.pink}; + --yellow: ${t.yellow}; --dark: ${t.dark}; + --sidebar-w: ${t.sidebarW}; --topbar-h: ${t.topbarH}; + --font-display: ${t.fontDisplay}; --font-mono: ${t.fontMono}; --font-body: ${t.fontBody}; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +.c3-root { + background: var(--bg); color: var(--text); font-family: var(--font-body); + min-height: 100vh; overflow-x: hidden; +} +/* Noise overlay */ +.c3-root::before { + content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 9999; + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E"); +} +/* Sidebar */ +.c3-sidebar { + position: fixed; left: 0; top: 0; bottom: 0; width: var(--sidebar-w); + background: linear-gradient(180deg, #0d1321 0%, var(--bg) 100%); + border-right: 1px solid var(--line); z-index: 100; + display: flex; flex-direction: column; transition: transform 0.3s ease; +} +.c3-sidebar-logo { + padding: 20px 20px 16px; border-bottom: 1px solid var(--line); text-align: center; +} +.c3-logo-text { + font-family: var(--font-display); font-weight: 900; font-size: 28px; letter-spacing: -1px; +} +.c3-logo-c { color: var(--green); } +.c3-logo-3 { color: var(--red); font-size: 16px; vertical-align: super; position: relative; top: -6px; } +.c3-logo-b { color: var(--blue); } +.c3-logo-o { color: var(--dark); } +.c3-logo-x { color: var(--green); } +.c3-logo-sub { + font-family: var(--font-mono); font-size: 10px; color: var(--muted); + letter-spacing: 2px; margin-top: 4px; +} +.c3-nav { flex: 1; padding: 12px 0; overflow-y: auto; } +.c3-nav-section { + padding: 8px 16px 4px; font-size: 10px; letter-spacing: 2px; + text-transform: uppercase; color: var(--muted); font-family: var(--font-mono); +} +.c3-nav-item { + display: flex; align-items: center; gap: 12px; padding: 10px 20px; + cursor: pointer; font-size: 14px; font-weight: 500; color: var(--muted); + transition: all 0.2s ease; border-left: 3px solid transparent; +} +.c3-nav-item:hover { background: rgba(255,255,255,0.03); color: var(--text); } +.c3-nav-item.active { + color: var(--green); background: rgba(0,200,83,0.05); border-left-color: var(--green); +} +.c3-nav-icon { font-size: 18px; width: 24px; text-align: center; } +.c3-sidebar-footer { + padding: 16px; border-top: 1px solid var(--line); text-align: center; +} +.c3-morph { + font-family: var(--font-display); font-size: 13px; font-weight: 700; + letter-spacing: 2px; min-height: 20px; +} +.c3-morph-secu { + background: linear-gradient(90deg, var(--green), var(--blue), var(--green)); + background-size: 200%; -webkit-background-clip: text; background-clip: text; + -webkit-text-fill-color: transparent; animation: gradShift 3s ease-in-out infinite; +} +.c3-footer-ver { font-size: 10px; color: var(--muted); font-family: var(--font-mono); margin-top: 4px; } +/* Topbar */ +.c3-topbar { + position: fixed; top: 0; left: var(--sidebar-w); right: 0; height: var(--topbar-h); + background: rgba(10,14,23,0.85); backdrop-filter: blur(12px); + border-bottom: 1px solid var(--line); z-index: 90; + display: flex; align-items: center; justify-content: space-between; padding: 0 24px; +} +.c3-topbar-left { display: flex; align-items: center; gap: 12px; } +.c3-topbar-title { font-family: var(--font-body); font-weight: 700; font-size: 18px; } +.c3-topbar-right { display: flex; align-items: center; gap: 16px; } +.c3-badge { + font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; + padding: 4px 10px; border-radius: 4px; +} +.c3-badge-green { color: var(--green); background: rgba(0,200,83,0.1); border: 1px solid rgba(0,200,83,0.25); } +.c3-badge-red { color: var(--red); background: rgba(255,23,68,0.1); border: 1px solid rgba(255,23,68,0.25); animation: pulse 2s infinite; } +.c3-badge-blue { color: var(--blue); background: rgba(41,121,255,0.1); border: 1px solid rgba(41,121,255,0.25); } +.c3-hamburger { display: none; cursor: pointer; font-size: 22px; color: var(--text); background: none; border: none; } +/* Main content */ +.c3-main { + margin-left: var(--sidebar-w); margin-top: var(--topbar-h); + padding: 28px; min-height: calc(100vh - var(--topbar-h)); +} +.c3-screen { display: none; animation: fadeIn 0.4s ease; } +.c3-screen.active { display: block; } +/* Hero screen */ +.c3-hero { + display: flex; flex-direction: column; align-items: center; justify-content: center; + min-height: calc(100vh - var(--topbar-h) - 56px); text-align: center; +} +.c3-cube-wrap { perspective: 800px; width: 200px; height: 200px; margin-bottom: 40px; } +.c3-cube { + width: 100%; height: 100%; position: relative; + transform-style: preserve-3d; animation: cubeRotate 18s ease-in-out infinite; +} +.c3-cube-face { + position: absolute; width: 200px; height: 200px; + display: flex; align-items: center; justify-content: center; + border: 1px solid rgba(0,200,83,0.15); background: rgba(6,10,18,0.88); + backdrop-filter: blur(4px); +} +.face-front { transform: translateZ(100px); } +.face-back { transform: rotateY(180deg) translateZ(100px); } +.face-right { transform: rotateY(90deg) translateZ(100px); } +.face-left { transform: rotateY(-90deg) translateZ(100px); } +.face-top { transform: rotateX(90deg) translateZ(100px); } +.face-bottom { transform: rotateX(-90deg) translateZ(100px); } +.c3-cube-logo { font-family: var(--font-display); font-weight: 900; font-size: 42px; } +.c3-cube-text { font-family: var(--font-display); font-weight: 700; opacity: 0.15; } +.c3-hero-title { + font-family: var(--font-display); font-weight: 900; font-size: clamp(32px, 5vw, 56px); + letter-spacing: -2px; margin-bottom: 12px; + background: linear-gradient(135deg, var(--green), var(--cyan), var(--blue)); + -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; +} +.c3-hero-sub { font-size: 18px; color: var(--muted); font-weight: 300; max-width: 550px; margin: 0 auto 8px; } +.c3-hero-tagline { font-family: var(--font-mono); font-size: 14px; color: var(--muted); margin-top: 12px; } +.c3-hero-tagline em { color: var(--red); font-style: normal; } +.c3-hero-stats { display: flex; gap: 24px; margin-top: 36px; flex-wrap: wrap; justify-content: center; } +.c3-hero-stat { + background: var(--card); border: 1px solid var(--line); border-radius: 12px; + padding: 18px 28px; text-align: center; transition: all 0.3s; +} +.c3-hero-stat:hover { border-color: rgba(0,200,83,0.3); transform: translateY(-3px); } +.c3-hero-stat-val { font-family: var(--font-display); font-weight: 900; font-size: 28px; color: var(--green); } +.c3-hero-stat-label { + font-size: 11px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; + margin-top: 4px; font-family: var(--font-mono); +} +/* Dashboard cards */ +.c3-dash-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; } +.c3-dash-card { + background: var(--card); border: 1px solid var(--line); border-radius: 14px; + padding: 20px; display: flex; align-items: center; gap: 16px; transition: all 0.3s; +} +.c3-dash-card:hover { border-color: rgba(0,200,83,0.2); transform: translateY(-2px); } +.c3-dash-card-icon { + width: 48px; height: 48px; border-radius: 12px; + display: flex; align-items: center; justify-content: center; font-size: 22px; +} +.c3-dash-card-val { font-family: var(--font-display); font-weight: 700; font-size: 22px; } +.c3-dash-card-label { font-size: 12px; color: var(--muted); } +.c3-dash-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; } +.c3-panel { + background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 20px; +} +.c3-panel-title { font-weight: 700; font-size: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; } +/* Resource bars */ +.c3-resource { margin-bottom: 16px; } +.c3-resource-head { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; } +.c3-resource-track { height: 8px; background: rgba(255,255,255,0.06); border-radius: 4px; overflow: hidden; } +.c3-resource-fill { height: 100%; border-radius: 4px; transition: width 1s ease; } +/* Quick actions */ +.c3-actions { display: flex; gap: 10px; flex-wrap: wrap; } +.c3-btn { + padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; + cursor: pointer; border: 1px solid var(--line); background: var(--bg2); + color: var(--text); transition: all 0.2s; display: flex; align-items: center; gap: 6px; +} +.c3-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); } +/* Services table */ +.c3-table { width: 100%; border-collapse: separate; border-spacing: 0; } +.c3-table th { + text-align: left; padding: 10px 16px; font-size: 11px; letter-spacing: 1px; + text-transform: uppercase; color: var(--muted); font-family: var(--font-mono); + border-bottom: 1px solid var(--line); +} +.c3-table td { padding: 10px 16px; font-size: 14px; border-bottom: 1px solid rgba(255,255,255,0.03); } +.c3-table tr:hover td { background: rgba(255,255,255,0.02); } +.c3-status { + font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 4px; font-family: var(--font-mono); +} +.c3-status-running { color: var(--green); background: rgba(0,200,83,0.12); } +.c3-status-stopped { color: var(--red); background: rgba(255,23,68,0.12); } +/* Defense chain */ +.c3-chain { + display: flex; align-items: center; gap: 6px; flex-wrap: wrap; + margin-bottom: 24px; padding: 16px; background: var(--card); + border: 1px solid var(--line); border-radius: 14px; +} +.c3-chain-node { + display: flex; flex-direction: column; align-items: center; gap: 4px; + padding: 12px 16px; border-radius: 10px; background: var(--bg2); + border: 1px solid var(--line); min-width: 80px; text-align: center; transition: all 0.3s; +} +.c3-chain-node:hover { transform: scale(1.05); } +.c3-chain-icon { font-size: 24px; } +.c3-chain-label { font-size: 10px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; } +.c3-chain-sub { font-size: 9px; color: var(--muted); } +.c3-chain-arrow { font-size: 16px; color: var(--orange); font-weight: 700; } +/* Services grid */ +.c3-svc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; } +.c3-svc-item { + background: var(--card); border: 1px solid var(--line); border-radius: 12px; + padding: 14px; display: flex; align-items: center; gap: 12px; transition: all 0.3s; cursor: default; +} +.c3-svc-item:hover { border-color: rgba(244,114,182,0.3); transform: translateY(-2px); } +.c3-svc-icon { font-size: 26px; } +.c3-svc-name { font-size: 13px; font-weight: 700; } +.c3-svc-cat { font-size: 10px; color: var(--muted); font-family: var(--font-mono); } +/* 3-Loop summary */ +.c3-loops { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 24px; } +.c3-loop { + background: var(--card); border: 1px solid; border-radius: 12px; padding: 18px; transition: all 0.3s; +} +.c3-loop:hover { transform: translateY(-3px); } +.c3-loop-icon { font-size: 26px; margin-bottom: 8px; } +.c3-loop-title { font-weight: 700; font-size: 15px; margin-bottom: 4px; } +.c3-loop-detail { font-size: 12px; color: var(--muted); line-height: 1.6; font-family: var(--font-mono); } +/* Mesh */ +.c3-mesh-hero { text-align: center; margin-bottom: 32px; } +.c3-mesh-hero h2 { font-family: var(--font-display); font-size: 28px; font-weight: 900; color: var(--yellow); margin-bottom: 8px; } +.c3-mesh-hero p { color: var(--muted); max-width: 600px; margin: 0 auto; } +.c3-mesh-formula { + background: linear-gradient(135deg, rgba(251,191,36,0.1), rgba(251,146,60,0.1)); + border: 1px solid rgba(251,191,36,0.2); border-radius: 14px; padding: 20px; + text-align: center; font-family: var(--font-display); font-size: 20px; + font-weight: 700; margin-bottom: 28px; +} +.c3-mesh-nodes { display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; margin-bottom: 28px; } +.c3-mesh-node { + background: var(--card); border: 1px solid var(--line); border-radius: 16px; + padding: 24px; width: 160px; text-align: center; position: relative; transition: all 0.3s; +} +.c3-mesh-node:hover { border-color: rgba(251,191,36,0.4); transform: translateY(-4px); } +.c3-mesh-node-icon { font-size: 36px; margin-bottom: 8px; } +.c3-mesh-node-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; } +.c3-mesh-node-status { font-size: 10px; font-family: var(--font-mono); } +.c3-mesh-feats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; } +.c3-mesh-feat { + background: var(--card); border: 1px solid var(--line); border-radius: 12px; + padding: 18px; transition: all 0.3s; +} +.c3-mesh-feat:hover { border-color: rgba(251,191,36,0.3); } +.c3-mesh-feat-icon { font-size: 28px; margin-bottom: 10px; } +.c3-mesh-feat-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; } +.c3-mesh-feat-desc { font-size: 12px; color: var(--muted); line-height: 1.5; font-family: var(--font-mono); } +/* Clone station */ +.c3-clone-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px; } +.c3-clone-stat { + background: var(--card); border: 1px solid var(--line); border-left: 4px solid; + border-radius: 10px; padding: 16px; +} +.c3-clone-stat-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; } +.c3-clone-stat-val { font-family: var(--font-display); font-weight: 700; font-size: 20px; } +.c3-uboot { + background: var(--card); border-left: 4px solid var(--green); + border-radius: 0 12px 12px 0; padding: 20px; margin-bottom: 24px; +} +.c3-uboot-title { font-weight: 700; font-size: 16px; margin-bottom: 4px; } +.c3-uboot-desc { font-size: 13px; color: var(--muted); margin-bottom: 12px; } +.c3-uboot-code { + background: #0d1117; border-radius: 8px; padding: 16px; + font-family: var(--font-mono); font-size: 13px; color: var(--green); + line-height: 1.8; overflow-x: auto; white-space: pre; +} +/* Animations */ +@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } +@keyframes pulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } } +@keyframes gradShift { 0%,100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } +@keyframes cubeRotate { + 0% { transform: rotateX(-8deg) rotateY(0deg); } + 25% { transform: rotateX(5deg) rotateY(90deg); } + 50% { transform: rotateX(-3deg) rotateY(180deg); } + 75% { transform: rotateX(8deg) rotateY(270deg); } + 100% { transform: rotateX(-8deg) rotateY(360deg); } +} +/* Responsive */ +@media (max-width: 900px) { + .c3-sidebar { transform: translateX(-100%); } + .c3-sidebar.open { transform: translateX(0); } + .c3-main, .c3-topbar { margin-left: 0 !important; left: 0 !important; } + .c3-dash-cards, .c3-clone-stats { grid-template-columns: repeat(2, 1fr); } + .c3-mesh-feats, .c3-loops { grid-template-columns: 1fr 1fr; } + .c3-dash-grid { grid-template-columns: 1fr; } + .c3-hamburger { display: flex !important; } +} +@media (max-width: 600px) { + .c3-dash-cards, .c3-clone-stats, .c3-loops { grid-template-columns: 1fr; } + .c3-mesh-feats { grid-template-columns: 1fr; } +} +`; +} + +// ========== COMPONENT LIBRARY ========== +// All components are pure functions returning DOM elements (generative-friendly) + +function C(tag, attrs, children) { + // Shorthand DOM builder + var el = document.createElement(tag); + if (attrs) { + for (var k in attrs) { + if (k === 'style' && typeof attrs[k] === 'object') { + for (var s in attrs[k]) el.style[s] = attrs[k][s]; + } else if (k.startsWith('on') && typeof attrs[k] === 'function') { + el.addEventListener(k.slice(2).toLowerCase(), attrs[k]); + } else { + el.setAttribute(k, attrs[k]); + } + } + } + if (children) { + (Array.isArray(children) ? children : [children]).forEach(function(c) { + if (c == null) return; + el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); + }); + } + return el; +} + +function Logo() { + return C('div', { 'class': 'c3-logo-text' }, [ + C('span', { 'class': 'c3-logo-c' }, 'C'), + C('span', { 'class': 'c3-logo-3' }, '3'), + C('span', { 'class': 'c3-logo-b' }, 'B'), + C('span', { 'class': 'c3-logo-o' }, 'O'), + C('span', { 'class': 'c3-logo-x' }, 'X') + ]); +} + +function Badge(text, type) { + return C('span', { 'class': 'c3-badge c3-badge-' + (type || 'green') }, text); +} + +function Panel(title, children) { + return C('div', { 'class': 'c3-panel' }, [ + C('div', { 'class': 'c3-panel-title' }, title), + C('div', {}, children) + ]); +} + +function ResourceBar(label, value, max, percent) { + var pct = percent || Math.round((value / max) * 100); + return C('div', { 'class': 'c3-resource' }, [ + C('div', { 'class': 'c3-resource-head' }, [ + C('span', {}, label), + C('span', {}, value + ' / ' + max) + ]), + C('div', { 'class': 'c3-resource-track' }, [ + C('div', { + 'class': 'c3-resource-fill', + 'style': 'width:' + pct + '%;background:linear-gradient(90deg,var(--green),var(--cyan));' + }) + ]), + C('div', { 'style': 'text-align:right;font-size:12px;color:var(--muted);margin-top:4px;' }, pct + '%') + ]); +} + +function Button(icon, label, onClick) { + return C('button', { 'class': 'c3-btn', 'onClick': onClick }, [ + icon ? C('span', {}, icon) : null, + label + ]); +} + +function StatusBadge(status) { + var cls = status === 'running' ? 'c3-status-running' : 'c3-status-stopped'; + var text = status === 'running' ? 'Running' : 'Stopped'; + return C('span', { 'class': 'c3-status ' + cls }, text); +} + +function ChainNode(icon, label, sub, color) { + var style = color ? 'border-color:' + color : ''; + var labelStyle = color ? 'color:' + color : ''; + return C('div', { 'class': 'c3-chain-node', 'style': style }, [ + C('span', { 'class': 'c3-chain-icon' }, icon), + C('span', { 'class': 'c3-chain-label', 'style': labelStyle }, label), + C('span', { 'class': 'c3-chain-sub' }, sub) + ]); +} + +function ChainArrow() { + return C('span', { 'class': 'c3-chain-arrow' }, '\u2192'); +} + +function ServiceItem(icon, name, cat, status) { + return C('div', { 'class': 'c3-svc-item' }, [ + C('span', { 'class': 'c3-svc-icon' }, icon), + C('div', {}, [ + C('div', { 'class': 'c3-svc-name' }, name), + C('div', { 'class': 'c3-svc-cat' }, cat) + ]), + status ? C('div', { 'class': 'c3-status ' + (status === 'running' ? 'c3-status-running' : 'c3-status-stopped'), 'style': 'margin-left:auto;' }, status === 'running' ? '\u25CF' : '\u25CB') : null + ]); +} + +function LoopCard(icon, title, detail, borderColor) { + return C('div', { 'class': 'c3-loop', 'style': 'border-color:' + borderColor }, [ + C('div', { 'class': 'c3-loop-icon' }, icon), + C('div', { 'class': 'c3-loop-title', 'style': 'color:' + borderColor.replace('0.2', '1') }, title), + C('div', { 'class': 'c3-loop-detail' }, detail) + ]); +} + +function MeshNode(icon, name, status, statusColor) { + return C('div', { 'class': 'c3-mesh-node' }, [ + C('div', { 'class': 'c3-mesh-node-icon' }, icon), + C('div', { 'class': 'c3-mesh-node-name' }, name), + C('div', { 'class': 'c3-mesh-node-status', 'style': 'color:' + statusColor }, '\u25CF ' + status) + ]); +} + +function MeshFeat(icon, name, desc) { + return C('div', { 'class': 'c3-mesh-feat' }, [ + C('div', { 'class': 'c3-mesh-feat-icon' }, icon), + C('div', { 'class': 'c3-mesh-feat-name' }, name), + C('div', { 'class': 'c3-mesh-feat-desc' }, desc) + ]); +} + +// ========== MAIN VIEW ========== return view.extend({ - serviceData: [], - dashboardCounts: {}, + currentScreen: 'hero', load: function() { - return Promise.all([ - callSystemBoard(), - callSystemInfo(), - callGetServices().catch(function() { return []; }), - callDashboardData().catch(function() { return { counts: {} }; }), - this.scanInitServices() - ]).then(function(results) { - return results; - }); + // Initialize cache - returns instantly + C3_CACHE.init(); + // Return cached data immediately for instant render + return Promise.resolve([true]); }, - scanInitServices: function() { - // Use helper script for reliable output capture - // Falls back to inline script if helper not available - return fs.exec('/usr/bin/secubox-services-status', []).then(function(res) { - var services = {}; - if (res && res.stdout) { - res.stdout.trim().split('\n').forEach(function(line) { - var parts = line.split(':'); - if (parts.length === 2 && parts[0] && parts[1]) { - services[parts[0]] = parts[1]; - } - }); - } - return services; - }).catch(function() { - // Fallback: use inline script - return fs.exec('/bin/sh', ['-c', - 'for s in /etc/init.d/*; do [ -x "$s" ] || continue; ' + - 'n=$(basename "$s"); r=stopped; ' + - 'pgrep -f "$n" >/dev/null 2>&1 && r=running; ' + - 'printf "%s:%s\\n" "$n" "$r"; done' - ]).then(function(res) { - var services = {}; - if (res && res.stdout) { - res.stdout.trim().split('\n').forEach(function(line) { - var parts = line.split(':'); - if (parts.length === 2 && parts[0] && parts[1]) { - services[parts[0]] = parts[1]; - } - }); - } - return services; - }).catch(function() { return {}; }); - }); - }, - - render: function(data) { - var boardInfo = data[0] || {}; - var sysInfo = data[1] || {}; - var detectedServices = Array.isArray(data[2]) ? data[2] : (data[2].services || []); - this.dashboardCounts = data[3] || {}; - var initServices = data[4] || {}; + render: function() { var self = this; - // Hide LuCI navigation for clean portal look + // Get cached data for instant render (no waiting) + var boardInfo = C3_CACHE.get('boardInfo'); + var sysInfo = C3_CACHE.get('sysInfo'); + var healthData = C3_CACHE.get('health'); + var modulesData = C3_CACHE.get('modules'); + var initServices = C3_CACHE.get('initServices'); + + // Hide LuCI navigation for full-screen portal this.hideNavigation(); // Inject CSS this.injectCSS(); - // Categorize services - var categorized = this.categorizeServices(initServices, detectedServices); - - return E('div', { 'class': 'sb-portal' }, [ - // Header - this.renderHeader(boardInfo), - - // Main content - E('div', { 'class': 'sb-content' }, [ - // Quick Stats - this.renderQuickStats(sysInfo, initServices), - - // Categorized Services Grid - this.renderServiceCategories(categorized), - - // Active Ports (external services) - this.renderActivePorts(detectedServices) + // Build the portal instantly from cache + var root = C('div', { 'class': 'c3-root', 'id': 'c3-portal' }, [ + this.renderSidebar(boardInfo), + this.renderTopbar(), + C('main', { 'class': 'c3-main' }, [ + this.renderHeroScreen(boardInfo, sysInfo, initServices, modulesData), + this.renderDashboardScreen(boardInfo, sysInfo, healthData, initServices), + this.renderSecurityScreen(), + this.renderServicesScreen(modulesData, initServices), + this.renderCloningScreen(), + this.renderMeshScreen() ]) ]); + + // Fire async fetches - each updates DOM on completion + this.asyncFetchAll(); + + // Start live updates + poll.add(function() { self.updateLiveData(); }, 5); + + // Start morph animation + this.startMorphAnimation(); + + return root; + }, + + // ========== ASYNC PROGRESSIVE FETCH ========== + asyncFetchAll: function() { + var self = this; + + // Fire all fetches in parallel - each updates DOM on completion + callSystemBoard().then(function(data) { + C3_CACHE.set('boardInfo', data); + updateText('[data-stat="hostname"]', data.hostname || 'SecuBox'); + updateText('[data-stat="model"]', data.model || 'C3BOX'); + }).catch(function() {}); + + callSystemInfo().then(function(data) { + C3_CACHE.set('sysInfo', data); + self.updateSysInfo(data); + }).catch(function() {}); + + callGetHealth().then(function(data) { + C3_CACHE.set('health', data); + }).catch(function() {}); + + callGetModules().then(function(data) { + C3_CACHE.set('modules', data); + var count = (data.modules || []).length || 38; + updateText('[data-stat="modules"]', String(count)); + }).catch(function() {}); + + callGetServices().then(function(data) { + C3_CACHE.set('services', data); + }).catch(function() {}); + }, + + updateSysInfo: function(sysInfo) { + var self = this; + if (!sysInfo) return; + + // Uptime + updateText('[data-stat="uptime"]', this.formatUptime(sysInfo.uptime || 0)); + + // CPU load + if (sysInfo.load) { + updateText('[data-stat="cpu-load"]', (sysInfo.load[0] / 65536).toFixed(2)); + } + + // Memory + var mem = sysInfo.memory || {}; + if (mem.total) { + var memPct = Math.round((mem.total - mem.free) / mem.total * 100); + var memUsedGB = ((mem.total - mem.free) / 1024 / 1024 / 1024).toFixed(1); + var memTotalGB = (mem.total / 1024 / 1024 / 1024).toFixed(1); + updateText('[data-stat="mem-percent"]', memPct + '%'); + updateText('[data-stat="mem-used"]', memUsedGB + ' GB'); + updateText('[data-stat="mem-total"]', memTotalGB + ' GB'); + } }, hideNavigation: function() { - document.body.classList.add('secubox-portal-mode'); + document.body.classList.add('c3-portal-mode'); ['#mainmenu', '.main-left', 'header.main-header', 'nav[role="navigation"]', 'aside'].forEach(function(sel) { var el = document.querySelector(sel); if (el) el.style.display = 'none'; @@ -164,375 +678,467 @@ return view.extend({ if (main) { main.style.marginLeft = '0'; main.style.width = '100%'; + main.style.padding = '0'; } }, injectCSS: function() { - if (document.querySelector('#sb-portal-css')) return; + if (document.querySelector('#c3-theme-css')) return; var style = document.createElement('style'); - style.id = 'sb-portal-css'; - style.textContent = ` - .sb-portal { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #0d1117; - color: #e6edf3; - min-height: 100vh; - padding: 1rem; - } - .sb-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 1.5rem; - background: linear-gradient(135deg, #161b22, #21262d); - border-radius: 12px; - margin-bottom: 1.5rem; - border: 1px solid #30363d; - } - .sb-brand { - display: flex; - align-items: center; - gap: 0.75rem; - } - .sb-logo { - width: 48px; - height: 48px; - background: linear-gradient(135deg, #0ff, #00a0a0); - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - font-weight: bold; - color: #000; - } - .sb-title { font-size: 1.5rem; font-weight: 700; color: #0ff; } - .sb-subtitle { font-size: 0.85rem; color: #8b949e; } - .sb-nav { - display: flex; - gap: 0.5rem; - } - .sb-nav-btn { - padding: 0.5rem 1rem; - background: #21262d; - border: 1px solid #30363d; - border-radius: 8px; - color: #e6edf3; - text-decoration: none; - font-size: 0.9rem; - transition: all 0.2s; - } - .sb-nav-btn:hover { background: #30363d; border-color: #0ff; } - .sb-nav-btn.active { background: rgba(0,255,255,0.1); border-color: #0ff; color: #0ff; } - .sb-content { max-width: 1400px; margin: 0 auto; } - .sb-stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; - } - .sb-stat { - background: #161b22; - border: 1px solid #30363d; - border-radius: 12px; - padding: 1.25rem; - text-align: center; - } - .sb-stat-icon { font-size: 2rem; margin-bottom: 0.5rem; } - .sb-stat-value { font-size: 1.75rem; font-weight: 700; color: #0ff; } - .sb-stat-label { font-size: 0.85rem; color: #8b949e; margin-top: 0.25rem; } - .sb-section { - margin-bottom: 2rem; - } - .sb-section-header { - display: flex; - align-items: center; - gap: 0.75rem; - margin-bottom: 1rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid #30363d; - } - .sb-section-icon { font-size: 1.5rem; } - .sb-section-title { font-size: 1.25rem; font-weight: 600; margin: 0; } - .sb-section-count { - margin-left: auto; - background: #21262d; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.8rem; - color: #8b949e; - } - .sb-services { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; - } - .sb-service { - background: #161b22; - border: 1px solid #30363d; - border-radius: 12px; - padding: 1rem; - display: flex; - align-items: center; - gap: 1rem; - text-decoration: none; - color: inherit; - transition: all 0.2s; - } - .sb-service:hover { border-color: #0ff; transform: translateY(-2px); } - .sb-service-icon { - width: 48px; - height: 48px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - } - .sb-service-info { flex: 1; min-width: 0; } - .sb-service-name { font-weight: 600; margin-bottom: 0.25rem; } - .sb-service-port { font-size: 0.8rem; color: #8b949e; } - .sb-service-status { - width: 10px; - height: 10px; - border-radius: 50%; - flex-shrink: 0; - } - .sb-service-status.running { background: #22c55e; box-shadow: 0 0 8px #22c55e; } - .sb-service-status.stopped { background: #ef4444; } - .sb-empty { - text-align: center; - padding: 2rem; - color: #8b949e; - font-style: italic; - } - .sb-ports-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.75rem; - } - .sb-port-card { - background: #161b22; - border: 1px solid #30363d; - border-radius: 8px; - padding: 0.75rem 1rem; - display: flex; - align-items: center; - gap: 0.75rem; - text-decoration: none; - color: inherit; - transition: all 0.2s; - } - .sb-port-card:hover { border-color: #0ff; } - .sb-port-icon { font-size: 1.25rem; } - .sb-port-info { flex: 1; } - .sb-port-name { font-weight: 500; font-size: 0.9rem; } - .sb-port-num { font-size: 0.75rem; color: #0ff; } - `; + style.id = 'c3-theme-css'; + style.textContent = generateCSS(C3_THEME); document.head.appendChild(style); }, - renderHeader: function(boardInfo) { - return E('div', { 'class': 'sb-header' }, [ - E('div', { 'class': 'sb-brand' }, [ - E('div', { 'class': 'sb-logo' }, 'S'), - E('div', {}, [ - E('div', { 'class': 'sb-title' }, 'SecuBox'), - E('div', { 'class': 'sb-subtitle' }, boardInfo.hostname || 'Portal') - ]) + renderSidebar: function(boardInfo) { + var self = this; + var currentSection = ''; + + var navItems = NAV_ITEMS.map(function(item) { + var items = []; + if (item.section !== currentSection) { + currentSection = item.section; + items.push(C('div', { 'class': 'c3-nav-section' }, item.section)); + } + items.push(C('div', { + 'class': 'c3-nav-item' + (item.id === self.currentScreen ? ' active' : ''), + 'data-screen': item.id, + 'onClick': function() { self.switchScreen(item.id); } + }, [ + C('span', { 'class': 'c3-nav-icon' }, item.icon), + item.label + ])); + return items; + }).flat(); + + return C('nav', { 'class': 'c3-sidebar', 'id': 'c3-sidebar' }, [ + C('div', { 'class': 'c3-sidebar-logo' }, [ + Logo(), + C('div', { 'class': 'c3-logo-sub' }, 'SECUBOX \u00B7 CYBERMIND.FR') ]), - E('nav', { 'class': 'sb-nav' }, [ - E('a', { 'class': 'sb-nav-btn active', 'href': L.url('admin/secubox/portal') }, '🏠 Portal'), - E('a', { 'class': 'sb-nav-btn', 'href': L.url('admin/secubox/dashboard') }, '🚀 Hub'), - E('a', { 'class': 'sb-nav-btn', 'href': L.url('admin/secubox/admin/dashboard') }, '⚙️ Admin'), - E('a', { 'class': 'sb-nav-btn', 'href': L.url('admin/status/overview') }, '📊 LuCI') + C('div', { 'class': 'c3-nav' }, navItems), + C('div', { 'class': 'c3-sidebar-footer' }, [ + C('div', { 'class': 'c3-morph', 'id': 'c3-morph' }), + C('div', { 'class': 'c3-footer-ver' }, 'OpenWrt 24.10 \u00B7 v0.20') ]) ]); }, - renderQuickStats: function(sysInfo, initServices) { + renderTopbar: function() { + var self = this; + return C('header', { 'class': 'c3-topbar' }, [ + C('div', { 'class': 'c3-topbar-left' }, [ + C('button', { + 'class': 'c3-hamburger', + 'onClick': function() { + document.getElementById('c3-sidebar').classList.toggle('open'); + } + }, '\u2630'), + C('span', { 'class': 'c3-topbar-title', 'id': 'c3-topbar-title' }, SCREEN_TITLES[this.currentScreen]) + ]), + C('div', { 'class': 'c3-topbar-right' }, [ + Badge('\uD83D\uDD12 SHIELD ACTIVE', 'green'), + Badge('\uD83D\uDCE1 MESH CONNECTED', 'blue'), + C('span', { 'class': 'c3-badge c3-badge-green', 'id': 'c3-refresh-badge' }, '\u27F3 LIVE') + ]) + ]); + }, + + switchScreen: function(screenId) { + this.currentScreen = screenId; + + // Update nav + document.querySelectorAll('.c3-nav-item').forEach(function(el) { + el.classList.toggle('active', el.dataset.screen === screenId); + }); + + // Update screens + document.querySelectorAll('.c3-screen').forEach(function(el) { + el.classList.toggle('active', el.id === 'c3-screen-' + screenId); + }); + + // Update topbar title + var titleEl = document.getElementById('c3-topbar-title'); + if (titleEl) titleEl.textContent = SCREEN_TITLES[screenId] || ''; + + // Close mobile sidebar + document.getElementById('c3-sidebar').classList.remove('open'); + }, + + startMorphAnimation: function() { + var morphEl = document.getElementById('c3-morph'); + if (!morphEl) return; + var showSecu = false; + function update() { + showSecu = !showSecu; + if (showSecu) { + morphEl.innerHTML = 'SecuBox'; + } else { + morphEl.innerHTML = 'C3BOX'; + } + } + update(); + setInterval(update, 3000); + }, + + // ========== SCREEN RENDERERS ========== + + renderHeroScreen: function(boardInfo, sysInfo, initServices, modulesData) { + var running = Object.values(initServices).filter(function(s) { return s === 'running'; }).length; + var modules = (modulesData.modules || []).length || 38; + + return C('section', { 'class': 'c3-screen active', 'id': 'c3-screen-hero' }, [ + C('div', { 'class': 'c3-hero' }, [ + // 3D Cube + C('div', { 'class': 'c3-cube-wrap' }, [ + C('div', { 'class': 'c3-cube' }, [ + C('div', { 'class': 'c3-cube-face face-front' }, [ + C('div', { 'class': 'c3-cube-logo' }, [ + C('span', { 'class': 'c3-logo-c' }, 'C'), + C('span', { 'class': 'c3-logo-3' }, '3'), + C('span', { 'class': 'c3-logo-b' }, 'B'), + C('span', { 'class': 'c3-logo-o' }, 'O'), + C('span', { 'class': 'c3-logo-x' }, 'X') + ]) + ]), + C('div', { 'class': 'c3-cube-face face-back' }, [ + C('div', { 'class': 'c3-cube-text', 'style': 'font-size:60px;color:var(--green);' }, 'C\u00B3') + ]), + C('div', { 'class': 'c3-cube-face face-right' }, [ + C('div', { 'class': 'c3-cube-text', 'style': 'font-size:60px;color:var(--red);' }, '\u00B3') + ]), + C('div', { 'class': 'c3-cube-face face-left' }, [ + C('div', { 'class': 'c3-cube-text', 'style': 'font-size:50px;' }, '\uD83D\uDEE1\uFE0F') + ]), + C('div', { 'class': 'c3-cube-face face-top' }, [ + C('div', { 'class': 'c3-cube-text', 'style': 'font-size:28px;color:var(--green);' }, 'SecuBox') + ]), + C('div', { 'class': 'c3-cube-face face-bottom' }, [ + C('div', { 'class': 'c3-cube-text', 'style': 'font-size:12px;letter-spacing:2px;color:var(--muted);' }, 'CyberMind.FR') + ]) + ]) + ]), + // Title + C('h1', { 'class': 'c3-hero-title' }, 'SecuBox Security Suite'), + C('p', { 'class': 'c3-hero-sub' }, 'From bare hardware to full mesh protection \u2014 the OpenWrt-based security suite by CyberMind.FR'), + C('p', { 'class': 'c3-hero-tagline' }, [ + 'C au cube \u2014 ', + C('em', {}, 'S\u00E9cube'), + ' \u2014 SecuBox', + C('br'), + 'Triple couche. Une seule box.' + ]), + // Stats + C('div', { 'class': 'c3-hero-stats' }, [ + C('div', { 'class': 'c3-hero-stat' }, [ + C('div', { 'class': 'c3-hero-stat-val', 'data-stat': 'modules' }, String(modules)), + C('div', { 'class': 'c3-hero-stat-label' }, 'Modules') + ]), + C('div', { 'class': 'c3-hero-stat' }, [ + C('div', { 'class': 'c3-hero-stat-val', 'data-stat': 'services' }, String(running)), + C('div', { 'class': 'c3-hero-stat-label' }, 'Services Running') + ]), + C('div', { 'class': 'c3-hero-stat' }, [ + C('div', { 'class': 'c3-hero-stat-val' }, '16'), + C('div', { 'class': 'c3-hero-stat-label' }, 'Arch Targets') + ]), + C('div', { 'class': 'c3-hero-stat' }, [ + C('div', { 'class': 'c3-hero-stat-val' }, '5'), + C('div', { 'class': 'c3-hero-stat-label' }, 'Network Modes') + ]), + C('div', { 'class': 'c3-hero-stat' }, [ + C('div', { 'class': 'c3-hero-stat-val' }, 'N\u00B2'), + C('div', { 'class': 'c3-hero-stat-label' }, 'Mesh Protection') + ]) + ]) + ]) + ]); + }, + + renderDashboardScreen: function(boardInfo, sysInfo, healthData, initServices) { + var self = this; var running = Object.values(initServices).filter(function(s) { return s === 'running'; }).length; var total = Object.keys(initServices).length; - var memUsed = sysInfo.memory ? Math.round((sysInfo.memory.total - sysInfo.memory.free) / sysInfo.memory.total * 100) : 0; - return E('div', { 'class': 'sb-stats' }, [ - E('div', { 'class': 'sb-stat' }, [ - E('div', { 'class': 'sb-stat-icon' }, '⚡'), - E('div', { 'class': 'sb-stat-value' }, running + '/' + total), - E('div', { 'class': 'sb-stat-label' }, 'Services Running') + var mem = sysInfo.memory || {}; + var memUsedMB = Math.round((mem.total - mem.free - (mem.buffered || 0)) / 1024 / 1024 * 10) / 10; + var memTotalMB = Math.round(mem.total / 1024 / 1024 * 10) / 10; + var memPct = mem.total ? Math.round((mem.total - mem.free) / mem.total * 100) : 0; + + var uptime = this.formatUptime(sysInfo.uptime || 0); + var load = sysInfo.load ? (sysInfo.load[0] / 65536).toFixed(2) : '0.00'; + + // Top services for table + var topServices = Object.keys(initServices).slice(0, 15).map(function(name) { + return { name: name, status: initServices[name] }; + }); + + return C('section', { 'class': 'c3-screen', 'id': 'c3-screen-dashboard' }, [ + // Top cards + C('div', { 'class': 'c3-dash-cards' }, [ + C('div', { 'class': 'c3-dash-card' }, [ + C('div', { 'class': 'c3-dash-card-icon', 'style': 'background:rgba(41,121,255,0.12);' }, '\uD83D\uDDA5\uFE0F'), + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--blue);' }, 'C3BOX'), + C('div', { 'class': 'c3-dash-card-label' }, boardInfo.model || 'SecuBox') + ]) + ]), + C('div', { 'class': 'c3-dash-card' }, [ + C('div', { 'class': 'c3-dash-card-icon', 'style': 'background:rgba(0,200,83,0.12);' }, '\u23F1\uFE0F'), + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--green);', 'data-stat': 'uptime' }, uptime), + C('div', { 'class': 'c3-dash-card-label' }, 'Uptime') + ]) + ]), + C('div', { 'class': 'c3-dash-card' }, [ + C('div', { 'class': 'c3-dash-card-icon', 'style': 'background:rgba(167,139,250,0.12);' }, '\u26A1'), + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--purple);', 'data-stat': 'services-count' }, String(running)), + C('div', { 'class': 'c3-dash-card-label' }, 'Services') + ]) + ]), + C('div', { 'class': 'c3-dash-card' }, [ + C('div', { 'class': 'c3-dash-card-icon', 'style': 'background:rgba(251,146,60,0.12);' }, '\uD83D\uDD25'), + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--orange);', 'data-stat': 'cpu-load' }, load), + C('div', { 'class': 'c3-dash-card-label' }, 'CPU Load') + ]) + ]) ]), - E('div', { 'class': 'sb-stat' }, [ - E('div', { 'class': 'sb-stat-icon' }, '💾'), - E('div', { 'class': 'sb-stat-value' }, memUsed + '%'), - E('div', { 'class': 'sb-stat-label' }, 'Memory Usage') + // Grid panels + C('div', { 'class': 'c3-dash-grid' }, [ + Panel('\uD83D\uDCC8 Resource Usage', [ + C('div', { 'id': 'c3-resource-mem' }, [ + ResourceBar('\uD83D\uDCBE Memory', memUsedMB + ' GB', memTotalMB + ' GB', memPct) + ]), + C('div', { 'id': 'c3-resource-disk' }, [ + ResourceBar('\uD83D\uDCBD Storage', '8%', '100%', 8) + ]) + ]), + Panel('\u26A1 Quick Actions', [ + C('div', { 'class': 'c3-actions' }, [ + Button('\u2699\uFE0F', 'System Settings'), + Button('\uD83D\uDD04', 'Reboot'), + Button('\uD83D\uDCBE', 'Backup/Flash'), + Button('\uD83D\uDD0D', 'Diagnostics'), + Button('\uD83D\uDC1B', 'Debug Console'), + Button('\uD83D\uDCE1', 'Remote Mgmt') + ]) + ]) ]), - E('div', { 'class': 'sb-stat' }, [ - E('div', { 'class': 'sb-stat-icon' }, '⏱️'), - E('div', { 'class': 'sb-stat-value' }, this.formatUptime(sysInfo.uptime || 0)), - E('div', { 'class': 'sb-stat-label' }, 'Uptime') - ]), - E('div', { 'class': 'sb-stat' }, [ - E('div', { 'class': 'sb-stat-icon' }, '🌐'), - E('div', { 'class': 'sb-stat-value' }, 'Online'), - E('div', { 'class': 'sb-stat-label' }, 'Network Status') + // Services table + Panel('\uD83D\uDD27 Services (' + running + '/' + total + ' running)', [ + C('table', { 'class': 'c3-table' }, [ + C('thead', {}, [ + C('tr', {}, [ + C('th', {}, 'Service'), + C('th', {}, 'Status'), + C('th', {}, 'Enabled') + ]) + ]), + C('tbody', { 'id': 'c3-svc-tbody' }, topServices.map(function(svc) { + return C('tr', {}, [ + C('td', {}, svc.name), + C('td', {}, [StatusBadge(svc.status)]), + C('td', {}, '\u2713') + ]); + })) + ]) ]) ]); }, - categorizeServices: function(initServices, detectedServices) { - var result = {}; - var self = this; - var assigned = {}; - - // Initialize categories - Object.keys(SERVICE_CATEGORIES).forEach(function(cat) { - result[cat] = []; - }); - - // Categorize init.d services - Object.keys(initServices).forEach(function(svc) { - var status = initServices[svc]; - var category = self.getServiceCategory(svc); - - if (category && !assigned[svc]) { - result[category].push({ - name: svc, - status: status, - port: self.getServicePort(svc) - }); - assigned[svc] = true; - } - }); - - // Add remaining running services with ports to 'apps' - Object.keys(initServices).forEach(function(svc) { - if (!assigned[svc] && initServices[svc] === 'running') { - var port = self.getServicePort(svc); - if (port) { - result['apps'].push({ - name: svc, - status: 'running', - port: port - }); - } - } - }); - - return result; - }, - - getServiceCategory: function(svc) { - var lowerSvc = svc.toLowerCase(); - for (var cat in SERVICE_CATEGORIES) { - var catServices = SERVICE_CATEGORIES[cat].services; - for (var i = 0; i < catServices.length; i++) { - if (lowerSvc.indexOf(catServices[i]) !== -1) { - return cat; - } - } - } - return null; - }, - - getServicePort: function(svc) { - var ports = { - 'haproxy': '80,443', 'nginx': '80', 'uhttpd': '80', 'squid': '3128', - 'crowdsec': '8080', 'adguardhome': '3000', 'mitmproxy': '8080', - 'localai': '8080', 'ollama': '11434', 'streamlit': '8501', - 'gitea': '3000', 'jellyfin': '8096', 'lyrion': '9000', - 'dropbear': '22', 'sshd': '22', 'dnsmasq': '53' - }; - return ports[svc] || ''; - }, - - renderServiceCategories: function(categorized) { - var self = this; - var sections = []; - - Object.keys(SERVICE_CATEGORIES).forEach(function(catKey) { - var cat = SERVICE_CATEGORIES[catKey]; - var services = categorized[catKey] || []; - - // Only show category if it has services - if (services.length === 0) return; - - var runningCount = services.filter(function(s) { return s.status === 'running'; }).length; - - sections.push(E('div', { 'class': 'sb-section' }, [ - E('div', { 'class': 'sb-section-header' }, [ - E('span', { 'class': 'sb-section-icon' }, cat.icon), - E('h3', { 'class': 'sb-section-title' }, cat.name), - E('span', { 'class': 'sb-section-count' }, runningCount + '/' + services.length + ' running') - ]), - E('div', { 'class': 'sb-services' }, - services.map(function(svc) { - return E('div', { 'class': 'sb-service' }, [ - E('div', { - 'class': 'sb-service-icon', - 'style': 'background: ' + cat.color + '22;' - }, cat.icon), - E('div', { 'class': 'sb-service-info' }, [ - E('div', { 'class': 'sb-service-name' }, svc.name), - svc.port ? E('div', { 'class': 'sb-service-port' }, 'Port: ' + svc.port) : null - ]), - E('div', { 'class': 'sb-service-status ' + svc.status }) - ]); - }) - ) - ])); - }); - - return E('div', {}, sections); - }, - - renderActivePorts: function(detectedServices) { - var external = detectedServices.filter(function(s) { - return s.external && s.url; - }); - - if (external.length === 0) { - return E('div', { 'class': 'sb-section' }, [ - E('div', { 'class': 'sb-section-header' }, [ - E('span', { 'class': 'sb-section-icon' }, '🔌'), - E('h3', { 'class': 'sb-section-title' }, 'Active Ports') - ]), - E('div', { 'class': 'sb-empty' }, 'No external services detected') - ]); - } - - var iconMap = { - 'lock': '🔐', 'globe': '🌐', 'arrow': '🔀', 'shield': '🔒', - 'git': '📦', 'blog': '📝', 'security': '🛡️', 'settings': '⚙️', - 'feed': '📡', 'chart': '📊', 'stats': '📈', 'admin': '🔧', - 'app': '🎨', 'music': '🎵', 'onion': '🧅' - }; - - return E('div', { 'class': 'sb-section' }, [ - E('div', { 'class': 'sb-section-header' }, [ - E('span', { 'class': 'sb-section-icon' }, '🔌'), - E('h3', { 'class': 'sb-section-title' }, 'Active Ports'), - E('span', { 'class': 'sb-section-count' }, external.length + ' services') + renderSecurityScreen: function() { + return C('section', { 'class': 'c3-screen', 'id': 'c3-screen-security' }, [ + // Tabs + C('div', { 'style': 'display:flex;gap:8px;margin-bottom:24px;' }, [ + C('div', { 'class': 'c3-btn', 'style': 'border-color:var(--cyan);color:var(--cyan);' }, '\uD83D\uDEE1\uFE0F DNS Guard'), + C('div', { 'class': 'c3-btn' }, '\uD83D\uDEE1\uFE0F CrowdSec'), + C('div', { 'class': 'c3-btn' }, '\uD83D\uDD0D Threat Analyst'), + C('div', { 'class': 'c3-btn' }, '\uD83D\uDD12 mitmproxy'), + C('div', { 'class': 'c3-btn' }, '\uD83D\uDC64 Client Guardian') ]), - E('div', { 'class': 'sb-ports-grid' }, - external.map(function(svc) { - var url = 'http://' + window.location.hostname + svc.url; - var icon = iconMap[svc.icon] || '⚡'; - return E('a', { - 'class': 'sb-port-card', - 'href': url, - 'target': '_blank' - }, [ - E('span', { 'class': 'sb-port-icon' }, icon), - E('div', { 'class': 'sb-port-info' }, [ - E('div', { 'class': 'sb-port-name' }, svc.name), - E('div', { 'class': 'sb-port-num' }, ':' + svc.port) - ]) - ]); - }) - ) + // Defense chain + C('div', { 'class': 'c3-chain' }, [ + ChainNode('\uD83C\uDF00', 'DNS', 'Block before connect', 'rgba(251,146,60,0.4)'), + ChainArrow(), + ChainNode('\uD83E\uDDF1', 'Firewall', 'Filter IPs'), + ChainArrow(), + ChainNode('\uD83D\uDD0D', 'WAF', 'Inspect requests'), + ChainArrow(), + ChainNode('\uD83D\uDCE1', 'Mesh', 'Share alerts P2P'), + ChainArrow(), + ChainNode('\u2705', 'CLEAN', 'Safe traffic', 'rgba(34,211,238,0.4)') + ]), + // DNS stats + C('div', { 'class': 'c3-dash-cards', 'style': 'grid-template-columns:repeat(3,1fr);' }, [ + C('div', { 'class': 'c3-dash-card', 'style': 'border-top:3px solid var(--green);' }, [ + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--green);font-family:var(--font-mono);' }, '9.9.9.9'), + C('div', { 'class': 'c3-dash-card-label' }, 'PRIMARY DNS') + ]) + ]), + C('div', { 'class': 'c3-dash-card', 'style': 'border-top:3px solid var(--blue);' }, [ + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--blue);font-family:var(--font-mono);' }, '149.112.112.112'), + C('div', { 'class': 'c3-dash-card-label' }, 'SECONDARY DNS') + ]) + ]), + C('div', { 'class': 'c3-dash-card', 'style': 'border-top:3px solid var(--cyan);' }, [ + C('div', {}, [ + C('div', { 'class': 'c3-dash-card-val', 'style': 'color:var(--cyan);' }, 'quad9'), + C('div', { 'class': 'c3-dash-card-label' }, 'PROVIDER') + ]) + ]) + ]), + // Smart config + C('div', { 'style': 'background:linear-gradient(135deg,rgba(0,200,83,0.08),rgba(34,211,238,0.08));border:1px solid rgba(0,200,83,0.2);border-radius:14px;padding:20px;margin-top:24px;' }, [ + C('div', { 'style': 'font-weight:700;font-size:16px;margin-bottom:8px;color:var(--green);' }, '\u26A1 Smart Config'), + C('div', { 'style': 'font-size:14px;color:var(--muted);margin-bottom:12px;' }, 'Auto-detect the fastest uncensored DNS for your location'), + Button('\uD83D\uDE80', 'Run Smart Config') + ]) ]); }, + renderServicesScreen: function(modulesData, initServices) { + var modules = [ + { icon: '\uD83D\uDEE1\uFE0F', name: 'CrowdSec', cat: 'security \u00B7 CTI' }, + { icon: '\uD83D\uDD0D', name: 'Netifyd DPI', cat: 'security \u00B7 inspect' }, + { icon: '\uD83C\uDF00', name: 'Vortex DNS', cat: 'security \u00B7 dns' }, + { icon: '\uD83E\uDDF1', name: 'nftables FW', cat: 'security \u00B7 firewall' }, + { icon: '\uD83D\uDD10', name: 'WireGuard', cat: 'network \u00B7 vpn' }, + { icon: '\uD83D\uDD11', name: 'Auth Guardian', cat: 'security \u00B7 auth' }, + { icon: '\uD83D\uDC64', name: 'Client Guardian', cat: 'network \u00B7 NAC' }, + { icon: '\uD83D\uDCCA', name: 'Netdata', cat: 'monitor \u00B7 metrics' }, + { icon: '\uD83C\uDFAC', name: 'Media Flow', cat: 'monitor \u00B7 streams' }, + { icon: '\uD83D\uDEA6', name: 'Traffic Shaper', cat: 'network \u00B7 QoS' }, + { icon: '\uD83D\uDCBE', name: 'CDN Cache', cat: 'network \u00B7 proxy' }, + { icon: '\uD83C\uDF10', name: 'VHost Manager', cat: 'infra \u00B7 19 templates' }, + { icon: '\uD83D\uDCF6', name: 'Bandwidth Mgr', cat: 'network \u00B7 8 levels' }, + { icon: '\uD83C\uDFE0', name: 'System Hub', cat: 'core \u00B7 unified' }, + { icon: '\uD83E\uDD16', name: 'AI Module', cat: 'ai \u00B7 detection' }, + { icon: '\uD83D\uDCE1', name: 'IoT Guard', cat: 'iot \u00B7 isolation' }, + { icon: '\uD83D\uDD12', name: 'mitmproxy', cat: 'security \u00B7 intercept' }, + { icon: '\uD83D\uDCCB', name: 'Threat Analyst', cat: 'security \u00B7 analysis' } + ]; + + return C('section', { 'class': 'c3-screen', 'id': 'c3-screen-services' }, [ + C('div', { 'style': 'margin-bottom:20px;' }, [ + C('h2', { 'style': 'font-size:22px;font-weight:700;margin-bottom:6px;' }, '\uD83D\uDCE6 38 Module Stack'), + C('p', { 'style': 'color:var(--muted);font-size:14px;' }, 'Each module has its own LuCI dashboard, RPCD backend, UCI config, and procd service management. No cloud required.') + ]), + // Services grid + C('div', { 'class': 'c3-svc-grid' }, modules.map(function(m) { + return ServiceItem(m.icon, m.name, m.cat); + })), + // Formula + C('div', { 'style': 'background:linear-gradient(135deg,rgba(244,114,182,0.08),rgba(167,139,250,0.08));border:1px solid rgba(244,114,182,0.15);border-radius:12px;padding:16px 24px;text-align:center;font-family:var(--font-mono);font-size:14px;margin-top:20px;' }, [ + C('span', { 'style': 'color:var(--cyan);font-weight:700;' }, '38'), + ' modules \u00B7 ', + C('span', { 'style': 'color:var(--cyan);font-weight:700;' }, '9'), + ' categories \u00B7 ', + C('span', { 'style': 'color:var(--cyan);font-weight:700;' }, '40'), + ' active services \u00B7 ', + C('em', { 'style': 'color:var(--purple);font-weight:700;font-style:normal;' }, '0 cloud dependencies'), + ' \u2601\uFE0F\u274C' + ]), + // 3-Loop + C('div', { 'class': 'c3-loops' }, [ + LoopCard('\u26A1', 'Loop 1 \u2014 Operational', 'ms \u2192 seconds\nnftables \u00B7 DPI \u00B7 Bouncer\n\uD83E\uDDF1 Immediate blocking', 'rgba(255,23,68,0.2)'), + LoopCard('\uD83D\uDD04', 'Loop 2 \u2014 Tactical', 'minutes \u2192 hours\nPattern analysis\n\uD83D\uDCCA Adaptive defense', 'rgba(251,146,60,0.2)'), + LoopCard('\uD83E\uDDE0', 'Loop 3 \u2014 Strategic', 'hours \u2192 days\nMesh intelligence\n\uD83D\uDCE1 Fleet-wide policy', 'rgba(41,121,255,0.2)') + ]) + ]); + }, + + renderCloningScreen: function() { + return C('section', { 'class': 'c3-screen', 'id': 'c3-screen-cloning' }, [ + // Status cards + C('div', { 'class': 'c3-clone-stats' }, [ + C('div', { 'class': 'c3-clone-stat', 'style': 'border-left-color:var(--green);' }, [ + C('div', { 'class': 'c3-clone-stat-label' }, 'Device'), + C('div', { 'class': 'c3-clone-stat-val', 'style': 'color:var(--green);' }, 'mochabin') + ]), + C('div', { 'class': 'c3-clone-stat', 'style': 'border-left-color:var(--cyan);' }, [ + C('div', { 'class': 'c3-clone-stat-label' }, 'TFTP'), + C('div', { 'class': 'c3-clone-stat-val', 'style': 'color:var(--cyan);' }, 'Running') + ]), + C('div', { 'class': 'c3-clone-stat', 'style': 'border-left-color:var(--purple);' }, [ + C('div', { 'class': 'c3-clone-stat-label' }, 'Tokens'), + C('div', { 'class': 'c3-clone-stat-val', 'style': 'color:var(--purple);' }, '0') + ]), + C('div', { 'class': 'c3-clone-stat', 'style': 'border-left-color:var(--orange);' }, [ + C('div', { 'class': 'c3-clone-stat-label' }, 'Clones'), + C('div', { 'class': 'c3-clone-stat-val', 'style': 'color:var(--orange);' }, '0') + ]) + ]), + // Quick actions + Panel('\u26A1 Quick Actions', [ + C('div', { 'class': 'c3-actions' }, [ + C('button', { 'class': 'c3-btn', 'style': 'border-color:rgba(0,200,83,0.3);color:var(--green);' }, '\uD83D\uDD28 Build Image'), + C('button', { 'class': 'c3-btn', 'style': 'border-color:rgba(255,23,68,0.3);color:var(--red);' }, '\u23F9\uFE0F Stop TFTP'), + C('button', { 'class': 'c3-btn', 'style': 'border-color:rgba(41,121,255,0.3);color:var(--blue);' }, '\uD83D\uDD11 New Token'), + C('button', { 'class': 'c3-btn', 'style': 'border-color:rgba(251,146,60,0.3);color:var(--orange);' }, '\u2705 Auto-Approve Token') + ]) + ]), + // U-Boot commands + C('div', { 'class': 'c3-uboot', 'style': 'margin-top:20px;' }, [ + C('div', { 'class': 'c3-uboot-title' }, '\uD83D\uDD27 U-Boot Flash Commands'), + C('div', { 'class': 'c3-uboot-desc' }, 'Run these commands in U-Boot (Marvell>> prompt) on the target device:'), + C('div', { 'class': 'c3-uboot-code' }, 'setenv serverip 192.168.255.1\nsetenv ipaddr 192.168.255.100\ndhcp\ntftpboot 0x6000000 secubox-clone.img\nmmc dev 1\nmmc write 0x6000000 0 ${filesize}\nreset') + ]) + ]); + }, + + renderMeshScreen: function() { + return C('section', { 'class': 'c3-screen', 'id': 'c3-screen-mesh' }, [ + // Hero + C('div', { 'class': 'c3-mesh-hero' }, [ + C('h2', {}, '\uD83D\uDCE1 MESH \u2014 MaaS Federation'), + C('p', {}, 'Each SecuBox node joins the P2P mesh via WireGuard tunnels, sharing threat intelligence in real-time. Every new node makes the entire fleet stronger.') + ]), + // Formula + C('div', { 'class': 'c3-mesh-formula' }, [ + C('span', { 'style': 'color:var(--yellow);' }, 'N nodes'), + C('span', { 'style': 'color:var(--muted);margin:0 8px;' }, '\u00D7'), + C('span', { 'style': 'color:var(--yellow);' }, 'N peers'), + C('span', { 'style': 'color:var(--muted);margin:0 8px;' }, '='), + C('span', { 'style': 'color:var(--orange);' }, 'N\u00B2 protection \uD83D\uDEE1\uFE0F') + ]), + // Nodes + C('div', { 'class': 'c3-mesh-nodes' }, [ + MeshNode('\uD83C\uDFE0', 'Home GW', 'ONLINE', 'var(--green)'), + MeshNode('\uD83C\uDFE2', 'Office HQ', 'ONLINE', 'var(--green)'), + MeshNode('\uD83D\uDCE1', 'Edge Node', 'SYNCING', 'var(--yellow)'), + MeshNode('\uD83C\uDF0D', 'Remote', 'ONLINE', 'var(--green)') + ]), + // Features + C('div', { 'class': 'c3-mesh-feats' }, [ + MeshFeat('\uD83D\uDD17', 'WireGuard Mesh', 'Auto-peering \uD83E\uDD1D\nKey rotation \uD83D\uDD11\nTrust scoring \u2B50\ndid:plc identity'), + MeshFeat('\uD83E\uDDE0', 'Shared Intel', 'CrowdSec decisions \uD83D\uDCE4\nThreat feeds sync \uD83D\uDD04\nBlocklist fusion \uD83D\uDCCB\nReal-time alerts \uD83D\uDEA8'), + MeshFeat('\uD83D\uDE80', 'MaaS Deploy', 'Master \u2192 fleet push \uD83D\uDCE1\nConfig sync \uD83D\uDD04\nLanding pages\ngk2.secubox.in \uD83C\uDF0D'), + MeshFeat('\uD83C\uDFC5', 'ANSSI Path', 'CSPN certification\nENISA compliance \u2705\nAudit trail \uD83D\uDCDC\nSovereign \uD83C\uDDEB\uD83C\uDDF7') + ]) + ]); + }, + + // ========== LIVE UPDATE (Async Progressive) ========== + updateLiveData: function() { + var self = this; + + // Async fetch - updates DOM on completion + callSystemInfo().then(function(data) { + C3_CACHE.set('sysInfo', data); + self.updateSysInfo(data); + }).catch(function() {}); + + // Flash refresh badge + var badge = document.getElementById('c3-refresh-badge'); + if (badge) { + badge.style.opacity = badge.style.opacity === '0.3' ? '1' : '0.3'; + } + }, + formatUptime: function(seconds) { if (!seconds) return '--'; var d = Math.floor(seconds / 86400); 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 ed653236..824a2b57 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 @@ -68,7 +68,16 @@ "title": "SecuBox", "order": 99, "action": { - "type": "firstchild" + "type": "alias", + "path": "secubox-public/portal" + } + }, + "secubox-public/portal": { + "title": "C3BOX Portal", + "order": 1, + "action": { + "type": "view", + "path": "secubox-portal/index" } }, "secubox-public/bugbounty": { diff --git a/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json b/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json index 5a5e41fe..9c72ff23 100644 --- a/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json +++ b/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json @@ -23,9 +23,34 @@ "get_health", "get_components", "list_services" - ] + ], + "system": ["board", "info"] } }, "write": {} + }, + "unauthenticated": { + "description": "Public access for SecuBox Portal", + "read": { + "ubus": { + "luci.secubox": [ + "modules", + "getModules", + "modules_by_category", + "get_theme", + "getStatus", + "getHealth", + "get_services" + ], + "luci.system-hub": [ + "status", + "get_system_info", + "get_health", + "get_components", + "list_services" + ], + "system": ["board", "info"] + } + } } }