From 31be5e07970a8fc0bd6d8592aa0e10fe3da55325 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 30 Jan 2026 09:16:50 +0100 Subject: [PATCH] feat(crowdsec-dashboard): Add extensible theming system Add theme manager with selectable themes and profiles: - classic: Professional SOC-style dark theme (default) - cards: Modern card-based UI with gradients - cyberpunk: Neon glow effects with terminal aesthetics Profiles extend themes with custom options: - default, soc, modern, hacker Theme selection available in Settings > Appearance with live preview. Co-Authored-By: Claude Opus 4.5 --- .../resources/crowdsec-dashboard/theme.js | 245 ++++++++++++ .../crowdsec-dashboard/themes/base.css | 120 ++++++ .../crowdsec-dashboard/themes/cards.css | 353 ++++++++++++++++++ .../crowdsec-dashboard/themes/classic.css | 240 ++++++++++++ .../crowdsec-dashboard/themes/cyberpunk.css | 306 +++++++++++++++ .../view/crowdsec-dashboard/overview.js | 154 ++++---- .../view/crowdsec-dashboard/settings.js | 249 +++++++----- .../root/etc/config/crowdsec-dashboard | 4 + 8 files changed, 1508 insertions(+), 163 deletions(-) create mode 100644 package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/theme.js create mode 100644 package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/base.css create mode 100644 package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cards.css create mode 100644 package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/classic.css create mode 100644 package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cyberpunk.css diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/theme.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/theme.js new file mode 100644 index 00000000..16423f90 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/theme.js @@ -0,0 +1,245 @@ +'use strict'; +'require baseclass'; +'require uci'; + +/** + * CrowdSec Dashboard Theme Manager + * Handles loading and switching between UI themes + * + * Available themes: + * - classic: Professional SOC-style dark theme (default) + * - cards: Modern card-based UI with gradients + * - cyberpunk: Neon glow effects with orange/cyan accents + * + * Profiles can extend themes with custom configurations + */ + +var ThemeManager = baseclass.extend({ + // Available themes + themes: { + 'classic': { + name: 'Classic SOC', + description: 'Professional Security Operations Center style', + css: 'themes/classic.css' + }, + 'cards': { + name: 'Modern Cards', + description: 'Card-based UI with gradients and shadows', + css: 'themes/cards.css' + }, + 'cyberpunk': { + name: 'Cyberpunk', + description: 'Neon glow effects with terminal aesthetics', + css: 'themes/cyberpunk.css' + } + }, + + // Theme profiles - extend base themes with custom settings + profiles: { + 'default': { + theme: 'classic', + options: {} + }, + 'soc': { + theme: 'classic', + options: { + fullwidth: true, + compactStats: false + } + }, + 'modern': { + theme: 'cards', + options: { + fullwidth: false, + animatedCards: true + } + }, + 'hacker': { + theme: 'cyberpunk', + options: { + fullwidth: true, + scanlines: true + } + } + }, + + currentTheme: null, + currentProfile: null, + + /** + * Initialize theme manager and load saved preferences + */ + init: function() { + var self = this; + return uci.load('crowdsec-dashboard').then(function() { + var theme = uci.get('crowdsec-dashboard', 'main', 'theme') || 'classic'; + var profile = uci.get('crowdsec-dashboard', 'main', 'profile') || 'default'; + return self.loadTheme(theme, profile); + }).catch(function() { + // Default to classic if config fails + return self.loadTheme('classic', 'default'); + }); + }, + + /** + * Load a theme and apply it to the dashboard + * @param {string} themeName - Theme identifier + * @param {string} profileName - Optional profile to apply + */ + loadTheme: function(themeName, profileName) { + var theme = this.themes[themeName] || this.themes['classic']; + var profile = this.profiles[profileName] || this.profiles['default']; + + // If profile specifies a different theme, use that + if (profile.theme && this.themes[profile.theme]) { + themeName = profile.theme; + theme = this.themes[themeName]; + } + + this.currentTheme = themeName; + this.currentProfile = profileName; + + // Load base CSS first + this.loadCSS('themes/base.css'); + + // Load theme-specific CSS + this.loadCSS(theme.css); + + // Apply theme class to body + document.body.classList.remove('theme-classic', 'theme-cards', 'theme-cyberpunk'); + document.body.classList.add('theme-' + themeName); + + // Apply profile options + if (profile.options) { + if (profile.options.fullwidth) { + document.body.classList.add('cs-fullwidth'); + } + } + + return Promise.resolve(); + }, + + /** + * Load a CSS file + * @param {string} path - Path relative to crowdsec-dashboard resources + */ + loadCSS: function(path) { + var fullPath = L.resource('crowdsec-dashboard/' + path); + + // Check if already loaded + var existing = document.querySelector('link[href="' + fullPath + '"]'); + if (existing) return; + + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = fullPath; + document.head.appendChild(link); + }, + + /** + * Switch to a different theme + * @param {string} themeName - Theme to switch to + */ + switchTheme: function(themeName) { + if (!this.themes[themeName]) { + console.warn('Unknown theme: ' + themeName); + return; + } + + // Remove old theme CSS + document.querySelectorAll('link[href*="themes/"]').forEach(function(el) { + if (!el.href.includes('base.css')) { + el.remove(); + } + }); + + return this.loadTheme(themeName, this.currentProfile); + }, + + /** + * Switch to a different profile + * @param {string} profileName - Profile to switch to + */ + switchProfile: function(profileName) { + if (!this.profiles[profileName]) { + console.warn('Unknown profile: ' + profileName); + return; + } + + var profile = this.profiles[profileName]; + return this.loadTheme(profile.theme, profileName); + }, + + /** + * Save current theme/profile to UCI config + */ + save: function() { + uci.set('crowdsec-dashboard', 'main', 'theme', this.currentTheme); + uci.set('crowdsec-dashboard', 'main', 'profile', this.currentProfile); + return uci.save(); + }, + + /** + * Get list of available themes + */ + getThemes: function() { + return Object.keys(this.themes).map(function(id) { + return { + id: id, + name: this.themes[id].name, + description: this.themes[id].description + }; + }, this); + }, + + /** + * Get list of available profiles + */ + getProfiles: function() { + return Object.keys(this.profiles).map(function(id) { + var p = this.profiles[id]; + return { + id: id, + theme: p.theme, + options: p.options + }; + }, this); + }, + + /** + * Register a custom theme + * @param {string} id - Theme identifier + * @param {object} config - Theme configuration + */ + registerTheme: function(id, config) { + if (this.themes[id]) { + console.warn('Theme already exists: ' + id); + return false; + } + this.themes[id] = config; + return true; + }, + + /** + * Register a custom profile + * @param {string} id - Profile identifier + * @param {object} config - Profile configuration + */ + registerProfile: function(id, config) { + if (this.profiles[id]) { + console.warn('Profile already exists: ' + id); + return false; + } + this.profiles[id] = config; + return true; + }, + + /** + * Get the CSS class for the dashboard container + */ + getDashboardClass: function() { + return 'cs-dashboard theme-' + (this.currentTheme || 'classic'); + } +}); + +return new ThemeManager(); diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/base.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/base.css new file mode 100644 index 00000000..b33cc5e8 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/base.css @@ -0,0 +1,120 @@ +/* CrowdSec Dashboard - Base Theme Variables */ +/* Shared foundation for all themes */ + +:root { + /* Base colors - can be overridden by theme */ + --cs-bg-primary: #0d1117; + --cs-bg-secondary: #161b22; + --cs-bg-tertiary: #21262d; + --cs-border: #30363d; + --cs-text: #c9d1d9; + --cs-text-muted: #8b949e; + --cs-text-heading: #ffffff; + + /* Accent colors */ + --cs-accent: #58a6ff; + --cs-success: #3fb950; + --cs-warning: #d29922; + --cs-danger: #f85149; + --cs-info: #79c0ff; + + /* Spacing */ + --cs-space-xs: 4px; + --cs-space-sm: 8px; + --cs-space-md: 16px; + --cs-space-lg: 24px; + --cs-space-xl: 32px; + + /* Border radius */ + --cs-radius-sm: 4px; + --cs-radius-md: 6px; + --cs-radius-lg: 8px; + --cs-radius-xl: 12px; + + /* Typography */ + --cs-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --cs-font-mono: 'JetBrains Mono', Consolas, monospace; + --cs-font-size-xs: 10px; + --cs-font-size-sm: 12px; + --cs-font-size-md: 14px; + --cs-font-size-lg: 16px; + --cs-font-size-xl: 20px; + --cs-font-size-2xl: 28px; + + /* Shadows */ + --cs-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --cs-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); + --cs-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3); + + /* Transitions */ + --cs-transition-fast: 0.15s ease; + --cs-transition-normal: 0.25s ease; +} + +/* Base resets */ +.cs-dashboard { + min-height: 100vh; + font-family: var(--cs-font-sans); + color: var(--cs-text); + background: var(--cs-bg-primary); +} + +.cs-dashboard * { + box-sizing: border-box; +} + +/* Hide LuCI sidebar for full-width view */ +body.cs-fullwidth #maincontainer > .pull-left, +body.cs-fullwidth #mainmenu { display: none !important; } +body.cs-fullwidth #maincontent { margin: 0 !important; width: 100% !important; } + +/* Common utility classes */ +.cs-text-muted { color: var(--cs-text-muted); } +.cs-text-success { color: var(--cs-success); } +.cs-text-warning { color: var(--cs-warning); } +.cs-text-danger { color: var(--cs-danger); } +.cs-text-accent { color: var(--cs-accent); } + +.cs-mono { font-family: var(--cs-font-mono); } +.cs-uppercase { text-transform: uppercase; letter-spacing: 0.5px; } + +/* Loading spinner */ +.cs-spinner { + width: 20px; + height: 20px; + border: 2px solid var(--cs-border); + border-top-color: var(--cs-accent); + border-radius: 50%; + animation: cs-spin 0.8s linear infinite; +} + +@keyframes cs-spin { to { transform: rotate(360deg); } } + +/* Pulse animation */ +@keyframes cs-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Toast notifications */ +.cs-toast { + position: fixed; + bottom: 20px; + right: 20px; + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + padding: 12px 20px; + border-radius: var(--cs-radius-md); + font-size: var(--cs-font-size-sm); + z-index: 9999; + animation: cs-slideIn 0.3s ease; +} + +.cs-toast.success { border-left: 3px solid var(--cs-success); } +.cs-toast.error { border-left: 3px solid var(--cs-danger); } +.cs-toast.warning { border-left: 3px solid var(--cs-warning); } + +@keyframes cs-slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cards.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cards.css new file mode 100644 index 00000000..768fce34 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cards.css @@ -0,0 +1,353 @@ +/* CrowdSec Dashboard - Cards Theme */ +/* Modern card-based UI with rounded corners and shadows */ + +/* Theme-specific variables */ +.cs-dashboard.theme-cards { + --cs-bg-primary: #0f0f0f; + --cs-bg-secondary: #1a1a1a; + --cs-bg-tertiary: #252525; + --cs-border: #333333; + --cs-accent: #6366f1; + --cs-radius-md: 12px; + --cs-radius-lg: 16px; + --cs-radius-xl: 20px; +} + +.theme-cards { + padding: var(--cs-space-lg); +} + +/* Header - Minimal centered */ +.theme-cards .cs-header { + text-align: center; + padding: var(--cs-space-xl) 0; + margin-bottom: var(--cs-space-xl); +} + +.theme-cards .cs-title { + font-size: 28px; + font-weight: 700; + display: inline-flex; + align-items: center; + gap: var(--cs-space-sm); + background: linear-gradient(135deg, var(--cs-accent), #a855f7); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.theme-cards .cs-title svg { + width: 32px; + height: 32px; + fill: var(--cs-accent); +} + +.theme-cards .cs-status { + display: inline-flex; + align-items: center; + gap: var(--cs-space-sm); + margin-top: var(--cs-space-sm); + padding: var(--cs-space-xs) var(--cs-space-md); + background: var(--cs-bg-secondary); + border-radius: 20px; + font-size: var(--cs-font-size-xs); +} + +.theme-cards .cs-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + animation: cs-pulse 2s infinite; +} + +.theme-cards .cs-status-dot.online { background: var(--cs-success); box-shadow: 0 0 10px var(--cs-success); } +.theme-cards .cs-status-dot.offline { background: var(--cs-danger); animation: none; } + +/* Navigation - Pill style */ +.theme-cards .cs-nav { + display: flex; + justify-content: center; + gap: var(--cs-space-xs); + margin-bottom: var(--cs-space-xl); + padding: var(--cs-space-xs); + background: var(--cs-bg-secondary); + border-radius: 24px; + width: fit-content; + margin-left: auto; + margin-right: auto; +} + +.theme-cards .cs-nav a { + color: var(--cs-text-muted); + text-decoration: none; + padding: var(--cs-space-sm) var(--cs-space-lg); + border-radius: 20px; + font-size: var(--cs-font-size-sm); + font-weight: 500; + transition: all var(--cs-transition-normal); +} + +.theme-cards .cs-nav a:hover { + color: var(--cs-text); +} + +.theme-cards .cs-nav a.active { + color: #fff; + background: linear-gradient(135deg, var(--cs-accent), #a855f7); +} + +/* Stats - Large cards with icons */ +.theme-cards .cs-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--cs-space-md); + margin-bottom: var(--cs-space-xl); +} + +.theme-cards .cs-stat { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + border-radius: var(--cs-radius-lg); + padding: var(--cs-space-lg); + text-align: left; + position: relative; + overflow: hidden; + transition: all var(--cs-transition-normal); +} + +.theme-cards .cs-stat:hover { + transform: translateY(-2px); + box-shadow: var(--cs-shadow-lg); + border-color: var(--cs-accent); +} + +.theme-cards .cs-stat::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 80px; + height: 80px; + background: linear-gradient(135deg, transparent 50%, rgba(99, 102, 241, 0.1)); + border-radius: 0 var(--cs-radius-lg) 0 80px; +} + +.theme-cards .cs-stat-value { + font-size: 36px; + font-weight: 800; + font-family: var(--cs-font-mono); + background: linear-gradient(135deg, var(--cs-accent), #a855f7); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.theme-cards .cs-stat-label { + font-size: var(--cs-font-size-sm); + color: var(--cs-text-muted); + margin-top: var(--cs-space-xs); + font-weight: 500; +} + +.theme-cards .cs-stat.danger .cs-stat-value { + background: linear-gradient(135deg, var(--cs-danger), #ff6b6b); + -webkit-background-clip: text; + background-clip: text; +} + +.theme-cards .cs-stat.warning .cs-stat-value { + background: linear-gradient(135deg, var(--cs-warning), #fbbf24); + -webkit-background-clip: text; + background-clip: text; +} + +.theme-cards .cs-stat.success .cs-stat-value { + background: linear-gradient(135deg, var(--cs-success), #34d399); + -webkit-background-clip: text; + background-clip: text; +} + +/* Cards - Elevated with glow */ +.theme-cards .cs-card { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + border-radius: var(--cs-radius-xl); + margin-bottom: var(--cs-space-lg); + overflow: hidden; + transition: all var(--cs-transition-normal); +} + +.theme-cards .cs-card:hover { + border-color: rgba(99, 102, 241, 0.3); + box-shadow: 0 0 30px rgba(99, 102, 241, 0.1); +} + +.theme-cards .cs-card-header { + padding: var(--cs-space-md) var(--cs-space-lg); + background: linear-gradient(135deg, var(--cs-bg-tertiary), var(--cs-bg-secondary)); + font-size: var(--cs-font-size-md); + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; +} + +.theme-cards .cs-card-body { + padding: var(--cs-space-lg); +} + +/* Tables - Clean minimal */ +.theme-cards .cs-table { + width: 100%; + border-collapse: separate; + border-spacing: 0 var(--cs-space-xs); + font-size: var(--cs-font-size-sm); +} + +.theme-cards .cs-table th { + text-align: left; + padding: var(--cs-space-sm) var(--cs-space-md); + color: var(--cs-text-muted); + font-weight: 500; + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.theme-cards .cs-table td { + padding: var(--cs-space-md); + background: var(--cs-bg-tertiary); +} + +.theme-cards .cs-table tr td:first-child { border-radius: var(--cs-radius-md) 0 0 var(--cs-radius-md); } +.theme-cards .cs-table tr td:last-child { border-radius: 0 var(--cs-radius-md) var(--cs-radius-md) 0; } + +.theme-cards .cs-table tr:hover td { background: rgba(99, 102, 241, 0.1); } + +/* Grid layouts */ +.theme-cards .cs-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--cs-space-lg); +} + +@media (max-width: 900px) { + .theme-cards .cs-grid-2 { grid-template-columns: 1fr; } +} + +/* Health Check - Icon cards */ +.theme-cards .cs-health { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--cs-space-md); +} + +.theme-cards .cs-health-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: var(--cs-space-sm); + padding: var(--cs-space-lg); + background: var(--cs-bg-tertiary); + border-radius: var(--cs-radius-lg); + transition: all var(--cs-transition-normal); +} + +.theme-cards .cs-health-item:hover { + transform: translateY(-2px); +} + +.theme-cards .cs-health-icon { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.theme-cards .cs-health-icon.ok { + background: linear-gradient(135deg, rgba(63, 185, 80, 0.2), rgba(52, 211, 153, 0.2)); + color: var(--cs-success); +} +.theme-cards .cs-health-icon.error { + background: linear-gradient(135deg, rgba(248, 81, 73, 0.2), rgba(255, 107, 107, 0.2)); + color: var(--cs-danger); +} + +.theme-cards .cs-health-label { + font-size: var(--cs-font-size-xs); + color: var(--cs-text-muted); +} + +.theme-cards .cs-health-value { + font-size: var(--cs-font-size-md); + font-weight: 600; +} + +/* Empty state */ +.theme-cards .cs-empty { + text-align: center; + padding: var(--cs-space-xl); + color: var(--cs-text-muted); +} + +.theme-cards .cs-empty-icon { + font-size: 48px; + margin-bottom: var(--cs-space-md); + opacity: 0.5; +} + +/* Buttons - Pill style */ +.theme-cards .cs-btn { + background: var(--cs-bg-tertiary); + border: 1px solid var(--cs-border); + color: var(--cs-text); + padding: 8px 16px; + border-radius: 20px; + font-size: var(--cs-font-size-sm); + font-weight: 500; + cursor: pointer; + transition: all var(--cs-transition-normal); +} + +.theme-cards .cs-btn:hover { + background: var(--cs-accent); + border-color: var(--cs-accent); + color: #fff; +} + +.theme-cards .cs-btn-sm { + padding: 4px 12px; + font-size: var(--cs-font-size-xs); +} + +/* Geo grid */ +.theme-cards .cs-geo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: var(--cs-space-sm); +} + +.theme-cards .cs-geo-item { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--cs-space-md); + background: var(--cs-bg-tertiary); + border-radius: var(--cs-radius-md); + transition: all var(--cs-transition-fast); +} + +.theme-cards .cs-geo-item:hover { + background: rgba(99, 102, 241, 0.1); +} + +.theme-cards .cs-flag { font-size: 24px; } +.theme-cards .cs-geo-count { + font-weight: 700; + font-family: var(--cs-font-mono); + margin-top: var(--cs-space-xs); +} diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/classic.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/classic.css new file mode 100644 index 00000000..8f099bf4 --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/classic.css @@ -0,0 +1,240 @@ +/* CrowdSec Dashboard - Classic Theme */ +/* Professional SOC-style dark theme */ + +/* Theme-specific variables */ +.cs-dashboard.theme-classic { + --cs-bg-primary: #0d1117; + --cs-bg-secondary: #161b22; + --cs-bg-tertiary: #21262d; + --cs-border: #30363d; + --cs-accent: #58a6ff; +} + +/* Header */ +.theme-classic .cs-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--cs-space-md); + border-bottom: 1px solid var(--cs-border); + margin-bottom: var(--cs-space-lg); +} + +.theme-classic .cs-title { + font-size: var(--cs-font-size-xl); + font-weight: 600; + display: flex; + align-items: center; + gap: var(--cs-space-sm); +} + +.theme-classic .cs-title svg { + width: 28px; + height: 28px; + fill: var(--cs-accent); +} + +.theme-classic .cs-status { + display: flex; + align-items: center; + gap: var(--cs-space-sm); + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.theme-classic .cs-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + animation: cs-pulse 2s infinite; +} + +.theme-classic .cs-status-dot.online { background: var(--cs-success); } +.theme-classic .cs-status-dot.offline { background: var(--cs-danger); animation: none; } + +/* Navigation */ +.theme-classic .cs-nav { + display: flex; + gap: var(--cs-space-xs); + margin-bottom: var(--cs-space-lg); + border-bottom: 1px solid var(--cs-border); + padding-bottom: var(--cs-space-sm); +} + +.theme-classic .cs-nav a { + color: var(--cs-text-muted); + text-decoration: none; + padding: var(--cs-space-sm) var(--cs-space-md); + border-radius: var(--cs-radius-sm); + font-size: var(--cs-font-size-sm); + transition: all var(--cs-transition-fast); +} + +.theme-classic .cs-nav a:hover { + color: var(--cs-text); + background: var(--cs-bg-secondary); +} + +.theme-classic .cs-nav a.active { + color: var(--cs-accent); + background: rgba(88, 166, 255, 0.1); +} + +/* Stats Grid */ +.theme-classic .cs-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: var(--cs-space-sm); + margin-bottom: var(--cs-space-lg); +} + +.theme-classic .cs-stat { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + border-radius: var(--cs-radius-md); + padding: var(--cs-space-md); + text-align: center; +} + +.theme-classic .cs-stat-value { + font-size: var(--cs-font-size-2xl); + font-weight: 700; + font-family: var(--cs-font-mono); + color: var(--cs-accent); +} + +.theme-classic .cs-stat-label { + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--cs-text-muted); + margin-top: var(--cs-space-xs); +} + +.theme-classic .cs-stat.danger .cs-stat-value { color: var(--cs-danger); } +.theme-classic .cs-stat.warning .cs-stat-value { color: var(--cs-warning); } +.theme-classic .cs-stat.success .cs-stat-value { color: var(--cs-success); } + +/* Cards */ +.theme-classic .cs-card { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + border-radius: var(--cs-radius-md); + margin-bottom: var(--cs-space-md); +} + +.theme-classic .cs-card-header { + padding: var(--cs-space-sm) var(--cs-space-md); + border-bottom: 1px solid var(--cs-border); + font-size: var(--cs-font-size-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.theme-classic .cs-card-body { + padding: var(--cs-space-md); +} + +/* Tables */ +.theme-classic .cs-table { + width: 100%; + border-collapse: collapse; + font-size: var(--cs-font-size-sm); +} + +.theme-classic .cs-table th { + text-align: left; + padding: var(--cs-space-sm); + background: var(--cs-bg-primary); + color: var(--cs-text-muted); + font-weight: 500; + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid var(--cs-border); +} + +.theme-classic .cs-table td { + padding: var(--cs-space-sm); + border-bottom: 1px solid var(--cs-border); +} + +.theme-classic .cs-table tr:last-child td { border-bottom: none; } +.theme-classic .cs-table tr:hover { background: rgba(88, 166, 255, 0.05); } + +/* Grid layouts */ +.theme-classic .cs-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--cs-space-md); +} + +@media (max-width: 900px) { + .theme-classic .cs-grid-2 { grid-template-columns: 1fr; } +} + +/* Health Check */ +.theme-classic .cs-health { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--cs-space-sm); +} + +.theme-classic .cs-health-item { + display: flex; + align-items: center; + gap: var(--cs-space-sm); + padding: var(--cs-space-sm); + background: var(--cs-bg-primary); + border-radius: var(--cs-radius-sm); +} + +.theme-classic .cs-health-icon { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; +} + +.theme-classic .cs-health-icon.ok { background: rgba(63, 185, 80, 0.2); color: var(--cs-success); } +.theme-classic .cs-health-icon.error { background: rgba(248, 81, 73, 0.2); color: var(--cs-danger); } + +/* Empty state */ +.theme-classic .cs-empty { + text-align: center; + padding: var(--cs-space-xl); + color: var(--cs-text-muted); +} + +.theme-classic .cs-empty-icon { + font-size: 32px; + margin-bottom: var(--cs-space-sm); + opacity: 0.5; +} + +/* Buttons */ +.theme-classic .cs-btn { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + color: var(--cs-text); + padding: 6px 12px; + border-radius: var(--cs-radius-sm); + font-size: var(--cs-font-size-sm); + cursor: pointer; + transition: all var(--cs-transition-fast); +} + +.theme-classic .cs-btn:hover { + background: var(--cs-border); + border-color: var(--cs-text-muted); +} + +.theme-classic .cs-btn-sm { padding: 3px 8px; font-size: var(--cs-font-size-xs); } diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cyberpunk.css b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cyberpunk.css new file mode 100644 index 00000000..5cf7afcf --- /dev/null +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/themes/cyberpunk.css @@ -0,0 +1,306 @@ +/* CrowdSec Dashboard - Cyberpunk Theme */ +/* Neon glow effects with orange/cyan accents */ + +/* Theme-specific variables */ +.cs-dashboard.theme-cyberpunk { + --cs-bg-primary: #0a0a0f; + --cs-bg-secondary: #12121a; + --cs-bg-tertiary: #1a1a25; + --cs-border: #2a2a3a; + --cs-accent: #ff5f1f; + --cs-accent-alt: #00ffff; + --cs-success: #00ff88; + --cs-warning: #ffaa00; + --cs-danger: #ff3366; +} + +.theme-cyberpunk { + padding: var(--cs-space-lg); + background: linear-gradient(135deg, #0a0a0f 0%, #12121a 50%, #0a0a0f 100%); +} + +/* Scanline effect overlay */ +.theme-cyberpunk::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.1) 0px, + rgba(0, 0, 0, 0.1) 1px, + transparent 1px, + transparent 2px + ); + pointer-events: none; + z-index: 1000; + opacity: 0.3; +} + +/* Header - Neon glow */ +.theme-cyberpunk .cs-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--cs-space-lg); + margin-bottom: var(--cs-space-xl); + border-bottom: 2px solid var(--cs-accent); + box-shadow: 0 2px 20px rgba(255, 95, 31, 0.3); +} + +.theme-cyberpunk .cs-title { + font-size: 24px; + font-weight: 700; + color: var(--cs-accent); + text-shadow: 0 0 10px rgba(255, 95, 31, 0.5), 0 0 20px rgba(255, 95, 31, 0.3); + display: flex; + align-items: center; + gap: var(--cs-space-sm); + text-transform: uppercase; + letter-spacing: 2px; +} + +.theme-cyberpunk .cs-title svg { + width: 32px; + height: 32px; + fill: var(--cs-accent); + filter: drop-shadow(0 0 5px var(--cs-accent)); +} + +.theme-cyberpunk .cs-status { + display: flex; + align-items: center; + gap: var(--cs-space-sm); + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 1px; + font-family: var(--cs-font-mono); +} + +.theme-cyberpunk .cs-status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + animation: cs-pulse 1s infinite; +} + +.theme-cyberpunk .cs-status-dot.online { + background: var(--cs-success); + box-shadow: 0 0 10px var(--cs-success), 0 0 20px var(--cs-success); +} +.theme-cyberpunk .cs-status-dot.offline { + background: var(--cs-danger); + box-shadow: 0 0 10px var(--cs-danger); + animation: none; +} + +/* Navigation - Glitch style */ +.theme-cyberpunk .cs-nav { + display: flex; + gap: var(--cs-space-xs); + margin-bottom: var(--cs-space-xl); + padding-bottom: var(--cs-space-sm); + border-bottom: 1px solid var(--cs-border); +} + +.theme-cyberpunk .cs-nav a { + color: var(--cs-text-muted); + text-decoration: none; + padding: var(--cs-space-sm) var(--cs-space-lg); + font-size: var(--cs-font-size-sm); + font-family: var(--cs-font-mono); + text-transform: uppercase; + letter-spacing: 1px; + transition: all var(--cs-transition-fast); + border: 1px solid transparent; +} + +.theme-cyberpunk .cs-nav a:hover { + color: var(--cs-accent-alt); + border-color: var(--cs-accent-alt); + text-shadow: 0 0 5px var(--cs-accent-alt); +} + +.theme-cyberpunk .cs-nav a.active { + color: var(--cs-accent); + border-color: var(--cs-accent); + background: rgba(255, 95, 31, 0.1); + box-shadow: 0 0 10px rgba(255, 95, 31, 0.3); +} + +/* Stats - Holographic cards */ +.theme-cyberpunk .cs-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--cs-space-md); + margin-bottom: var(--cs-space-xl); +} + +.theme-cyberpunk .cs-stat { + background: linear-gradient(135deg, var(--cs-bg-secondary), var(--cs-bg-tertiary)); + border: 1px solid var(--cs-accent); + border-radius: var(--cs-radius-sm); + padding: var(--cs-space-lg); + text-align: center; + position: relative; + overflow: hidden; +} + +.theme-cyberpunk .cs-stat::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 95, 31, 0.1), transparent); + animation: scan 3s linear infinite; +} + +@keyframes scan { + to { left: 100%; } +} + +.theme-cyberpunk .cs-stat-value { + font-size: 32px; + font-weight: 700; + font-family: var(--cs-font-mono); + color: var(--cs-accent-alt); + text-shadow: 0 0 10px var(--cs-accent-alt); +} + +.theme-cyberpunk .cs-stat-label { + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 1px; + color: var(--cs-text-muted); + margin-top: var(--cs-space-sm); + font-family: var(--cs-font-mono); +} + +.theme-cyberpunk .cs-stat.danger .cs-stat-value { + color: var(--cs-danger); + text-shadow: 0 0 10px var(--cs-danger); +} + +.theme-cyberpunk .cs-stat.warning .cs-stat-value { + color: var(--cs-warning); + text-shadow: 0 0 10px var(--cs-warning); +} + +.theme-cyberpunk .cs-stat.success .cs-stat-value { + color: var(--cs-success); + text-shadow: 0 0 10px var(--cs-success); +} + +/* Cards - Terminal style */ +.theme-cyberpunk .cs-card { + background: var(--cs-bg-secondary); + border: 1px solid var(--cs-border); + border-radius: var(--cs-radius-sm); + margin-bottom: var(--cs-space-lg); +} + +.theme-cyberpunk .cs-card-header { + padding: var(--cs-space-sm) var(--cs-space-md); + background: var(--cs-bg-primary); + border-bottom: 1px solid var(--cs-accent); + font-size: var(--cs-font-size-sm); + font-weight: 600; + font-family: var(--cs-font-mono); + text-transform: uppercase; + letter-spacing: 1px; + color: var(--cs-accent); + display: flex; + justify-content: space-between; + align-items: center; +} + +.theme-cyberpunk .cs-card-header::before { + content: '>'; + margin-right: var(--cs-space-sm); + color: var(--cs-accent-alt); +} + +.theme-cyberpunk .cs-card-body { + padding: var(--cs-space-md); + font-family: var(--cs-font-mono); +} + +/* Tables - Matrix style */ +.theme-cyberpunk .cs-table { + width: 100%; + border-collapse: collapse; + font-size: var(--cs-font-size-sm); + font-family: var(--cs-font-mono); +} + +.theme-cyberpunk .cs-table th { + text-align: left; + padding: var(--cs-space-sm); + background: var(--cs-bg-primary); + color: var(--cs-accent); + font-weight: 500; + font-size: var(--cs-font-size-xs); + text-transform: uppercase; + letter-spacing: 1px; + border-bottom: 1px solid var(--cs-accent); +} + +.theme-cyberpunk .cs-table td { + padding: var(--cs-space-sm); + border-bottom: 1px solid var(--cs-border); + color: var(--cs-accent-alt); +} + +.theme-cyberpunk .cs-table tr:hover td { + background: rgba(0, 255, 255, 0.05); + text-shadow: 0 0 5px var(--cs-accent-alt); +} + +/* Grid layouts */ +.theme-cyberpunk .cs-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--cs-space-lg); +} + +@media (max-width: 900px) { + .theme-cyberpunk .cs-grid-2 { grid-template-columns: 1fr; } +} + +/* Buttons - Neon */ +.theme-cyberpunk .cs-btn { + background: transparent; + border: 1px solid var(--cs-accent); + color: var(--cs-accent); + padding: 6px 16px; + font-size: var(--cs-font-size-sm); + font-family: var(--cs-font-mono); + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + transition: all var(--cs-transition-fast); +} + +.theme-cyberpunk .cs-btn:hover { + background: var(--cs-accent); + color: #000; + box-shadow: 0 0 15px rgba(255, 95, 31, 0.5); +} + +/* Empty state */ +.theme-cyberpunk .cs-empty { + text-align: center; + padding: var(--cs-space-xl); + color: var(--cs-text-muted); + font-family: var(--cs-font-mono); +} + +.theme-cyberpunk .cs-empty-icon { + font-size: 40px; + margin-bottom: var(--cs-space-md); + filter: drop-shadow(0 0 10px var(--cs-accent)); +} diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index 1a059ea6..4f6dceea 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -3,55 +3,57 @@ 'require dom'; 'require poll'; 'require ui'; +'require uci'; 'require crowdsec-dashboard.api as api'; +'require crowdsec-dashboard.theme as theme'; /** * CrowdSec SOC Dashboard - Overview - * Minimal SOC-compliant design with GeoIP - * Version 1.0.0 + * Themeable SOC-compliant design with GeoIP + * Version 1.1.0 */ return view.extend({ title: _('CrowdSec SOC'), load: function() { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('crowdsec-dashboard/soc.css'); - document.head.appendChild(link); - document.body.classList.add('cs-soc-fullwidth'); - - return api.getOverview().catch(function() { return {}; }); + return Promise.all([ + theme.init(), + api.getOverview().catch(function() { return {}; }) + ]); }, render: function(data) { var self = this; - var status = data || {}; + var status = data[1] || {}; - var view = E('div', { 'class': 'soc-dashboard' }, [ + // Apply theme class + document.body.classList.add('cs-fullwidth'); + + var view = E('div', { 'class': theme.getDashboardClass() }, [ this.renderHeader(status), this.renderNav('overview'), - E('div', { 'id': 'soc-stats' }, this.renderStats(status)), - E('div', { 'class': 'soc-grid-2' }, [ - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, ['Recent Alerts', E('span', { 'class': 'soc-time' }, 'Last 24h')]), - E('div', { 'class': 'soc-card-body', 'id': 'recent-alerts' }, this.renderAlerts(status.alerts || [])) + E('div', { 'id': 'cs-stats' }, this.renderStats(status)), + E('div', { 'class': 'cs-grid-2' }, [ + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, ['Recent Alerts', E('span', { 'class': 'cs-time' }, 'Last 24h')]), + E('div', { 'class': 'cs-card-body', 'id': 'recent-alerts' }, this.renderAlerts(status.alerts || [])) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, 'Threat Origins'), - E('div', { 'class': 'soc-card-body', 'id': 'geo-dist' }, this.renderGeo(status.countries || {})) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Threat Origins'), + E('div', { 'class': 'cs-card-body', 'id': 'geo-dist' }, this.renderGeo(status.countries || {})) ]) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, [ + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ 'System Health', - E('button', { 'class': 'soc-btn soc-btn-sm', 'click': function() { self.runHealthCheck(); } }, 'Test') + E('button', { 'class': 'cs-btn cs-btn-sm', 'click': function() { self.runHealthCheck(); } }, 'Test') ]), - E('div', { 'class': 'soc-card-body', 'id': 'health-check' }, this.renderHealth(status)) + E('div', { 'class': 'cs-card-body', 'id': 'health-check' }, this.renderHealth(status)) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, 'Threat Types Blocked'), - E('div', { 'class': 'soc-card-body' }, this.renderThreatTypes(status.top_scenarios_raw)) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Threat Types Blocked'), + E('div', { 'class': 'cs-card-body' }, this.renderThreatTypes(status.top_scenarios_raw)) ]) ]); @@ -60,15 +62,15 @@ return view.extend({ }, renderHeader: function(s) { - return E('div', { 'class': 'soc-header' }, [ - E('div', { 'class': 'soc-title' }, [ + return E('div', { 'class': 'cs-header' }, [ + E('div', { 'class': 'cs-title' }, [ E('svg', { 'viewBox': '0 0 24 24' }, [ E('path', { 'd': 'M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18l6.9 3.45L12 11.09 5.1 7.63 12 4.18zM4 8.82l7 3.5v7.36l-7-3.5V8.82zm9 10.86v-7.36l7-3.5v7.36l-7 3.5z' }) ]), 'CrowdSec Security Operations' ]), - E('div', { 'class': 'soc-status' }, [ - E('span', { 'class': 'soc-status-dot ' + (s.crowdsec === 'running' ? 'online' : 'offline') }), + E('div', { 'class': 'cs-status' }, [ + E('span', { 'class': 'cs-status-dot ' + (s.crowdsec === 'running' ? 'online' : 'offline') }), s.crowdsec === 'running' ? 'OPERATIONAL' : 'OFFLINE' ]) ]); @@ -82,7 +84,7 @@ return view.extend({ { id: 'bouncers', label: 'Bouncers' }, { id: 'settings', label: 'Settings' } ]; - return E('div', { 'class': 'soc-nav' }, tabs.map(function(t) { + return E('div', { 'class': 'cs-nav' }, tabs.map(function(t) { return E('a', { 'href': L.url('admin/secubox/security/crowdsec/' + t.id), 'class': active === t.id ? 'active' : '' @@ -102,10 +104,10 @@ return view.extend({ { label: 'Local Bans', value: d.local_decisions || 0, type: (d.local_decisions || 0) > 0 ? 'warning' : '' }, { label: 'Bouncers', value: d.bouncer_count || 0, type: (d.bouncer_count || 0) > 0 ? 'success' : 'warning' } ]; - return E('div', { 'class': 'soc-stats' }, stats.map(function(s) { - return E('div', { 'class': 'soc-stat ' + s.type }, [ - E('div', { 'class': 'soc-stat-value' }, String(s.value)), - E('div', { 'class': 'soc-stat-label' }, s.label) + return E('div', { 'class': 'cs-stats' }, stats.map(function(s) { + return E('div', { 'class': 'cs-stat ' + s.type }, [ + E('div', { 'class': 'cs-stat-value' }, String(s.value)), + E('div', { 'class': 'cs-stat-label' }, s.label) ]); })); }, @@ -125,12 +127,12 @@ return view.extend({ renderAlerts: function(alerts) { if (!alerts || !alerts.length) { - return E('div', { 'class': 'soc-empty' }, [ - E('div', { 'class': 'soc-empty-icon' }, '\u2713'), + return E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '\u2713'), 'No recent alerts' ]); } - return E('table', { 'class': 'soc-table' }, [ + return E('table', { 'class': 'cs-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Time'), E('th', {}, 'Source'), @@ -142,12 +144,12 @@ return view.extend({ var ip = src.ip || a.source_ip || 'N/A'; var country = src.cn || src.country || ''; return E('tr', {}, [ - E('td', { 'class': 'soc-time' }, api.formatRelativeTime(a.created_at)), - E('td', {}, E('span', { 'class': 'soc-ip' }, ip)), - E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(a.scenario))), - E('td', { 'class': 'soc-geo' }, [ - E('span', { 'class': 'soc-flag' }, api.getCountryFlag(country)), - E('span', { 'class': 'soc-country' }, country) + E('td', { 'class': 'cs-time' }, api.formatRelativeTime(a.created_at)), + E('td', {}, E('span', { 'class': 'cs-ip' }, ip)), + E('td', {}, E('span', { 'class': 'cs-scenario' }, api.parseScenario(a.scenario))), + E('td', { 'class': 'cs-geo' }, [ + E('span', { 'class': 'cs-flag' }, api.getCountryFlag(country)), + E('span', { 'class': 'cs-country' }, country) ]) ]); })) @@ -157,17 +159,17 @@ return view.extend({ renderGeo: function(countries) { var entries = Object.entries(countries || {}); if (!entries.length) { - return E('div', { 'class': 'soc-empty' }, [ - E('div', { 'class': 'soc-empty-icon' }, '\u{1F30D}'), + return E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '\u{1F30D}'), 'No geographic data' ]); } entries.sort(function(a, b) { return b[1] - a[1]; }); - return E('div', { 'class': 'soc-geo-grid' }, entries.slice(0, 12).map(function(e) { - return E('div', { 'class': 'soc-geo-item' }, [ - E('span', { 'class': 'soc-flag' }, api.getCountryFlag(e[0])), - E('span', { 'class': 'soc-geo-count' }, String(e[1])), - E('span', { 'class': 'soc-country' }, e[0]) + return E('div', { 'class': 'cs-geo-grid' }, entries.slice(0, 12).map(function(e) { + return E('div', { 'class': 'cs-geo-item' }, [ + E('span', { 'class': 'cs-flag' }, api.getCountryFlag(e[0])), + E('span', { 'class': 'cs-geo-count' }, String(e[1])), + E('span', { 'class': 'cs-country' }, e[0]) ]); })); }, @@ -181,12 +183,12 @@ return view.extend({ { label: 'GeoIP', value: d.geoip_enabled ? 'Enabled' : 'Disabled', ok: d.geoip_enabled }, { label: 'Acquisition', value: (d.acquisition_count || 0) + ' sources', ok: (d.acquisition_count || 0) > 0 } ]; - return E('div', { 'class': 'soc-health' }, checks.map(function(c) { - return E('div', { 'class': 'soc-health-item' }, [ - E('div', { 'class': 'soc-health-icon ' + (c.ok ? 'ok' : 'error') }, c.ok ? '\u2713' : '\u2717'), + return E('div', { 'class': 'cs-health' }, checks.map(function(c) { + return E('div', { 'class': 'cs-health-item' }, [ + E('div', { 'class': 'cs-health-icon ' + (c.ok ? 'ok' : 'error') }, c.ok ? '\u2713' : '\u2717'), E('div', {}, [ - E('div', { 'class': 'soc-health-label' }, c.label), - E('div', { 'class': 'soc-health-value' }, c.value) + E('div', { 'class': 'cs-health-label' }, c.label), + E('div', { 'class': 'cs-health-value' }, c.value) ]) ]); })); @@ -194,9 +196,9 @@ return view.extend({ renderScenarios: function(scenarios) { if (!scenarios || !scenarios.length) { - return E('div', { 'class': 'soc-empty' }, 'No scenarios loaded'); + return E('div', { 'class': 'cs-empty' }, 'No scenarios loaded'); } - return E('table', { 'class': 'soc-table' }, [ + return E('table', { 'class': 'cs-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Scenario'), E('th', {}, 'Status'), @@ -207,9 +209,9 @@ return view.extend({ var enabled = !s.status || s.status.includes('enabled'); var isLocal = s.status && s.status.includes('local'); return E('tr', {}, [ - E('td', {}, E('span', { 'class': 'soc-scenario' }, api.parseScenario(name))), - E('td', {}, E('span', { 'class': 'soc-severity ' + (enabled ? 'low' : 'medium') }, enabled ? 'ENABLED' : 'DISABLED')), - E('td', { 'class': 'soc-time' }, isLocal ? 'Local' : 'Hub') + E('td', {}, E('span', { 'class': 'cs-scenario' }, api.parseScenario(name))), + E('td', {}, E('span', { 'class': 'cs-severity ' + (enabled ? 'low' : 'medium') }, enabled ? 'ENABLED' : 'DISABLED')), + E('td', { 'class': 'cs-time' }, isLocal ? 'Local' : 'Hub') ]); })) ]); @@ -222,14 +224,14 @@ return view.extend({ try { threats = JSON.parse(rawJson); } catch(e) {} } if (!threats || !threats.length) { - return E('div', { 'class': 'soc-empty' }, [ - E('div', { 'class': 'soc-empty-icon' }, '\u{1F6E1}'), + return E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '\u{1F6E1}'), 'No threats blocked yet' ]); } var total = threats.reduce(function(sum, t) { return sum + (t.count || 0); }, 0); - return E('div', { 'class': 'soc-threat-types' }, [ - E('table', { 'class': 'soc-table' }, [ + return E('div', { 'class': 'cs-threat-types' }, [ + E('table', { 'class': 'cs-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Threat Type'), E('th', {}, 'Blocked'), @@ -242,18 +244,18 @@ return view.extend({ t.scenario.includes('scan') ? 'medium' : 'low'; return E('tr', {}, [ E('td', {}, [ - E('span', { 'class': 'soc-threat-icon ' + severity }, self.getThreatIcon(t.scenario)), - E('span', { 'class': 'soc-scenario' }, t.scenario) + E('span', { 'class': 'cs-threat-icon ' + severity }, self.getThreatIcon(t.scenario)), + E('span', { 'class': 'cs-scenario' }, t.scenario) ]), - E('td', { 'class': 'soc-threat-count' }, self.formatNumber(t.count)), - E('td', {}, E('div', { 'class': 'soc-bar-wrap' }, [ - E('div', { 'class': 'soc-bar ' + severity, 'style': 'width:' + pct + '%' }), - E('span', { 'class': 'soc-bar-pct' }, pct + '%') + E('td', { 'class': 'cs-threat-count' }, self.formatNumber(t.count)), + E('td', {}, E('div', { 'class': 'cs-bar-wrap' }, [ + E('div', { 'class': 'cs-bar ' + severity, 'style': 'width:' + pct + '%' }), + E('span', { 'class': 'cs-bar-pct' }, pct + '%') ])) ]); })) ]), - E('div', { 'class': 'soc-threat-total' }, 'Total blocked: ' + self.formatNumber(total)) + E('div', { 'class': 'cs-threat-total' }, 'Total blocked: ' + self.formatNumber(total)) ]); }, @@ -268,7 +270,7 @@ return view.extend({ pollData: function() { var self = this; return api.getOverview().then(function(data) { - var el = document.getElementById('soc-stats'); + var el = document.getElementById('cs-stats'); if (el) dom.content(el, self.renderStats(data)); el = document.getElementById('recent-alerts'); if (el) dom.content(el, self.renderAlerts(data.alerts || [])); @@ -280,7 +282,7 @@ return view.extend({ runHealthCheck: function() { var self = this; var el = document.getElementById('health-check'); - if (el) dom.content(el, E('div', { 'class': 'soc-loading' }, [E('div', { 'class': 'soc-spinner' }), 'Testing...'])); + if (el) dom.content(el, E('div', { 'class': 'cs-loading' }, [E('div', { 'class': 'cs-spinner' }), 'Testing...'])); return api.getHealthCheck().then(function(r) { if (el) dom.content(el, self.renderHealth(r)); self.showToast('Health check completed', 'success'); @@ -290,9 +292,9 @@ return view.extend({ }, showToast: function(msg, type) { - var t = document.querySelector('.soc-toast'); + var t = document.querySelector('.cs-toast'); if (t) t.remove(); - t = E('div', { 'class': 'soc-toast ' + type }, msg); + t = E('div', { 'class': 'cs-toast ' + type }, msg); document.body.appendChild(t); setTimeout(function() { t.remove(); }, 4000); }, diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js index 56e39bc1..c764d55c 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/settings.js @@ -2,11 +2,14 @@ 'require view'; 'require dom'; 'require ui'; +'require uci'; 'require crowdsec-dashboard.api as api'; +'require crowdsec-dashboard.theme as theme'; /** * CrowdSec SOC - Settings View * System configuration and management + * With theme/appearance settings */ return view.extend({ @@ -16,79 +19,151 @@ return view.extend({ collections: [], load: function() { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('crowdsec-dashboard/soc.css'); - document.head.appendChild(link); - document.body.classList.add('cs-soc-fullwidth'); - return Promise.all([ + theme.init(), api.getStatus(), api.getMachines(), api.getCollections(), - api.getAcquisitionConfig() + api.getAcquisitionConfig(), + uci.load('crowdsec-dashboard') ]); }, render: function(data) { var self = this; - this.status = data[0] || {}; - var machinesData = data[1] || {}; + // data[0] is theme.init() result + this.status = data[1] || {}; + var machinesData = data[2] || {}; this.machines = Array.isArray(machinesData) ? machinesData : (machinesData.machines || []); - var collectionsData = data[2] || {}; + var collectionsData = data[3] || {}; this.collections = collectionsData.collections || []; if (this.collections.collections) this.collections = this.collections.collections; - this.acquisition = data[3] || {}; + this.acquisition = data[4] || {}; - return E('div', { 'class': 'soc-dashboard' }, [ + document.body.classList.add('cs-fullwidth'); + + return E('div', { 'class': theme.getDashboardClass() }, [ this.renderHeader(), this.renderNav('settings'), - E('div', { 'class': 'soc-stats' }, this.renderServiceStats()), - E('div', { 'class': 'soc-grid-2' }, [ - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, [ + E('div', { 'class': 'cs-stats' }, this.renderServiceStats()), + E('div', { 'class': 'cs-grid-2' }, [ + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ 'Service Control', - E('span', { 'class': 'soc-severity ' + (this.status.crowdsec === 'running' ? 'low' : 'critical') }, + E('span', { 'class': 'cs-severity ' + (this.status.crowdsec === 'running' ? 'low' : 'critical') }, this.status.crowdsec === 'running' ? 'RUNNING' : 'STOPPED') ]), - E('div', { 'class': 'soc-card-body' }, this.renderServiceControl()) + E('div', { 'class': 'cs-card-body' }, this.renderServiceControl()) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, 'Acquisition Sources'), - E('div', { 'class': 'soc-card-body' }, this.renderAcquisition()) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Appearance'), + E('div', { 'class': 'cs-card-body' }, this.renderAppearance()) ]) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, [ - 'Installed Collections (' + this.collections.filter(function(c) { return c.status === 'enabled' || c.installed; }).length + ')', - E('button', { 'class': 'soc-btn soc-btn-sm', 'click': L.bind(this.updateHub, this) }, 'Update Hub') + E('div', { 'class': 'cs-grid-2' }, [ + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Acquisition Sources'), + E('div', { 'class': 'cs-card-body' }, this.renderAcquisition()) ]), - E('div', { 'class': 'soc-card-body', 'id': 'collections-list' }, this.renderCollections()) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Registered Machines'), + E('div', { 'class': 'cs-card-body' }, this.renderMachines()) + ]) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, 'Registered Machines'), - E('div', { 'class': 'soc-card-body' }, this.renderMachines()) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + 'Installed Collections (' + this.collections.filter(function(c) { return c.status === 'enabled' || c.installed; }).length + ')', + E('button', { 'class': 'cs-btn cs-btn-sm', 'click': L.bind(this.updateHub, this) }, 'Update Hub') + ]), + E('div', { 'class': 'cs-card-body', 'id': 'collections-list' }, this.renderCollections()) ]), - E('div', { 'class': 'soc-card' }, [ - E('div', { 'class': 'soc-card-header' }, 'Configuration Files'), - E('div', { 'class': 'soc-card-body' }, this.renderConfigFiles()) + E('div', { 'class': 'cs-card' }, [ + E('div', { 'class': 'cs-card-header' }, 'Configuration Files'), + E('div', { 'class': 'cs-card-body' }, this.renderConfigFiles()) ]) ]); }, + renderAppearance: function() { + var self = this; + var currentTheme = uci.get('crowdsec-dashboard', 'main', 'theme') || 'classic'; + var currentProfile = uci.get('crowdsec-dashboard', 'main', 'profile') || 'default'; + + var themes = theme.getThemes(); + var profiles = theme.getProfiles(); + + return E('div', {}, [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 8px; color: var(--cs-text-muted); font-size: 12px; text-transform: uppercase;' }, 'Theme'), + E('select', { + 'id': 'theme-select', + 'style': 'width: 100%; padding: 8px; background: var(--cs-bg-primary); border: 1px solid var(--cs-border); border-radius: 4px; color: var(--cs-text);', + 'change': function(ev) { self.previewTheme(ev.target.value); } + }, themes.map(function(t) { + return E('option', { 'value': t.id, 'selected': t.id === currentTheme }, t.name + ' - ' + t.description); + })) + ]), + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 8px; color: var(--cs-text-muted); font-size: 12px; text-transform: uppercase;' }, 'Profile'), + E('select', { + 'id': 'profile-select', + 'style': 'width: 100%; padding: 8px; background: var(--cs-bg-primary); border: 1px solid var(--cs-border); border-radius: 4px; color: var(--cs-text);', + 'change': function(ev) { self.previewProfile(ev.target.value); } + }, profiles.map(function(p) { + return E('option', { 'value': p.id, 'selected': p.id === currentProfile }, p.id.charAt(0).toUpperCase() + p.id.slice(1)); + })) + ]), + E('div', { 'style': 'display: flex; gap: 8px;' }, [ + E('button', { + 'class': 'cs-btn', + 'click': L.bind(this.saveAppearance, this) + }, 'Save Theme'), + E('button', { + 'class': 'cs-btn', + 'click': function() { location.reload(); } + }, 'Reset') + ]) + ]); + }, + + previewTheme: function(themeName) { + theme.switchTheme(themeName); + }, + + previewProfile: function(profileName) { + theme.switchProfile(profileName); + }, + + saveAppearance: function() { + var self = this; + var selectedTheme = document.getElementById('theme-select').value; + var selectedProfile = document.getElementById('profile-select').value; + + uci.set('crowdsec-dashboard', 'main', 'theme', selectedTheme); + uci.set('crowdsec-dashboard', 'main', 'profile', selectedProfile); + + uci.save().then(function() { + return uci.apply(); + }).then(function() { + self.showToast('Theme saved', 'success'); + }).catch(function(e) { + self.showToast('Failed to save: ' + e.message, 'error'); + }); + }, + renderHeader: function() { - return E('div', { 'class': 'soc-header' }, [ - E('div', { 'class': 'soc-title' }, [ + return E('div', { 'class': 'cs-header' }, [ + E('div', { 'class': 'cs-title' }, [ E('svg', { 'viewBox': '0 0 24 24' }, [E('path', { 'd': 'M12 2L2 7v10l10 5 10-5V7L12 2z' })]), 'CrowdSec Security Operations' ]), - E('div', { 'class': 'soc-status' }, [E('span', { 'class': 'soc-status-dot online' }), 'SETTINGS']) + E('div', { 'class': 'cs-status' }, [E('span', { 'class': 'cs-status-dot online' }), 'SETTINGS']) ]); }, renderNav: function(active) { var tabs = ['overview', 'alerts', 'decisions', 'bouncers', 'settings']; - return E('div', { 'class': 'soc-nav' }, tabs.map(function(t) { + return E('div', { 'class': 'cs-nav' }, tabs.map(function(t) { return E('a', { 'href': L.url('admin/secubox/security/crowdsec/' + t), 'class': active === t ? 'active' : '' @@ -99,21 +174,21 @@ return view.extend({ renderServiceStats: function() { var s = this.status; return [ - E('div', { 'class': 'soc-stat ' + (s.crowdsec === 'running' ? 'success' : 'danger') }, [ - E('div', { 'class': 'soc-stat-value' }, s.crowdsec === 'running' ? 'ON' : 'OFF'), - E('div', { 'class': 'soc-stat-label' }, 'CrowdSec Agent') + E('div', { 'class': 'cs-stat ' + (s.crowdsec === 'running' ? 'success' : 'danger') }, [ + E('div', { 'class': 'cs-stat-value' }, s.crowdsec === 'running' ? 'ON' : 'OFF'), + E('div', { 'class': 'cs-stat-label' }, 'CrowdSec Agent') ]), - E('div', { 'class': 'soc-stat ' + (s.lapi_status === 'available' ? 'success' : 'danger') }, [ - E('div', { 'class': 'soc-stat-value' }, s.lapi_status === 'available' ? 'OK' : 'DOWN'), - E('div', { 'class': 'soc-stat-label' }, 'Local API') + E('div', { 'class': 'cs-stat ' + (s.lapi_status === 'available' ? 'success' : 'danger') }, [ + E('div', { 'class': 'cs-stat-value' }, s.lapi_status === 'available' ? 'OK' : 'DOWN'), + E('div', { 'class': 'cs-stat-label' }, 'Local API') ]), - E('div', { 'class': 'soc-stat' }, [ - E('div', { 'class': 'soc-stat-value' }, s.version || 'N/A'), - E('div', { 'class': 'soc-stat-label' }, 'Version') + E('div', { 'class': 'cs-stat' }, [ + E('div', { 'class': 'cs-stat-value' }, s.version || 'N/A'), + E('div', { 'class': 'cs-stat-label' }, 'Version') ]), - E('div', { 'class': 'soc-stat' }, [ - E('div', { 'class': 'soc-stat-value' }, String(this.machines.length)), - E('div', { 'class': 'soc-stat-label' }, 'Machines') + E('div', { 'class': 'cs-stat' }, [ + E('div', { 'class': 'cs-stat-value' }, String(this.machines.length)), + E('div', { 'class': 'cs-stat-label' }, 'Machines') ]) ]; }, @@ -124,44 +199,44 @@ return view.extend({ return E('div', {}, [ E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;' }, [ E('button', { - 'class': 'soc-btn ' + (running ? '' : 'primary'), + 'class': 'cs-btn ' + (running ? '' : 'primary'), 'click': function() { self.serviceAction('start'); } }, 'Start'), E('button', { - 'class': 'soc-btn ' + (running ? 'danger' : ''), + 'class': 'cs-btn ' + (running ? 'danger' : ''), 'click': function() { self.serviceAction('stop'); } }, 'Stop'), E('button', { - 'class': 'soc-btn', + 'class': 'cs-btn', 'click': function() { self.serviceAction('restart'); } }, 'Restart'), E('button', { - 'class': 'soc-btn', + 'class': 'cs-btn', 'click': function() { self.serviceAction('reload'); } }, 'Reload') ]), - E('div', { 'class': 'soc-health' }, [ - E('div', { 'class': 'soc-health-item' }, [ - E('div', { 'class': 'soc-health-icon ' + (running ? 'ok' : 'error') }, running ? '\u2713' : '\u2717'), + E('div', { 'class': 'cs-health' }, [ + E('div', { 'class': 'cs-health-item' }, [ + E('div', { 'class': 'cs-health-icon ' + (running ? 'ok' : 'error') }, running ? '\u2713' : '\u2717'), E('div', {}, [ - E('div', { 'class': 'soc-health-label' }, 'Agent'), - E('div', { 'class': 'soc-health-value' }, running ? 'Running' : 'Stopped') + E('div', { 'class': 'cs-health-label' }, 'Agent'), + E('div', { 'class': 'cs-health-value' }, running ? 'Running' : 'Stopped') ]) ]), - E('div', { 'class': 'soc-health-item' }, [ - E('div', { 'class': 'soc-health-icon ' + (this.status.lapi_status === 'available' ? 'ok' : 'error') }, + E('div', { 'class': 'cs-health-item' }, [ + E('div', { 'class': 'cs-health-icon ' + (this.status.lapi_status === 'available' ? 'ok' : 'error') }, this.status.lapi_status === 'available' ? '\u2713' : '\u2717'), E('div', {}, [ - E('div', { 'class': 'soc-health-label' }, 'LAPI'), - E('div', { 'class': 'soc-health-value' }, this.status.lapi_status === 'available' ? 'Available' : 'Unavailable') + E('div', { 'class': 'cs-health-label' }, 'LAPI'), + E('div', { 'class': 'cs-health-value' }, this.status.lapi_status === 'available' ? 'Available' : 'Unavailable') ]) ]), - E('div', { 'class': 'soc-health-item' }, [ - E('div', { 'class': 'soc-health-icon ' + (this.status.capi_enrolled ? 'ok' : 'warn') }, + E('div', { 'class': 'cs-health-item' }, [ + E('div', { 'class': 'cs-health-icon ' + (this.status.capi_enrolled ? 'ok' : 'warn') }, this.status.capi_enrolled ? '\u2713' : '!'), E('div', {}, [ - E('div', { 'class': 'soc-health-label' }, 'CAPI'), - E('div', { 'class': 'soc-health-value' }, this.status.capi_enrolled ? 'Enrolled' : 'Not enrolled') + E('div', { 'class': 'cs-health-label' }, 'CAPI'), + E('div', { 'class': 'cs-health-value' }, this.status.capi_enrolled ? 'Enrolled' : 'Not enrolled') ]) ]) ]) @@ -176,12 +251,12 @@ return view.extend({ { name: 'Firewall', enabled: acq.firewall_enabled }, { name: 'HTTP', enabled: acq.http_enabled } ]; - return E('div', { 'class': 'soc-health' }, sources.map(function(src) { - return E('div', { 'class': 'soc-health-item' }, [ - E('div', { 'class': 'soc-health-icon ' + (src.enabled ? 'ok' : 'error') }, src.enabled ? '\u2713' : '\u2717'), + return E('div', { 'class': 'cs-health' }, sources.map(function(src) { + return E('div', { 'class': 'cs-health-item' }, [ + E('div', { 'class': 'cs-health-icon ' + (src.enabled ? 'ok' : 'error') }, src.enabled ? '\u2713' : '\u2717'), E('div', {}, [ - E('div', { 'class': 'soc-health-label' }, src.name), - E('div', { 'class': 'soc-health-value' }, src.enabled ? (src.path || 'Enabled') : 'Disabled') + E('div', { 'class': 'cs-health-label' }, src.name), + E('div', { 'class': 'cs-health-value' }, src.enabled ? (src.path || 'Enabled') : 'Disabled') ]) ]); })); @@ -194,13 +269,13 @@ return view.extend({ }); if (!installed.length) { - return E('div', { 'class': 'soc-empty' }, [ - E('div', { 'class': 'soc-empty-icon' }, '\u26A0'), + return E('div', { 'class': 'cs-empty' }, [ + E('div', { 'class': 'cs-empty-icon' }, '\u26A0'), 'No collections installed. Click "Update Hub" to fetch available collections.' ]); } - return E('table', { 'class': 'soc-table' }, [ + return E('table', { 'class': 'cs-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Collection'), E('th', {}, 'Version'), @@ -209,11 +284,11 @@ return view.extend({ ])), E('tbody', {}, installed.map(function(c) { return E('tr', {}, [ - E('td', {}, E('span', { 'class': 'soc-scenario' }, c.name || 'Unknown')), - E('td', { 'class': 'soc-time' }, c.version || c.local_version || 'N/A'), - E('td', {}, E('span', { 'class': 'soc-severity low' }, 'INSTALLED')), + E('td', {}, E('span', { 'class': 'cs-scenario' }, c.name || 'Unknown')), + E('td', { 'class': 'cs-time' }, c.version || c.local_version || 'N/A'), + E('td', {}, E('span', { 'class': 'cs-severity low' }, 'INSTALLED')), E('td', {}, E('button', { - 'class': 'soc-btn soc-btn-sm danger', + 'class': 'cs-btn cs-btn-sm danger', 'click': function() { self.removeCollection(c.name); } }, 'Remove')) ]); @@ -223,10 +298,10 @@ return view.extend({ renderMachines: function() { if (!this.machines.length) { - return E('div', { 'class': 'soc-empty' }, 'No machines registered'); + return E('div', { 'class': 'cs-empty' }, 'No machines registered'); } - return E('table', { 'class': 'soc-table' }, [ + return E('table', { 'class': 'cs-table' }, [ E('thead', {}, E('tr', {}, [ E('th', {}, 'Machine ID'), E('th', {}, 'IP Address'), @@ -237,9 +312,9 @@ return view.extend({ var isActive = m.isValidated || m.is_validated; return E('tr', {}, [ E('td', {}, E('strong', {}, m.machineId || m.machine_id || 'Unknown')), - E('td', {}, E('span', { 'class': 'soc-ip' }, m.ipAddress || m.ip_address || 'N/A')), - E('td', { 'class': 'soc-time' }, api.formatRelativeTime(m.updated_at || m.updatedAt)), - E('td', {}, E('span', { 'class': 'soc-severity ' + (isActive ? 'low' : 'medium') }, + E('td', {}, E('span', { 'class': 'cs-ip' }, m.ipAddress || m.ip_address || 'N/A')), + E('td', { 'class': 'cs-time' }, api.formatRelativeTime(m.updated_at || m.updatedAt)), + E('td', {}, E('span', { 'class': 'cs-severity ' + (isActive ? 'low' : 'medium') }, isActive ? 'ACTIVE' : 'PENDING')) ]); })) @@ -256,9 +331,9 @@ return view.extend({ ]; return E('div', { 'style': 'display: grid; gap: 8px;' }, configs.map(function(cfg) { - return E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: var(--soc-bg); border-radius: 4px;' }, [ - E('span', { 'style': 'color: var(--soc-text-muted);' }, cfg.label), - E('code', { 'class': 'soc-ip' }, cfg.path) + return E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: var(--cs-bg); border-radius: 4px;' }, [ + E('span', { 'style': 'color: var(--cs-text-muted);' }, cfg.label), + E('code', { 'class': 'cs-ip' }, cfg.path) ]); })); }, @@ -301,9 +376,9 @@ return view.extend({ }, showToast: function(msg, type) { - var t = document.querySelector('.soc-toast'); + var t = document.querySelector('.cs-toast'); if (t) t.remove(); - t = E('div', { 'class': 'soc-toast ' + type }, msg); + t = E('div', { 'class': 'cs-toast ' + type }, msg); document.body.appendChild(t); setTimeout(function() { t.remove(); }, 4000); }, diff --git a/package/secubox/luci-app-crowdsec-dashboard/root/etc/config/crowdsec-dashboard b/package/secubox/luci-app-crowdsec-dashboard/root/etc/config/crowdsec-dashboard index 49fd472c..13f33711 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/root/etc/config/crowdsec-dashboard +++ b/package/secubox/luci-app-crowdsec-dashboard/root/etc/config/crowdsec-dashboard @@ -2,3 +2,7 @@ config settings 'main' option enrollment_key '' option machine_name '' option auto_enroll '0' + # Theme: classic, cards, cyberpunk + option theme 'classic' + # Profile: default, soc, modern, hacker + option profile 'default'