From 03e90bb4af675f7cdf9f0a769dddfbc50333eb3e Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 11 Feb 2026 12:00:00 +0100 Subject: [PATCH] feat(ui): Add KISS theme with eye toggle and git status tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared kiss-theme.js module for consistent dark theme across views - Add eye toggle button (👁️) to switch between KISS and LuCI modes - Add git repo status methods to luci.gitea RPCD: - get_repo_status: branch, ahead/behind, staged/modified files - get_commit_history: recent commits with stats - get_commit_stats: daily commit counts for graphs - Update InterceptoR overview with KISS styling and responsive grid - Fix quick links paths (network-tweaks → admin/network/) Co-Authored-By: Claude Opus 4.5 --- .../root/usr/libexec/rpcd/luci.gitea | 160 +++++++- .../resources/view/interceptor/overview.js | 343 ++++++++++-------- .../resources/secubox/kiss-theme.js | 289 +++++++++++++++ 3 files changed, 643 insertions(+), 149 deletions(-) create mode 100644 package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js diff --git a/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea index b7a5b480..80619557 100755 --- a/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea +++ b/package/secubox/luci-app-gitea/root/usr/libexec/rpcd/luci.gitea @@ -634,6 +634,152 @@ create_repo() { fi } +# Get git repo status (for local development tracking) +get_repo_status() { + read -r input + local repo_path + repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null) + + # Default to secubox-openwrt if no path specified + [ -z "$repo_path" ] && repo_path="/root/secubox-openwrt" + + if [ ! -d "$repo_path/.git" ]; then + json_error "Not a git repository: $repo_path" + return + fi + + cd "$repo_path" || { json_error "Cannot access $repo_path"; return; } + + local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") + local remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "") + local commit_hash=$(git rev-parse --short HEAD 2>/dev/null || echo "") + local commit_msg=$(git log -1 --pretty=%s 2>/dev/null || echo "") + local commit_date=$(git log -1 --pretty=%ci 2>/dev/null || echo "") + + # Commits ahead/behind + local ahead=0 behind=0 + if [ -n "$remote_branch" ]; then + ahead=$(git rev-list --count HEAD ^${remote_branch} 2>/dev/null || echo "0") + behind=$(git rev-list --count ${remote_branch} ^HEAD 2>/dev/null || echo "0") + fi + + # Diff stats + local staged=$(git diff --cached --numstat 2>/dev/null | wc -l) + local modified=$(git diff --numstat 2>/dev/null | wc -l) + local untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l) + + # Lines changed + local lines_added=$(git diff --numstat 2>/dev/null | awk '{sum+=$1} END {print sum+0}') + local lines_deleted=$(git diff --numstat 2>/dev/null | awk '{sum+=$2} END {print sum+0}') + + json_init + json_add_boolean "success" 1 + json_add_string "repo_path" "$repo_path" + json_add_string "branch" "$branch" + json_add_string "remote_branch" "$remote_branch" + json_add_string "commit_hash" "$commit_hash" + json_add_string "commit_msg" "$commit_msg" + json_add_string "commit_date" "$commit_date" + json_add_int "ahead" "${ahead:-0}" + json_add_int "behind" "${behind:-0}" + json_add_int "staged" "${staged:-0}" + json_add_int "modified" "${modified:-0}" + json_add_int "untracked" "${untracked:-0}" + json_add_int "lines_added" "${lines_added:-0}" + json_add_int "lines_deleted" "${lines_deleted:-0}" + json_dump +} + +# Get recent commits for evolution graph +get_commit_history() { + read -r input + local repo_path limit + repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null) + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + + [ -z "$repo_path" ] && repo_path="/root/secubox-openwrt" + [ -z "$limit" ] && limit=20 + + if [ ! -d "$repo_path/.git" ]; then + json_error "Not a git repository: $repo_path" + return + fi + + cd "$repo_path" || { json_error "Cannot access $repo_path"; return; } + + json_init + json_add_boolean "success" 1 + json_add_array "commits" + + git log --pretty=format:'%H|%h|%s|%an|%ae|%ci|%cr' -n "$limit" 2>/dev/null | while IFS='|' read -r hash short_hash msg author email date relative; do + # Get stats for this commit + local stats=$(git show --stat --format='' "$hash" 2>/dev/null | tail -1) + local files_changed=$(echo "$stats" | grep -oP '\d+(?= files? changed)' || echo "0") + local insertions=$(echo "$stats" | grep -oP '\d+(?= insertions?)' || echo "0") + local deletions=$(echo "$stats" | grep -oP '\d+(?= deletions?)' || echo "0") + + json_add_object "" + json_add_string "hash" "$hash" + json_add_string "short_hash" "$short_hash" + json_add_string "message" "$msg" + json_add_string "author" "$author" + json_add_string "date" "$date" + json_add_string "relative" "$relative" + json_add_int "files_changed" "${files_changed:-0}" + json_add_int "insertions" "${insertions:-0}" + json_add_int "deletions" "${deletions:-0}" + json_close_object + done + + json_close_array + json_dump +} + +# Get daily commit stats for graph +get_commit_stats() { + read -r input + local repo_path days + repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null) + days=$(echo "$input" | jsonfilter -e '@.days' 2>/dev/null) + + [ -z "$repo_path" ] && repo_path="/root/secubox-openwrt" + [ -z "$days" ] && days=30 + + if [ ! -d "$repo_path/.git" ]; then + json_error "Not a git repository: $repo_path" + return + fi + + cd "$repo_path" || { json_error "Cannot access $repo_path"; return; } + + json_init + json_add_boolean "success" 1 + json_add_array "daily_stats" + + # Get commits per day for last N days + for i in $(seq 0 $((days - 1))); do + local date_str=$(date -d "-${i} days" +%Y-%m-%d 2>/dev/null || date -v-${i}d +%Y-%m-%d 2>/dev/null) + [ -z "$date_str" ] && continue + + local count=$(git log --after="${date_str} 00:00" --before="${date_str} 23:59" --oneline 2>/dev/null | wc -l) + + json_add_object "" + json_add_string "date" "$date_str" + json_add_int "commits" "${count:-0}" + json_close_object + done + + json_close_array + + # Total stats + local total_commits=$(git rev-list --count HEAD 2>/dev/null || echo "0") + local total_authors=$(git log --format='%ae' | sort -u | wc -l) + + json_add_int "total_commits" "$total_commits" + json_add_int "total_authors" "$total_authors" + json_dump +} + # Create backup create_backup() { local result @@ -789,7 +935,10 @@ case "$1" in "create_backup": {}, "list_backups": {}, "restore_backup": {"file": "str"}, - "get_install_progress": {} + "get_install_progress": {}, + "get_repo_status": {"path": "str"}, + "get_commit_history": {"path": "str", "limit": 20}, + "get_commit_stats": {"path": "str", "days": 30} } EOF ;; @@ -861,6 +1010,15 @@ case "$1" in get_install_progress) get_install_progress ;; + get_repo_status) + get_repo_status + ;; + get_commit_history) + get_commit_history + ;; + get_commit_stats) + get_commit_stats + ;; *) json_error "Unknown method: $2" ;; diff --git a/package/secubox/luci-app-interceptor/htdocs/luci-static/resources/view/interceptor/overview.js b/package/secubox/luci-app-interceptor/htdocs/luci-static/resources/view/interceptor/overview.js index 22612292..8b76c7f7 100644 --- a/package/secubox/luci-app-interceptor/htdocs/luci-static/resources/view/interceptor/overview.js +++ b/package/secubox/luci-app-interceptor/htdocs/luci-static/resources/view/interceptor/overview.js @@ -9,174 +9,221 @@ var callGetStatus = rpc.declare({ expect: {} }); -var PILLAR_ICONS = { - wpad: '🌐', // Globe for WPAD - mitm: '🛡', // Shield for mitmproxy - cdn_cache: '💾', // Disk for CDN Cache - cookie_tracker: '🍪', // Cookie for Cookie Tracker - api_failover: '⚡' // Lightning for API Failover -}; +var PILLARS = [ + { id: 'wpad', name: 'WPAD', icon: '🌐', desc: 'Auto-proxy discovery' }, + { id: 'mitm', name: 'MITM Proxy', icon: '🛡️', desc: 'Traffic inspection' }, + { id: 'cdn_cache', name: 'CDN Cache', icon: '💾', desc: 'Content caching' }, + { id: 'cookie_tracker', name: 'Cookies', icon: '🍪', desc: 'Tracker detection' }, + { id: 'api_failover', name: 'API Failover', icon: '⚡', desc: 'Graceful degradation' } +]; -var PILLAR_NAMES = { - wpad: 'WPAD Redirector', - mitm: 'MITM Proxy', - cdn_cache: 'CDN Cache', - cookie_tracker: 'Cookie Tracker', - api_failover: 'API Failover' -}; +var QUICK_LINKS = [ + { name: 'Network Tweaks', path: 'admin/network/network-tweaks', icon: '🌐' }, + { name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy/status', icon: '🔍' }, + { name: 'CDN Cache', path: 'admin/services/cdn-cache', icon: '💾' }, + { name: 'CrowdSec', path: 'admin/secubox/security/crowdsec/overview', icon: '🛡️' } +]; return view.extend({ load: function() { - return callGetStatus(); - }, - - renderHealthScore: function(data) { - var summary = data.summary || {}; - var score = summary.health_score || 0; - var pillars_active = summary.pillars_active || 0; - var pillars_total = summary.pillars_total || 5; - - var scoreColor = score >= 80 ? '#4caf50' : score >= 50 ? '#ff9800' : '#f44336'; - - return E('div', { 'class': 'cbi-section', 'style': 'text-align: center; padding: 30px;' }, [ - E('div', { 'style': 'font-size: 64px; margin-bottom: 10px;' }, [ - E('span', { 'style': 'color: ' + scoreColor + '; font-weight: bold;' }, score + '%') - ]), - E('div', { 'style': 'font-size: 18px; color: #888;' }, - 'InterceptoR Health Score'), - E('div', { 'style': 'font-size: 14px; color: #666; margin-top: 10px;' }, - pillars_active + ' of ' + pillars_total + ' pillars active') - ]); - }, - - renderPillarCard: function(id, data, name, icon) { - var pillarData = data[id] || {}; - var enabled = pillarData.enabled || false; - var running = pillarData.running !== undefined ? pillarData.running : enabled; - - var statusColor = running ? '#4caf50' : '#f44336'; - var statusText = running ? 'Active' : 'Inactive'; - - var statsHtml = []; - - // Build stats based on pillar type - switch(id) { - case 'wpad': - if (pillarData.dhcp_configured) { - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'DHCP: Configured')); - } - if (pillarData.enforce_enabled) { - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #4caf50;' }, - 'Enforcement: ON')); - } - break; - - case 'mitm': - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Threats Today: ' + (pillarData.threats_today || 0))); - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Active: ' + (pillarData.active_connections || 0))); - break; - - case 'cdn_cache': - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Hit Ratio: ' + (pillarData.hit_ratio || 0) + '%')); - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Saved: ' + (pillarData.saved_mb || 0) + ' MB')); - if (pillarData.offline_mode) { - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #ff9800;' }, - 'OFFLINE MODE')); - } - break; - - case 'cookie_tracker': - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Cookies: ' + (pillarData.total_cookies || 0))); - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #f44336;' }, - 'Trackers: ' + (pillarData.trackers_detected || 0))); - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Blocked: ' + (pillarData.blocked || 0))); - break; - - case 'api_failover': - statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' }, - 'Stale Serves: ' + (pillarData.stale_serves || 0))); - break; - } - - return E('div', { - 'style': 'background: #222; border-radius: 8px; padding: 20px; margin: 10px; ' + - 'min-width: 200px; flex: 1; text-align: center; ' + - 'border-left: 4px solid ' + statusColor + ';' - }, [ - E('div', { 'style': 'font-size: 32px; margin-bottom: 10px;' }, icon), - E('div', { 'style': 'font-size: 16px; font-weight: bold; margin-bottom: 5px;' }, name), - E('div', { 'style': 'font-size: 12px; color: ' + statusColor + '; margin-bottom: 10px;' }, - statusText), - E('div', {}, statsHtml) - ]); + return callGetStatus().catch(function() { + return { success: false }; + }); }, render: function(data) { + var self = this; + + // Inject KISS CSS + this.injectCSS(); + if (!data || !data.success) { - return E('div', { 'class': 'alert-message warning' }, - 'Failed to load InterceptoR status'); + return E('div', { 'class': 'kiss-root' }, [ + E('div', { 'class': 'kiss-card kiss-panel-red' }, [ + E('div', { 'class': 'kiss-card-title' }, '⚠️ InterceptoR Status Unavailable'), + E('p', { 'style': 'color: var(--kiss-muted);' }, 'Failed to load status. Check if RPCD service is running.') + ]) + ]); } - var pillars = [ - { id: 'wpad', name: PILLAR_NAMES.wpad, icon: PILLAR_ICONS.wpad }, - { id: 'mitm', name: PILLAR_NAMES.mitm, icon: PILLAR_ICONS.mitm }, - { id: 'cdn_cache', name: PILLAR_NAMES.cdn_cache, icon: PILLAR_ICONS.cdn_cache }, - { id: 'cookie_tracker', name: PILLAR_NAMES.cookie_tracker, icon: PILLAR_ICONS.cookie_tracker }, - { id: 'api_failover', name: PILLAR_NAMES.api_failover, icon: PILLAR_ICONS.api_failover } - ]; + var summary = data.summary || {}; + var score = summary.health_score || 0; + var pillarsActive = summary.pillars_active || 0; - var cards = pillars.map(function(p) { - return this.renderPillarCard(p.id, data, p.name, p.icon); - }, this); + return E('div', { 'class': 'kiss-root' }, [ + // Header + E('div', { 'style': 'margin-bottom: 24px;' }, [ + E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0 0 8px 0;' }, '🧙 InterceptoR'), + E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, 'The Gandalf Proxy — Transparent traffic interception') + ]), - return E('div', { 'class': 'cbi-map' }, [ - E('h2', { 'style': 'margin-bottom: 5px;' }, 'SecuBox InterceptoR'), - E('p', { 'style': 'color: #888; margin-bottom: 20px;' }, - 'The Gandalf Proxy - Transparent traffic interception and protection'), - - // Health Score - this.renderHealthScore(data), + // Health Score Card + E('div', { 'class': 'kiss-card', 'style': 'text-align: center; padding: 30px; margin-bottom: 20px;' }, [ + E('div', { 'style': 'font-size: 56px; font-weight: 900; color: ' + this.scoreColor(score) + ';' }, score + '%'), + E('div', { 'style': 'font-size: 14px; color: var(--kiss-muted); margin-top: 8px;' }, 'Health Score'), + E('div', { 'style': 'font-size: 12px; color: var(--kiss-cyan); margin-top: 4px;' }, + pillarsActive + ' of 5 pillars active') + ]), // Pillars Grid - E('h3', { 'style': 'margin-top: 30px;' }, 'Interception Pillars'), - E('div', { - 'style': 'display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 15px;' - }, cards), + E('div', { 'class': 'kiss-grid kiss-grid-auto', 'style': 'margin-bottom: 24px;' }, + PILLARS.map(function(p) { + return self.renderPillar(p, data[p.id] || {}); + }) + ), // Quick Links - E('h3', { 'style': 'margin-top: 30px;' }, 'Quick Links'), - E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px;' }, [ - E('a', { - 'href': '/cgi-bin/luci/admin/secubox/network-tweaks', - 'class': 'cbi-button', - 'style': 'text-decoration: none;' - }, 'Network Tweaks (WPAD)'), - E('a', { - 'href': '/cgi-bin/luci/admin/secubox/mitmproxy/overview', - 'class': 'cbi-button', - 'style': 'text-decoration: none;' - }, 'mitmproxy'), - E('a', { - 'href': '/cgi-bin/luci/admin/secubox/cdn-cache/overview', - 'class': 'cbi-button', - 'style': 'text-decoration: none;' - }, 'CDN Cache'), - E('a', { - 'href': '/cgi-bin/luci/admin/secubox/crowdsec/overview', - 'class': 'cbi-button', - 'style': 'text-decoration: none;' - }, 'CrowdSec') + E('div', { 'class': 'kiss-card' }, [ + E('div', { 'class': 'kiss-card-title' }, '🔗 Quick Links'), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px;' }, + QUICK_LINKS.map(function(link) { + return E('a', { + 'href': '/cgi-bin/luci/' + link.path, + 'class': 'kiss-btn', + 'style': 'text-decoration: none;' + }, link.icon + ' ' + link.name); + }) + ) ]) ]); }, + renderPillar: function(pillar, data) { + var enabled = data.enabled || false; + var running = data.running !== undefined ? data.running : enabled; + var statusColor = running ? 'var(--kiss-green)' : 'var(--kiss-red)'; + var statusText = running ? 'Active' : 'Inactive'; + + var stats = []; + switch(pillar.id) { + case 'mitm': + stats.push('Threats: ' + (data.threats_today || 0)); + stats.push('Connections: ' + (data.active_connections || 0)); + break; + case 'cdn_cache': + stats.push('Hit Ratio: ' + (data.hit_ratio || 0) + '%'); + if (data.offline_mode) stats.push('⚠️ OFFLINE'); + break; + case 'cookie_tracker': + stats.push('Cookies: ' + (data.total_cookies || 0)); + stats.push('Trackers: ' + (data.trackers_detected || 0)); + break; + case 'wpad': + if (data.dhcp_configured) stats.push('DHCP: ✓'); + if (data.enforce_enabled) stats.push('Enforce: ✓'); + break; + case 'api_failover': + stats.push('Stale serves: ' + (data.stale_serves || 0)); + break; + } + + return E('div', { 'class': 'kiss-card', 'style': 'text-align: center; border-left: 3px solid ' + statusColor + ';' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, pillar.icon), + E('div', { 'style': 'font-weight: 700; font-size: 14px;' }, pillar.name), + E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-bottom: 8px;' }, pillar.desc), + E('div', { 'style': 'font-size: 11px; color: ' + statusColor + '; font-weight: 600;' }, statusText), + stats.length ? E('div', { 'style': 'font-size: 10px; color: var(--kiss-muted); margin-top: 8px;' }, + stats.join(' • ')) : null + ]); + }, + + scoreColor: function(score) { + if (score >= 80) return 'var(--kiss-green)'; + if (score >= 50) return 'var(--kiss-yellow)'; + return 'var(--kiss-red)'; + }, + + kissMode: true, + + injectCSS: function() { + var self = this; + if (document.querySelector('#kiss-interceptor-css')) return; + + var css = ` +:root { + --kiss-bg: #0a0e17; --kiss-bg2: #111827; --kiss-card: #161e2e; + --kiss-line: #1e293b; --kiss-text: #e2e8f0; --kiss-muted: #94a3b8; + --kiss-green: #00C853; --kiss-red: #FF1744; --kiss-blue: #2979FF; + --kiss-cyan: #22d3ee; --kiss-yellow: #fbbf24; +} +.kiss-root { + background: var(--kiss-bg); color: var(--kiss-text); + font-family: 'Segoe UI', sans-serif; min-height: 100vh; padding: 20px; +} +.kiss-card { + background: var(--kiss-card); border: 1px solid var(--kiss-line); + border-radius: 12px; padding: 20px; margin-bottom: 16px; +} +.kiss-card-title { font-weight: 700; font-size: 16px; margin-bottom: 12px; } +.kiss-grid { display: grid; gap: 16px; } +.kiss-grid-auto { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); } +.kiss-btn { + padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; + cursor: pointer; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); + color: var(--kiss-text); transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; +} +.kiss-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); } +.kiss-panel-red { border-left: 3px solid var(--kiss-red); } +#kiss-toggle { + position: fixed; top: 10px; right: 10px; z-index: 99999; + font-size: 32px; cursor: pointer; opacity: 0.7; transition: all 0.3s; + background: rgba(0,0,0,0.5); border-radius: 50%; width: 50px; height: 50px; + display: flex; align-items: center; justify-content: center; +} +#kiss-toggle:hover { opacity: 1; transform: scale(1.1); } +@media (max-width: 600px) { .kiss-grid-auto { grid-template-columns: 1fr 1fr; } } +`; + var style = document.createElement('style'); + style.id = 'kiss-interceptor-css'; + style.textContent = css; + document.head.appendChild(style); + + // Add toggle button + if (!document.querySelector('#kiss-toggle')) { + var toggle = document.createElement('div'); + toggle.id = 'kiss-toggle'; + toggle.innerHTML = '👁️'; + toggle.title = 'Toggle KISS/LuCI mode'; + toggle.onclick = function() { self.toggleMode(); }; + document.body.appendChild(toggle); + } + + // Hide LuCI chrome + this.hideChrome(); + }, + + hideChrome: function() { + document.body.style.background = 'var(--kiss-bg)'; + ['#mainmenu', '.main-left', 'header', 'footer', '#topmenu', '#tabmenu'].forEach(function(sel) { + var el = document.querySelector(sel); + if (el) el.style.display = 'none'; + }); + var main = document.querySelector('.main-right') || document.querySelector('#maincontent'); + if (main) { main.style.marginLeft = '0'; main.style.padding = '0'; main.style.maxWidth = 'none'; } + }, + + showChrome: function() { + document.body.style.background = ''; + ['#mainmenu', '.main-left', 'header', 'footer', '#topmenu', '#tabmenu'].forEach(function(sel) { + var el = document.querySelector(sel); + if (el) el.style.display = ''; + }); + var main = document.querySelector('.main-right') || document.querySelector('#maincontent'); + if (main) { main.style.marginLeft = ''; main.style.padding = ''; main.style.maxWidth = ''; } + }, + + toggleMode: function() { + this.kissMode = !this.kissMode; + var toggle = document.getElementById('kiss-toggle'); + if (this.kissMode) { + toggle.innerHTML = '👁️'; + this.hideChrome(); + } else { + toggle.innerHTML = '👁️‍🗨️'; + this.showChrome(); + } + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js new file mode 100644 index 00000000..4bf3aff8 --- /dev/null +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js @@ -0,0 +1,289 @@ +'use strict'; + +/** + * SecuBox KISS Theme - Shared styling for all LuCI dashboards + * + * Usage in any LuCI view: + * 'require secubox/kiss-theme'; + * return view.extend({ + * render: function() { + * KissTheme.apply(); // Injects CSS and hides LuCI chrome + * return E('div', { 'class': 'kiss-root' }, [...]); + * } + * }); + */ + +window.KissTheme = window.KissTheme || { + // Core palette + colors: { + bg: '#0a0e17', + bg2: '#111827', + card: '#161e2e', + cardHover: '#1c2640', + line: '#1e293b', + text: '#e2e8f0', + muted: '#94a3b8', + green: '#00C853', + greenGlow: '#00E676', + red: '#FF1744', + blue: '#2979FF', + blueGlow: '#448AFF', + cyan: '#22d3ee', + purple: '#a78bfa', + orange: '#fb923c', + pink: '#f472b6', + yellow: '#fbbf24' + }, + + // CSS generation + generateCSS: function() { + var c = this.colors; + return ` +/* SecuBox KISS Theme */ +:root { + --kiss-bg: ${c.bg}; --kiss-bg2: ${c.bg2}; --kiss-card: ${c.card}; + --kiss-line: ${c.line}; --kiss-text: ${c.text}; --kiss-muted: ${c.muted}; + --kiss-green: ${c.green}; --kiss-red: ${c.red}; --kiss-blue: ${c.blue}; + --kiss-cyan: ${c.cyan}; --kiss-purple: ${c.purple}; --kiss-orange: ${c.orange}; + --kiss-yellow: ${c.yellow}; +} +.kiss-root { + background: var(--kiss-bg); color: var(--kiss-text); + font-family: 'Outfit', 'Segoe UI', sans-serif; + min-height: 100vh; padding: 20px; +} +.kiss-root * { box-sizing: border-box; } +/* Cards */ +.kiss-card { + background: var(--kiss-card); border: 1px solid var(--kiss-line); + border-radius: 12px; padding: 20px; margin-bottom: 16px; + transition: all 0.2s; +} +.kiss-card:hover { border-color: rgba(0,200,83,0.2); } +.kiss-card-title { + font-weight: 700; font-size: 16px; margin-bottom: 12px; + display: flex; align-items: center; gap: 8px; +} +/* Grid */ +.kiss-grid { display: grid; gap: 16px; } +.kiss-grid-2 { grid-template-columns: repeat(2, 1fr); } +.kiss-grid-3 { grid-template-columns: repeat(3, 1fr); } +.kiss-grid-4 { grid-template-columns: repeat(4, 1fr); } +.kiss-grid-auto { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } +/* Stats */ +.kiss-stat { + background: var(--kiss-card); border: 1px solid var(--kiss-line); + border-radius: 10px; padding: 16px; text-align: center; +} +.kiss-stat-value { + font-family: 'Orbitron', monospace; font-weight: 700; + font-size: 28px; color: var(--kiss-green); +} +.kiss-stat-label { + font-size: 11px; color: var(--kiss-muted); text-transform: uppercase; + letter-spacing: 1px; margin-top: 4px; +} +/* Buttons */ +.kiss-btn { + padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; + cursor: pointer; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); + color: var(--kiss-text); transition: all 0.2s; display: inline-flex; + align-items: center; gap: 6px; text-decoration: none; +} +.kiss-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); } +.kiss-btn-green { border-color: var(--kiss-green); color: var(--kiss-green); } +.kiss-btn-red { border-color: var(--kiss-red); color: var(--kiss-red); } +.kiss-btn-blue { border-color: var(--kiss-blue); color: var(--kiss-blue); } +/* Status badges */ +.kiss-badge { + font-family: monospace; font-size: 10px; letter-spacing: 1px; + padding: 4px 10px; border-radius: 4px; display: inline-block; +} +.kiss-badge-green { color: var(--kiss-green); background: rgba(0,200,83,0.1); border: 1px solid rgba(0,200,83,0.25); } +.kiss-badge-red { color: var(--kiss-red); background: rgba(255,23,68,0.1); border: 1px solid rgba(255,23,68,0.25); } +.kiss-badge-blue { color: var(--kiss-blue); background: rgba(41,121,255,0.1); border: 1px solid rgba(41,121,255,0.25); } +.kiss-badge-yellow { color: var(--kiss-yellow); background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.25); } +/* Tables */ +.kiss-table { width: 100%; border-collapse: separate; border-spacing: 0; } +.kiss-table th { + text-align: left; padding: 10px 16px; font-size: 11px; letter-spacing: 1px; + text-transform: uppercase; color: var(--kiss-muted); font-family: monospace; + border-bottom: 1px solid var(--kiss-line); +} +.kiss-table td { padding: 10px 16px; font-size: 14px; border-bottom: 1px solid rgba(255,255,255,0.03); } +.kiss-table tr:hover td { background: rgba(255,255,255,0.02); } +/* Progress bars */ +.kiss-progress { height: 8px; background: rgba(255,255,255,0.06); border-radius: 4px; overflow: hidden; } +.kiss-progress-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--kiss-green), var(--kiss-cyan)); } +/* Panels with border accent */ +.kiss-panel-green { border-left: 3px solid var(--kiss-green); } +.kiss-panel-red { border-left: 3px solid var(--kiss-red); } +.kiss-panel-blue { border-left: 3px solid var(--kiss-blue); } +.kiss-panel-orange { border-left: 3px solid var(--kiss-orange); } +/* Responsive */ +@media (max-width: 768px) { + .kiss-grid-2, .kiss-grid-3, .kiss-grid-4 { grid-template-columns: 1fr; } +} +`; + }, + + // State + isKissMode: true, + + // Apply theme to page + apply: function(options) { + options = options || {}; + + // Inject CSS if not already done + if (!document.querySelector('#kiss-theme-css')) { + var style = document.createElement('style'); + style.id = 'kiss-theme-css'; + style.textContent = this.generateCSS(); + document.head.appendChild(style); + } + + // Add toggle button + this.addToggle(); + + // Hide LuCI chrome if requested (default: true) + if (options.hideChrome !== false) { + this.hideChrome(); + } + }, + + // Add eye toggle button + addToggle: function() { + var self = this; + if (document.querySelector('#kiss-toggle')) return; + + var toggle = document.createElement('div'); + toggle.id = 'kiss-toggle'; + toggle.innerHTML = '👁️'; + toggle.title = 'Toggle KISS/LuCI mode'; + toggle.style.cssText = 'position:fixed;top:10px;right:10px;z-index:99999;' + + 'font-size:32px;cursor:pointer;opacity:0.7;transition:all 0.3s;' + + 'background:rgba(0,0,0,0.5);border-radius:50%;width:50px;height:50px;' + + 'display:flex;align-items:center;justify-content:center;'; + + toggle.onmouseover = function() { this.style.opacity = '1'; this.style.transform = 'scale(1.1)'; }; + toggle.onmouseout = function() { this.style.opacity = '0.7'; this.style.transform = 'scale(1)'; }; + toggle.onclick = function() { self.toggleMode(); }; + + document.body.appendChild(toggle); + }, + + // Toggle between KISS and LuCI mode + toggleMode: function() { + this.isKissMode = !this.isKissMode; + var toggle = document.getElementById('kiss-toggle'); + + if (this.isKissMode) { + // KISS mode - hide chrome + toggle.innerHTML = '👁️'; + this.hideChrome(); + document.body.classList.add('kiss-mode'); + } else { + // LuCI mode - show chrome + toggle.innerHTML = '👁️‍🗨️'; + this.showChrome(); + document.body.classList.remove('kiss-mode'); + } + }, + + // Show LuCI chrome + showChrome: function() { + [ + '#mainmenu', '.main-left', 'header.main-header', 'header', + 'nav[role="navigation"]', 'aside', 'footer', '.container > header', + '.pull-right', '#indicators', '.brand', '#topmenu', '#tabmenu' + ].forEach(function(sel) { + var el = document.querySelector(sel); + if (el) el.style.display = ''; + }); + var main = document.querySelector('.main-right') || document.querySelector('#maincontent') || document.querySelector('.container'); + if (main) { + main.style.marginLeft = ''; + main.style.marginTop = ''; + main.style.width = ''; + main.style.padding = ''; + main.style.maxWidth = ''; + } + document.body.style.padding = ''; + document.body.style.margin = ''; + }, + + // Hide LuCI navigation chrome + hideChrome: function() { + document.body.classList.add('kiss-mode'); + [ + '#mainmenu', '.main-left', 'header.main-header', 'header', + 'nav[role="navigation"]', 'aside', 'footer', '.container > header', + '.pull-right', '#indicators', '.brand', '#topmenu', '#tabmenu' + ].forEach(function(sel) { + var el = document.querySelector(sel); + if (el) el.style.display = 'none'; + }); + var main = document.querySelector('.main-right') || document.querySelector('#maincontent') || document.querySelector('.container'); + if (main) { + main.style.marginLeft = '0'; + main.style.marginTop = '0'; + main.style.width = '100%'; + main.style.padding = '0'; + main.style.maxWidth = 'none'; + } + document.body.style.padding = '0'; + document.body.style.margin = '0'; + }, + + // Helper: Create element with KISS classes + E: function(tag, attrs, children) { + var el = document.createElement(tag); + if (attrs) { + for (var k in attrs) { + if (k === 'class') { + el.className = attrs[k]; + } 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; + }, + + // Component helpers + card: function(title, content) { + return this.E('div', { 'class': 'kiss-card' }, [ + title ? this.E('div', { 'class': 'kiss-card-title' }, title) : null, + this.E('div', {}, content) + ]); + }, + + stat: function(value, label, color) { + var style = color ? 'color:' + color : ''; + return this.E('div', { 'class': 'kiss-stat' }, [ + this.E('div', { 'class': 'kiss-stat-value', 'style': style }, String(value)), + this.E('div', { 'class': 'kiss-stat-label' }, label) + ]); + }, + + badge: function(text, type) { + return this.E('span', { 'class': 'kiss-badge kiss-badge-' + (type || 'green') }, text); + }, + + btn: function(label, onClick, type) { + return this.E('button', { + 'class': 'kiss-btn' + (type ? ' kiss-btn-' + type : ''), + 'onClick': onClick + }, label); + } +}; + +return window.KissTheme;