ok
This commit is contained in:
parent
47975483c5
commit
b610239551
@ -274,7 +274,21 @@
|
||||
"Bash(do scp \"$f\" root@192.168.8.191:/www/luci-static/resources/view/secubox/)",
|
||||
"Bash(node:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(for f in luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js luci-app-secubox/htdocs/luci-static/resources/secubox/api.js)"
|
||||
"Bash(for f in luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js luci-app-secubox/htdocs/luci-static/resources/secubox/api.js)",
|
||||
"Bash(for f in /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/*.js)",
|
||||
"Bash(do scp \"$f\" root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)",
|
||||
"Bash(for f in dashboard.js health.js logs.js settings.js)",
|
||||
"Bash(root@192.168.8.191:/www/luci-static/resources/view/secubox-admin/)",
|
||||
"Bash(root@192.168.8.191:/usr/libexec/rpcd/luci.secubox)",
|
||||
"Bash(root@192.168.8.191:/www/luci-static/resources/secubox-admin/)",
|
||||
"Bash(root@192.168.8.191:/www/luci-static/resources/system-hub/)",
|
||||
"Bash(root@192.168.8.191:/usr/libexec/rpcd/luci.system-hub)",
|
||||
"Bash(root@192.168.8.191:/usr/sbin/secubox-appstore)",
|
||||
"Bash(for f in zones.js overview.js clients.js)",
|
||||
"Bash(do scp /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/$f root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)",
|
||||
"Bash(for f in clients.js overview.js)",
|
||||
"Bash(for f in htdocs/luci-static/resources/view/client-guardian/settings.js htdocs/luci-static/resources/client-guardian/api.js root/usr/libexec/rpcd/luci.client-guardian root/etc/config/client-guardian)",
|
||||
"Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,309 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require uci';
|
||||
|
||||
/**
|
||||
* Client Guardian Debug Module
|
||||
* Provides comprehensive logging and debugging capabilities
|
||||
*/
|
||||
|
||||
var DEBUG_LEVELS = {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3,
|
||||
TRACE: 4
|
||||
};
|
||||
|
||||
var DEBUG_COLORS = {
|
||||
ERROR: '#ef4444',
|
||||
WARN: '#f59e0b',
|
||||
INFO: '#3b82f6',
|
||||
DEBUG: '#8b5cf6',
|
||||
TRACE: '#6b7280'
|
||||
};
|
||||
|
||||
var debugEnabled = false;
|
||||
var debugLevel = DEBUG_LEVELS.INFO;
|
||||
var logBuffer = [];
|
||||
var maxBufferSize = 500;
|
||||
|
||||
return baseclass.extend({
|
||||
init: function() {
|
||||
// Check if debug mode is enabled in UCI
|
||||
return uci.load('client-guardian').then(L.bind(function() {
|
||||
debugEnabled = uci.get('client-guardian', 'config', 'debug_enabled') === '1';
|
||||
var level = uci.get('client-guardian', 'config', 'debug_level') || 'INFO';
|
||||
debugLevel = DEBUG_LEVELS[level] || DEBUG_LEVELS.INFO;
|
||||
|
||||
if (debugEnabled) {
|
||||
this.info('Client Guardian Debug Mode Enabled', {
|
||||
level: level,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}, this)).catch(function() {
|
||||
// UCI not available, use defaults
|
||||
debugEnabled = false;
|
||||
});
|
||||
},
|
||||
|
||||
isEnabled: function() {
|
||||
return debugEnabled;
|
||||
},
|
||||
|
||||
setEnabled: function(enabled) {
|
||||
debugEnabled = enabled;
|
||||
if (enabled) {
|
||||
this.info('Debug mode enabled manually');
|
||||
}
|
||||
},
|
||||
|
||||
setLevel: function(level) {
|
||||
if (typeof level === 'string') {
|
||||
debugLevel = DEBUG_LEVELS[level.toUpperCase()] || DEBUG_LEVELS.INFO;
|
||||
} else {
|
||||
debugLevel = level;
|
||||
}
|
||||
this.info('Debug level changed', { level: debugLevel });
|
||||
},
|
||||
|
||||
_log: function(level, levelName, message, data) {
|
||||
if (!debugEnabled || level > debugLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timestamp = new Date().toISOString();
|
||||
var logEntry = {
|
||||
timestamp: timestamp,
|
||||
level: levelName,
|
||||
message: message,
|
||||
data: data || {}
|
||||
};
|
||||
|
||||
// Add to buffer
|
||||
logBuffer.push(logEntry);
|
||||
if (logBuffer.length > maxBufferSize) {
|
||||
logBuffer.shift();
|
||||
}
|
||||
|
||||
// Console output with styling
|
||||
var style = 'color: ' + DEBUG_COLORS[levelName] + '; font-weight: bold;';
|
||||
var prefix = '[CG:' + levelName + ']';
|
||||
|
||||
if (data) {
|
||||
console.log('%c' + prefix + ' ' + timestamp, style, message, data);
|
||||
} else {
|
||||
console.log('%c' + prefix + ' ' + timestamp, style, message);
|
||||
}
|
||||
},
|
||||
|
||||
error: function(message, data) {
|
||||
this._log(DEBUG_LEVELS.ERROR, 'ERROR', message, data);
|
||||
},
|
||||
|
||||
warn: function(message, data) {
|
||||
this._log(DEBUG_LEVELS.WARN, 'WARN', message, data);
|
||||
},
|
||||
|
||||
info: function(message, data) {
|
||||
this._log(DEBUG_LEVELS.INFO, 'INFO', message, data);
|
||||
},
|
||||
|
||||
debug: function(message, data) {
|
||||
this._log(DEBUG_LEVELS.DEBUG, 'DEBUG', message, data);
|
||||
},
|
||||
|
||||
trace: function(message, data) {
|
||||
this._log(DEBUG_LEVELS.TRACE, 'TRACE', message, data);
|
||||
},
|
||||
|
||||
// API call tracing
|
||||
traceAPICall: function(method, params) {
|
||||
this.debug('API Call: ' + method, {
|
||||
params: params,
|
||||
stack: new Error().stack
|
||||
});
|
||||
},
|
||||
|
||||
traceAPIResponse: function(method, response, duration) {
|
||||
this.debug('API Response: ' + method, {
|
||||
response: response,
|
||||
duration: duration + 'ms'
|
||||
});
|
||||
},
|
||||
|
||||
traceAPIError: function(method, error) {
|
||||
this.error('API Error: ' + method, {
|
||||
error: error.toString(),
|
||||
stack: error.stack
|
||||
});
|
||||
},
|
||||
|
||||
// UI event tracing
|
||||
traceEvent: function(eventName, target, data) {
|
||||
this.trace('Event: ' + eventName, {
|
||||
target: target,
|
||||
data: data
|
||||
});
|
||||
},
|
||||
|
||||
// Performance monitoring
|
||||
startTimer: function(label) {
|
||||
if (!debugEnabled) return null;
|
||||
|
||||
var timer = {
|
||||
label: label,
|
||||
start: performance.now()
|
||||
};
|
||||
|
||||
this.trace('Timer started: ' + label);
|
||||
return timer;
|
||||
},
|
||||
|
||||
endTimer: function(timer) {
|
||||
if (!debugEnabled || !timer) return;
|
||||
|
||||
var duration = (performance.now() - timer.start).toFixed(2);
|
||||
this.debug('Timer ended: ' + timer.label, {
|
||||
duration: duration + 'ms'
|
||||
});
|
||||
|
||||
return duration;
|
||||
},
|
||||
|
||||
// Get log buffer
|
||||
getLogs: function(level, count) {
|
||||
var filtered = logBuffer;
|
||||
|
||||
if (level) {
|
||||
var levelValue = DEBUG_LEVELS[level.toUpperCase()];
|
||||
filtered = logBuffer.filter(function(entry) {
|
||||
return DEBUG_LEVELS[entry.level] <= levelValue;
|
||||
});
|
||||
}
|
||||
|
||||
if (count) {
|
||||
filtered = filtered.slice(-count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// Export logs as text
|
||||
exportLogs: function() {
|
||||
var text = '=== Client Guardian Debug Logs ===\n';
|
||||
text += 'Generated: ' + new Date().toISOString() + '\n';
|
||||
text += 'Total entries: ' + logBuffer.length + '\n\n';
|
||||
|
||||
logBuffer.forEach(function(entry) {
|
||||
text += '[' + entry.timestamp + '] [' + entry.level + '] ' + entry.message;
|
||||
if (Object.keys(entry.data).length > 0) {
|
||||
text += '\n Data: ' + JSON.stringify(entry.data, null, 2);
|
||||
}
|
||||
text += '\n\n';
|
||||
});
|
||||
|
||||
return text;
|
||||
},
|
||||
|
||||
// Download logs as file
|
||||
downloadLogs: function() {
|
||||
var text = this.exportLogs();
|
||||
var blob = new Blob([text], { type: 'text/plain' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'client-guardian-debug-' + Date.now() + '.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.info('Logs downloaded');
|
||||
},
|
||||
|
||||
// Clear log buffer
|
||||
clearLogs: function() {
|
||||
logBuffer = [];
|
||||
this.info('Log buffer cleared');
|
||||
},
|
||||
|
||||
// Get system info for debugging
|
||||
getSystemInfo: function() {
|
||||
return {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
screenResolution: window.screen.width + 'x' + window.screen.height,
|
||||
windowSize: window.innerWidth + 'x' + window.innerHeight,
|
||||
cookiesEnabled: navigator.cookieEnabled,
|
||||
onLine: navigator.onLine,
|
||||
timestamp: new Date().toISOString(),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
memory: performance.memory ? {
|
||||
usedJSHeapSize: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
totalJSHeapSize: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB'
|
||||
} : 'N/A'
|
||||
};
|
||||
},
|
||||
|
||||
// Network request monitoring
|
||||
monitorFetch: function(originalFetch) {
|
||||
if (!debugEnabled) return originalFetch;
|
||||
|
||||
var self = this;
|
||||
return function() {
|
||||
var args = arguments;
|
||||
var url = args[0];
|
||||
var timer = self.startTimer('Fetch: ' + url);
|
||||
|
||||
self.trace('Fetch request', {
|
||||
url: url,
|
||||
options: args[1]
|
||||
});
|
||||
|
||||
return originalFetch.apply(this, args).then(function(response) {
|
||||
var duration = self.endTimer(timer);
|
||||
self.trace('Fetch response', {
|
||||
url: url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: duration
|
||||
});
|
||||
return response;
|
||||
}).catch(function(error) {
|
||||
self.error('Fetch error', {
|
||||
url: url,
|
||||
error: error.toString()
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
// Initialize global error handler
|
||||
setupGlobalErrorHandler: function() {
|
||||
var self = this;
|
||||
|
||||
window.addEventListener('error', function(event) {
|
||||
self.error('Global error', {
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno,
|
||||
error: event.error ? event.error.toString() : 'Unknown'
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
self.error('Unhandled promise rejection', {
|
||||
reason: event.reason,
|
||||
promise: event.promise
|
||||
});
|
||||
});
|
||||
|
||||
this.info('Global error handlers registered');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,259 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require client-guardian/debug as Debug';
|
||||
|
||||
var callGetLogs = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'logs',
|
||||
params: ['limit', 'level'],
|
||||
expect: { logs: [] }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
Debug.init(),
|
||||
uci.load('client-guardian'),
|
||||
callGetLogs(100, 'debug')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var backendLogs = data[2].logs || [];
|
||||
var self = this;
|
||||
|
||||
var debugEnabled = uci.get('client-guardian', 'config', 'debug_enabled') === '1';
|
||||
var debugLevel = uci.get('client-guardian', 'config', 'debug_level') || 'INFO';
|
||||
|
||||
return E('div', { 'class': 'client-guardian-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('client-guardian/dashboard.css') }),
|
||||
|
||||
E('div', { 'class': 'cg-header' }, [
|
||||
E('div', { 'class': 'cg-logo' }, [
|
||||
E('div', { 'class': 'cg-logo-icon' }, '🐛'),
|
||||
E('div', { 'class': 'cg-logo-text' }, 'Mode Debug')
|
||||
]),
|
||||
E('div', { 'class': 'cg-debug-controls' }, [
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-sm',
|
||||
'click': L.bind(this.handleRefreshLogs, this)
|
||||
}, '🔄 Actualiser'),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-sm',
|
||||
'click': L.bind(this.handleClearLogs, this)
|
||||
}, '🗑️ Effacer'),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-sm cg-btn-primary',
|
||||
'click': L.bind(this.handleDownloadLogs, this)
|
||||
}, '💾 Télécharger')
|
||||
])
|
||||
]),
|
||||
|
||||
// Debug Status Card
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '⚙️'),
|
||||
'Configuration Debug'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
E('div', { 'class': 'cg-debug-status-grid' }, [
|
||||
E('div', { 'class': 'cg-debug-status-item' }, [
|
||||
E('div', { 'class': 'cg-debug-status-label' }, 'Mode Debug'),
|
||||
E('div', { 'class': 'cg-debug-status-value' }, [
|
||||
E('span', {
|
||||
'class': 'cg-status-badge ' + (debugEnabled ? 'approved' : 'offline')
|
||||
}, [
|
||||
E('span', { 'class': 'cg-status-dot' }),
|
||||
debugEnabled ? 'Activé' : 'Désactivé'
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-sm',
|
||||
'style': 'margin-left: 8px',
|
||||
'click': L.bind(this.handleToggleDebug, this, !debugEnabled)
|
||||
}, debugEnabled ? 'Désactiver' : 'Activer')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-debug-status-item' }, [
|
||||
E('div', { 'class': 'cg-debug-status-label' }, 'Niveau de Log'),
|
||||
E('select', {
|
||||
'class': 'cg-input cg-input-sm',
|
||||
'id': 'debug-level-select',
|
||||
'value': debugLevel,
|
||||
'change': L.bind(this.handleChangeLevel, this)
|
||||
}, [
|
||||
E('option', { 'value': 'ERROR' }, 'ERROR'),
|
||||
E('option', { 'value': 'WARN' }, 'WARN'),
|
||||
E('option', { 'value': 'INFO', 'selected': debugLevel === 'INFO' }, 'INFO'),
|
||||
E('option', { 'value': 'DEBUG' }, 'DEBUG'),
|
||||
E('option', { 'value': 'TRACE' }, 'TRACE')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-debug-status-item' }, [
|
||||
E('div', { 'class': 'cg-debug-status-label' }, 'Logs Backend'),
|
||||
E('div', { 'class': 'cg-debug-status-value' }, backendLogs.length + ' entrées')
|
||||
]),
|
||||
E('div', { 'class': 'cg-debug-status-item' }, [
|
||||
E('div', { 'class': 'cg-debug-status-label' }, 'Logs Frontend'),
|
||||
E('div', { 'class': 'cg-debug-status-value' }, Debug.getLogs().length + ' entrées')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// System Information
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, 'ℹ️'),
|
||||
'Informations Système'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
this.renderSystemInfo(Debug.getSystemInfo())
|
||||
])
|
||||
]),
|
||||
|
||||
// Backend Logs
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '📋'),
|
||||
'Logs Backend RPCD'
|
||||
]),
|
||||
E('span', { 'class': 'cg-card-badge' }, backendLogs.length + ' entrées')
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
E('div', { 'class': 'cg-log-container', 'id': 'backend-logs' },
|
||||
backendLogs.length > 0 ?
|
||||
backendLogs.map(L.bind(this.renderLogEntry, this)) :
|
||||
E('div', { 'class': 'cg-empty-state' }, [
|
||||
E('div', { 'class': 'cg-empty-state-icon' }, '📝'),
|
||||
E('div', { 'class': 'cg-empty-state-title' }, 'Aucun log backend'),
|
||||
E('div', { 'class': 'cg-empty-state-text' }, 'Les logs du serveur apparaîtront ici')
|
||||
])
|
||||
)
|
||||
])
|
||||
]),
|
||||
|
||||
// Frontend Console Logs
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '💻'),
|
||||
'Logs Frontend Console'
|
||||
]),
|
||||
E('span', { 'class': 'cg-card-badge' }, Debug.getLogs().length + ' entrées')
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
E('div', { 'class': 'cg-log-container', 'id': 'frontend-logs' },
|
||||
Debug.getLogs().length > 0 ?
|
||||
Debug.getLogs().reverse().slice(0, 100).map(L.bind(this.renderLogEntry, this)) :
|
||||
E('div', { 'class': 'cg-empty-state' }, [
|
||||
E('div', { 'class': 'cg-empty-state-icon' }, '🖥️'),
|
||||
E('div', { 'class': 'cg-empty-state-title' }, 'Aucun log frontend'),
|
||||
E('div', { 'class': 'cg-empty-state-text' }, 'Les logs du navigateur apparaîtront ici')
|
||||
])
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderSystemInfo: function(info) {
|
||||
return E('div', { 'class': 'cg-system-info-grid' }, [
|
||||
this.renderInfoItem('Navigateur', info.userAgent),
|
||||
this.renderInfoItem('Plateforme', info.platform),
|
||||
this.renderInfoItem('Langue', info.language),
|
||||
this.renderInfoItem('Résolution', info.screenResolution),
|
||||
this.renderInfoItem('Fenêtre', info.windowSize),
|
||||
this.renderInfoItem('Cookies', info.cookiesEnabled ? 'Activés' : 'Désactivés'),
|
||||
this.renderInfoItem('Connexion', info.onLine ? 'En ligne' : 'Hors ligne'),
|
||||
this.renderInfoItem('Fuseau horaire', info.timezone),
|
||||
this.renderInfoItem('Mémoire JS', typeof info.memory === 'object' ?
|
||||
'Utilisée: ' + info.memory.usedJSHeapSize + ' / Limite: ' + info.memory.jsHeapSizeLimit :
|
||||
info.memory
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
renderInfoItem: function(label, value) {
|
||||
return E('div', { 'class': 'cg-info-item' }, [
|
||||
E('div', { 'class': 'cg-info-label' }, label + ':'),
|
||||
E('div', { 'class': 'cg-info-value' }, value)
|
||||
]);
|
||||
},
|
||||
|
||||
renderLogEntry: function(log) {
|
||||
var levelClass = 'cg-log-' + (log.level || 'info').toLowerCase();
|
||||
var levelIcon = {
|
||||
'ERROR': '🚨',
|
||||
'WARN': '⚠️',
|
||||
'INFO': 'ℹ️',
|
||||
'DEBUG': '🐛',
|
||||
'TRACE': '🔍'
|
||||
}[log.level] || 'ℹ️';
|
||||
|
||||
return E('div', { 'class': 'cg-log-entry ' + levelClass }, [
|
||||
E('div', { 'class': 'cg-log-header' }, [
|
||||
E('span', { 'class': 'cg-log-icon' }, levelIcon),
|
||||
E('span', { 'class': 'cg-log-level' }, log.level || 'INFO'),
|
||||
E('span', { 'class': 'cg-log-time' }, log.timestamp || new Date().toISOString())
|
||||
]),
|
||||
E('div', { 'class': 'cg-log-message' }, log.message),
|
||||
log.data && Object.keys(log.data).length > 0 ?
|
||||
E('details', { 'class': 'cg-log-details' }, [
|
||||
E('summary', {}, 'Données additionnelles'),
|
||||
E('pre', { 'class': 'cg-log-data' }, JSON.stringify(log.data, null, 2))
|
||||
]) :
|
||||
E('span')
|
||||
]);
|
||||
},
|
||||
|
||||
handleToggleDebug: function(enabled, ev) {
|
||||
uci.set('client-guardian', 'config', 'debug_enabled', enabled ? '1' : '0');
|
||||
uci.save().then(L.bind(function() {
|
||||
return uci.apply();
|
||||
}, this)).then(L.bind(function() {
|
||||
ui.addNotification(null, E('p', {}, 'Mode debug ' + (enabled ? 'activé' : 'désactivé')), 'success');
|
||||
Debug.setEnabled(enabled);
|
||||
location.reload();
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleChangeLevel: function(ev) {
|
||||
var level = ev.target.value;
|
||||
uci.set('client-guardian', 'config', 'debug_level', level);
|
||||
uci.save().then(L.bind(function() {
|
||||
return uci.apply();
|
||||
}, this)).then(L.bind(function() {
|
||||
ui.addNotification(null, E('p', {}, 'Niveau de debug changé: ' + level), 'success');
|
||||
Debug.setLevel(level);
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleRefreshLogs: function(ev) {
|
||||
location.reload();
|
||||
},
|
||||
|
||||
handleClearLogs: function(ev) {
|
||||
Debug.clearLogs();
|
||||
ui.addNotification(null, E('p', {}, 'Logs frontend effacés'), 'success');
|
||||
location.reload();
|
||||
},
|
||||
|
||||
handleDownloadLogs: function(ev) {
|
||||
Debug.downloadLogs();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,405 @@
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"id": "family_home",
|
||||
"name": "Maison Familiale",
|
||||
"description": "Configuration optimale pour une maison avec parents, enfants et appareils IoT",
|
||||
"icon": "🏠",
|
||||
"zones": [
|
||||
{
|
||||
"id": "lan_private",
|
||||
"name": "Réseau Principal",
|
||||
"description": "Appareils de confiance des parents",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "home",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "kids",
|
||||
"name": "Enfants",
|
||||
"description": "Contrôle parental actif avec horaires",
|
||||
"network": "lan",
|
||||
"color": "#06b6d4",
|
||||
"icon": "child",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 50,
|
||||
"time_restrictions": true,
|
||||
"schedule_start": "08:00",
|
||||
"schedule_end": "21:00",
|
||||
"content_filter": "kids",
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "iot",
|
||||
"name": "Objets Connectés",
|
||||
"description": "Caméras, thermostats, ampoules (isolés)",
|
||||
"network": "iot",
|
||||
"color": "#f59e0b",
|
||||
"icon": "cpu",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 10,
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "guest",
|
||||
"name": "Invités",
|
||||
"description": "Accès Internet limité pour visiteurs",
|
||||
"network": "guest",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "users",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 25,
|
||||
"session_duration": 7200,
|
||||
"portal_required": true,
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "small_business",
|
||||
"name": "Petite Entreprise",
|
||||
"description": "Séparation réseau employés, invités et équipements",
|
||||
"icon": "🏢",
|
||||
"zones": [
|
||||
{
|
||||
"id": "corporate",
|
||||
"name": "Réseau Entreprise",
|
||||
"description": "Postes de travail des employés",
|
||||
"network": "lan",
|
||||
"color": "#3b82f6",
|
||||
"icon": "briefcase",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "servers",
|
||||
"name": "Serveurs",
|
||||
"description": "Infrastructure critique",
|
||||
"network": "servers",
|
||||
"color": "#ef4444",
|
||||
"icon": "server",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "critical"
|
||||
},
|
||||
{
|
||||
"id": "byod",
|
||||
"name": "BYOD",
|
||||
"description": "Appareils personnels des employés",
|
||||
"network": "byod",
|
||||
"color": "#f59e0b",
|
||||
"icon": "smartphone",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 50,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "guest",
|
||||
"name": "Visiteurs",
|
||||
"description": "Accès Internet isolé",
|
||||
"network": "guest",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "users",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 10,
|
||||
"portal_required": true,
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hotel",
|
||||
"name": "Hôtel / Gîte",
|
||||
"description": "Gestion multi-chambres avec isolation stricte",
|
||||
"icon": "🏨",
|
||||
"zones": [
|
||||
{
|
||||
"id": "management",
|
||||
"name": "Administration",
|
||||
"description": "Réseau de gestion",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "shield",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "critical"
|
||||
},
|
||||
{
|
||||
"id": "rooms_floor1",
|
||||
"name": "Chambres Étage 1",
|
||||
"description": "Clients étage 1 (isolés)",
|
||||
"network": "rooms1",
|
||||
"color": "#3b82f6",
|
||||
"icon": "bed",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 20,
|
||||
"portal_required": true,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "rooms_floor2",
|
||||
"name": "Chambres Étage 2",
|
||||
"description": "Clients étage 2 (isolés)",
|
||||
"network": "rooms2",
|
||||
"color": "#06b6d4",
|
||||
"icon": "bed",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 20,
|
||||
"portal_required": true,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "public",
|
||||
"name": "Espaces Communs",
|
||||
"description": "Lobby, restaurant, bar",
|
||||
"network": "public",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "wifi",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 15,
|
||||
"portal_required": true,
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "apartment",
|
||||
"name": "Immeuble / Colocation",
|
||||
"description": "Isolation stricte entre locataires",
|
||||
"icon": "🏘️",
|
||||
"zones": [
|
||||
{
|
||||
"id": "landlord",
|
||||
"name": "Propriétaire",
|
||||
"description": "Réseau administrateur",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "key",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "tenant_a",
|
||||
"name": "Locataire A",
|
||||
"description": "Appartement/Chambre A",
|
||||
"network": "tenant_a",
|
||||
"color": "#3b82f6",
|
||||
"icon": "door",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 100,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "tenant_b",
|
||||
"name": "Locataire B",
|
||||
"description": "Appartement/Chambre B",
|
||||
"network": "tenant_b",
|
||||
"color": "#06b6d4",
|
||||
"icon": "door",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 100,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "tenant_c",
|
||||
"name": "Locataire C",
|
||||
"description": "Appartement/Chambre C",
|
||||
"network": "tenant_c",
|
||||
"color": "#f59e0b",
|
||||
"icon": "door",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 100,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "common",
|
||||
"name": "Parties Communes",
|
||||
"description": "Couloirs, buanderie",
|
||||
"network": "common",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "building",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 20,
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "school",
|
||||
"name": "École / Formation",
|
||||
"description": "Séparation élèves, enseignants, administration",
|
||||
"icon": "🎓",
|
||||
"zones": [
|
||||
{
|
||||
"id": "admin",
|
||||
"name": "Administration",
|
||||
"description": "Direction et services",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "shield",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "teachers",
|
||||
"name": "Enseignants",
|
||||
"description": "Salle des professeurs",
|
||||
"network": "teachers",
|
||||
"color": "#3b82f6",
|
||||
"icon": "chalkboard",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "students",
|
||||
"name": "Élèves",
|
||||
"description": "Salles de classe avec filtrage",
|
||||
"network": "students",
|
||||
"color": "#06b6d4",
|
||||
"icon": "book",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 50,
|
||||
"content_filter": "kids",
|
||||
"time_restrictions": true,
|
||||
"schedule_start": "08:00",
|
||||
"schedule_end": "17:00",
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "lab",
|
||||
"name": "Laboratoire Info",
|
||||
"description": "Postes de travail contrôlés",
|
||||
"network": "lab",
|
||||
"color": "#f59e0b",
|
||||
"icon": "computer",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 100,
|
||||
"priority": "normal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "secure_home",
|
||||
"name": "Maison Sécurisée",
|
||||
"description": "Maximum de segmentation pour sécurité avancée",
|
||||
"icon": "🔒",
|
||||
"zones": [
|
||||
{
|
||||
"id": "trusted",
|
||||
"name": "Confiance Totale",
|
||||
"description": "Appareils principaux uniquement",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "shield-check",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "critical"
|
||||
},
|
||||
{
|
||||
"id": "work",
|
||||
"name": "Télétravail",
|
||||
"description": "Poste de travail professionnel isolé",
|
||||
"network": "work",
|
||||
"color": "#3b82f6",
|
||||
"icon": "briefcase",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "iot_secure",
|
||||
"name": "IoT Sécurisé",
|
||||
"description": "Appareils connectés de confiance",
|
||||
"network": "iot_secure",
|
||||
"color": "#06b6d4",
|
||||
"icon": "lock",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 10,
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "iot_untrusted",
|
||||
"name": "IoT Non Vérifié",
|
||||
"description": "Appareils chinois et non certifiés",
|
||||
"network": "iot_untrusted",
|
||||
"color": "#f59e0b",
|
||||
"icon": "alert",
|
||||
"internet_access": false,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 5,
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "guest",
|
||||
"name": "Invités",
|
||||
"description": "Accès Internet uniquement",
|
||||
"network": "guest",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "users",
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 20,
|
||||
"portal_required": true,
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,51 @@
|
||||
/* SecuBox Admin - Admin-Specific Styles */
|
||||
/* SecuBox Admin - Enhanced SecuBox Theme */
|
||||
|
||||
:root {
|
||||
/* SecuBox Brand Colors */
|
||||
--sb-primary: #6366f1;
|
||||
--sb-primary-light: #818cf8;
|
||||
--sb-primary-dark: #4f46e5;
|
||||
--sb-secondary: #8b5cf6;
|
||||
--sb-secondary-light: #a78bfa;
|
||||
--sb-accent: #3b82f6;
|
||||
--sb-accent-cyan: #06b6d4;
|
||||
--sb-success: #10b981;
|
||||
--sb-warning: #f59e0b;
|
||||
--sb-danger: #ef4444;
|
||||
|
||||
/* Backgrounds */
|
||||
--sb-bg: #f9fafb;
|
||||
--sb-bg-secondary: #ffffff;
|
||||
--sb-bg-tertiary: #f3f4f6;
|
||||
|
||||
/* Text */
|
||||
--sb-text: #1f2937;
|
||||
--sb-text-secondary: #6b7280;
|
||||
--sb-text-dim: #9ca3af;
|
||||
|
||||
/* Borders */
|
||||
--sb-border: #e5e7eb;
|
||||
--sb-border-light: #f3f4f6;
|
||||
|
||||
/* Gradients */
|
||||
--sb-gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
--sb-gradient-accent: linear-gradient(135deg, #3b82f6, #06b6d4);
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
.secubox-admin-dashboard h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@ -31,14 +67,20 @@
|
||||
|
||||
.health-label {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
color: var(--sb-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.health-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: right;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.alerts-section {
|
||||
@ -48,7 +90,7 @@
|
||||
.alerts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
@ -66,17 +108,33 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.actions-grid .btn:hover {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.actions-grid .icon {
|
||||
font-size: 2rem;
|
||||
font-size: 2.5rem;
|
||||
filter: drop-shadow(0 4px 8px rgba(99, 102, 241, 0.2));
|
||||
}
|
||||
|
||||
/* Apps Manager */
|
||||
.secubox-apps-manager h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-filters {
|
||||
@ -87,58 +145,95 @@
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--sb-bg-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
outline: none;
|
||||
border-color: var(--sb-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
min-width: 150px;
|
||||
background: var(--sb-bg-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-filter:focus {
|
||||
outline: none;
|
||||
border-color: var(--sb-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
background: var(--sb-bg-secondary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--sb-gradient-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.15);
|
||||
transform: translateY(-6px);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.app-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: 3rem;
|
||||
font-size: 3.5rem;
|
||||
text-align: center;
|
||||
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.2));
|
||||
}
|
||||
|
||||
.app-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
color: #333;
|
||||
color: var(--sb-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
color: #666;
|
||||
color: var(--sb-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.app-meta {
|
||||
@ -149,46 +244,76 @@
|
||||
}
|
||||
|
||||
.app-category {
|
||||
background-color: #E3F2FD;
|
||||
color: #1976D2;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--sb-primary);
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
background: var(--sb-bg-tertiary);
|
||||
color: var(--sb-text-secondary);
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
background-color: #F5F5F5;
|
||||
color: #666;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.app-actions .btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.app-actions .btn-primary {
|
||||
background: var(--sb-gradient-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.app-actions .btn-primary:hover {
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.secubox-settings h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
padding: 1.5rem;
|
||||
padding: 1.75rem;
|
||||
background: var(--sb-bg-secondary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
@ -207,81 +332,117 @@
|
||||
}
|
||||
|
||||
.app-title .app-icon {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.2));
|
||||
}
|
||||
|
||||
.app-title h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--sb-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.settings-info {
|
||||
color: #666;
|
||||
color: var(--sb-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.settings-info strong {
|
||||
color: #333;
|
||||
color: var(--sb-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Health */
|
||||
.secubox-health h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.health-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 1.5rem;
|
||||
padding: 1.75rem;
|
||||
background: var(--sb-bg-secondary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15);
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.metric-card h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
color: var(--sb-text-secondary);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-value .value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.metric-value .unit {
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
color: var(--sb-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-card.success {
|
||||
border-left: 4px solid #4CAF50;
|
||||
border-left: 4px solid var(--sb-success);
|
||||
}
|
||||
|
||||
.metric-card.success:hover {
|
||||
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.metric-card.warning {
|
||||
border-left: 4px solid #FF9800;
|
||||
border-left: 4px solid var(--sb-warning);
|
||||
}
|
||||
|
||||
.metric-card.warning:hover {
|
||||
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
.metric-card.danger {
|
||||
border-left: 4px solid #f44336;
|
||||
border-left: 4px solid var(--sb-danger);
|
||||
}
|
||||
|
||||
.metric-card.danger:hover {
|
||||
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.health-details {
|
||||
@ -291,25 +452,40 @@
|
||||
.health-details .table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--sb-bg-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.health-details .table th,
|
||||
.health-details .table td {
|
||||
padding: 0.75rem;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--sb-border);
|
||||
}
|
||||
|
||||
.health-details .table th {
|
||||
background-color: #f5f5f5;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--sb-text);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.health-details .table tbody tr:hover {
|
||||
background: rgba(99, 102, 241, 0.04);
|
||||
}
|
||||
|
||||
/* Logs */
|
||||
.secubox-logs h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--sb-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logs-controls {
|
||||
@ -322,24 +498,35 @@
|
||||
.service-selector {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--sb-bg-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-selector:focus {
|
||||
outline: none;
|
||||
border-color: var(--sb-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.logs-viewer {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
background: #0f0f23;
|
||||
color: #e5e7eb;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: 1rem;
|
||||
line-height: 1.6;
|
||||
padding: 1.5rem;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
@ -348,6 +535,131 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Global Enhancements */
|
||||
.card {
|
||||
background: var(--sb-bg-secondary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--sb-gradient-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--sb-primary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--sb-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Progress bars */
|
||||
.progress {
|
||||
height: 10px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: var(--sb-gradient-primary);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 0 12px rgba(99, 102, 241, 0.5);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--sb-primary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--sb-success);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--sb-warning);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--sb-danger);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid,
|
||||
|
||||
@ -1,149 +1,241 @@
|
||||
/* SecuBox Admin - Common Styles */
|
||||
/* SecuBox Admin - Common Styles with SecuBox Theme */
|
||||
|
||||
/* Stat Cards */
|
||||
.stat-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
padding: 1.75rem;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.stat-card.blue { border-left: 4px solid #2196F3; }
|
||||
.stat-card.green { border-left: 4px solid #4CAF50; }
|
||||
.stat-card.success { border-left: 4px solid #8BC34A; }
|
||||
.stat-card.warning { border-left: 4px solid #FF9800; }
|
||||
.stat-card.muted { border-left: 4px solid #9E9E9E; }
|
||||
.stat-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stat-card.blue { border-left: 4px solid #3b82f6; }
|
||||
.stat-card.green { border-left: 4px solid #10b981; }
|
||||
.stat-card.success { border-left: 4px solid #10b981; }
|
||||
.stat-card.warning { border-left: 4px solid #f59e0b; }
|
||||
.stat-card.muted { border-left: 4px solid #9ca3af; }
|
||||
.stat-card.primary { border-left: 4px solid #6366f1; }
|
||||
.stat-card.purple { border-left: 4px solid #8b5cf6; }
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
filter: drop-shadow(0 4px 8px rgba(99, 102, 241, 0.2));
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.75rem;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #FF9800;
|
||||
color: white;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background-color: #757575;
|
||||
color: white;
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #6b7280;
|
||||
border: 1px solid rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
/* Progress Bars */
|
||||
.progress {
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
height: 10px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: #2196F3;
|
||||
transition: width 0.3s ease;
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 0 12px rgba(99, 102, 241, 0.5);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
.progress-bar.success {
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
box-shadow: 0 0 12px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.progress-bar.warning {
|
||||
background: linear-gradient(90deg, #f59e0b, #d97706);
|
||||
box-shadow: 0 0 12px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.progress-bar.danger {
|
||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
color: #333;
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.25rem;
|
||||
position: relative;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #E3F2FD;
|
||||
border-left: 4px solid #2196F3;
|
||||
color: #0D47A1;
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border-left: 4px solid #6366f1;
|
||||
color: #4f46e5;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #FFF3E0;
|
||||
border-left: 4px solid #FF9800;
|
||||
color: #E65100;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border-left: 4px solid #f59e0b;
|
||||
color: #d97706;
|
||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #FFEBEE;
|
||||
border-left: 4px solid #f44336;
|
||||
color: #B71C1C;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-left: 4px solid #ef4444;
|
||||
color: #dc2626;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #E8F5E9;
|
||||
border-left: 4px solid #4CAF50;
|
||||
color: #1B5E20;
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border-left: 4px solid #10b981;
|
||||
color: #059669;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.alert-close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-close:hover {
|
||||
@ -153,17 +245,17 @@
|
||||
/* Loader */
|
||||
.loader-container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #2196F3;
|
||||
border: 4px solid rgba(99, 102, 241, 0.1);
|
||||
border-top: 4px solid #6366f1;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@ -173,95 +265,204 @@
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #f5f5f5;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2196F3;
|
||||
border-color: #2196F3;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1976D2;
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #4CAF50;
|
||||
border-color: #4CAF50;
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #388E3C;
|
||||
background: #059669;
|
||||
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #f44336;
|
||||
border-color: #f44336;
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #D32F2F;
|
||||
background: #dc2626;
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #FF9800;
|
||||
border-color: #FF9800;
|
||||
background: #f59e0b;
|
||||
border-color: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #F57C00;
|
||||
background: #d97706;
|
||||
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-muted {
|
||||
color: #757575;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
color: #2196F3;
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.view-all-link:hover {
|
||||
color: #8b5cf6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: rgba(99, 102, 241, 0.04);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,857 @@
|
||||
/* SecuBox Admin - Enhanced Cyber Console Theme */
|
||||
|
||||
:root {
|
||||
/* SecuBox Brand Colors - Indigo/Purple Gradient System */
|
||||
--cyber-primary: #6366f1;
|
||||
--cyber-primary-light: #818cf8;
|
||||
--cyber-primary-dark: #4f46e5;
|
||||
--cyber-secondary: #8b5cf6;
|
||||
--cyber-secondary-light: #a78bfa;
|
||||
--cyber-accent: #3b82f6;
|
||||
--cyber-accent-cyan: #06b6d4;
|
||||
--cyber-warning: #f59e0b;
|
||||
--cyber-danger: #ef4444;
|
||||
--cyber-success: #10b981;
|
||||
|
||||
/* Backgrounds - Deep dark with subtle purple tint */
|
||||
--cyber-bg: #0f0f23;
|
||||
--cyber-bg-secondary: #1a1a2e;
|
||||
--cyber-bg-tertiary: #16213e;
|
||||
|
||||
/* Borders & Effects */
|
||||
--cyber-border: #6366f1;
|
||||
--cyber-border-dim: #4f46e5;
|
||||
|
||||
/* Text */
|
||||
--cyber-text: #e5e7eb;
|
||||
--cyber-text-bright: #f9fafb;
|
||||
--cyber-text-dim: #9ca3af;
|
||||
|
||||
/* Gradients */
|
||||
--cyber-gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
--cyber-gradient-primary-reverse: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||
--cyber-gradient-accent: linear-gradient(135deg, #3b82f6, #06b6d4);
|
||||
}
|
||||
|
||||
/* Cyberpunk base styles */
|
||||
.cyberpunk-mode {
|
||||
background: var(--cyber-bg);
|
||||
color: var(--cyber-text);
|
||||
font-family: 'Inter', 'Segoe UI', 'Roboto', sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.cyberpunk-mode * {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Dual console layout */
|
||||
.cyber-dual-console {
|
||||
display: grid;
|
||||
grid-template-columns: 420px 1fr;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.cyber-dual-console {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left console - Stats & Quick Actions */
|
||||
.cyber-console-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Right console - Main Content */
|
||||
.cyber-console-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Terminal panel - Enhanced with glass morphism */
|
||||
.cyber-panel {
|
||||
background: rgba(26, 26, 46, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(99, 102, 241, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cyber-panel:hover {
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
box-shadow:
|
||||
0 12px 48px rgba(99, 102, 241, 0.25),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Animated gradient border effect */
|
||||
.cyber-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--cyber-gradient-primary);
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cyber-panel-header {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cyber-panel-title {
|
||||
color: var(--cyber-text-bright);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cyber-panel-title::before {
|
||||
content: '▸';
|
||||
color: var(--cyber-secondary);
|
||||
font-size: 14px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cyber-panel-badge {
|
||||
background: var(--cyber-gradient-primary);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.cyber-panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Stats grid - Enhanced cards */
|
||||
.cyber-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cyber-stat-card {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-left: 4px solid var(--cyber-primary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated background glow */
|
||||
.cyber-stat-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%);
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cyber-stat-card:hover {
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(99, 102, 241, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-4px);
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.cyber-stat-card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cyber-stat-card.warning {
|
||||
border-left-color: var(--cyber-warning);
|
||||
}
|
||||
|
||||
.cyber-stat-card.warning:hover {
|
||||
border-color: rgba(245, 158, 11, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.cyber-stat-card.danger {
|
||||
border-left-color: var(--cyber-danger);
|
||||
}
|
||||
|
||||
.cyber-stat-card.danger:hover {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.cyber-stat-card.accent {
|
||||
border-left-color: var(--cyber-accent-cyan);
|
||||
}
|
||||
|
||||
.cyber-stat-card.accent:hover {
|
||||
border-color: rgba(6, 182, 212, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
.cyber-stat-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
filter: drop-shadow(0 2px 8px currentColor);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cyber-stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
background: var(--cyber-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cyber-stat-label {
|
||||
font-size: 10px;
|
||||
color: var(--cyber-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Quick actions - Modern button style */
|
||||
.cyber-quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cyber-action-btn {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
color: var(--cyber-text);
|
||||
padding: 16px 18px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Gradient slide effect on hover */
|
||||
.cyber-action-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--cyber-gradient-primary);
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.cyber-action-btn:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.cyber-action-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--cyber-gradient-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.cyber-action-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.2);
|
||||
transform: translateX(4px);
|
||||
color: var(--cyber-text-bright);
|
||||
}
|
||||
|
||||
.cyber-action-btn:hover::after {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.cyber-action-btn:active {
|
||||
transform: translateX(4px) scale(0.98);
|
||||
}
|
||||
|
||||
.cyber-action-icon {
|
||||
font-size: 20px;
|
||||
filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.3));
|
||||
}
|
||||
|
||||
.cyber-action-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cyber-action-arrow {
|
||||
color: var(--cyber-secondary);
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cyber-action-btn:hover .cyber-action-arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* System status - Enhanced metrics */
|
||||
.cyber-system-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cyber-metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cyber-metric-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cyber-metric-label {
|
||||
color: var(--cyber-text-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cyber-metric-value {
|
||||
background: var(--cyber-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cyber-progress-bar {
|
||||
height: 10px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cyber-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--cyber-gradient-primary);
|
||||
box-shadow: 0 0 12px rgba(99, 102, 241, 0.5);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Animated shimmer effect */
|
||||
.cyber-progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.4) 50%,
|
||||
transparent 100%);
|
||||
animation: shimmer 2.5s infinite;
|
||||
}
|
||||
|
||||
.cyber-progress-fill.warning {
|
||||
background: linear-gradient(90deg, var(--cyber-warning), var(--cyber-danger));
|
||||
box-shadow: 0 0 12px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.cyber-progress-fill.danger {
|
||||
background: linear-gradient(90deg, var(--cyber-danger), #dc2626);
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
/* System meta info */
|
||||
.cyber-system-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.cyber-system-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cyber-system-meta-item .label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--cyber-text-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cyber-system-meta-item .value {
|
||||
font-size: 12px;
|
||||
color: var(--cyber-text-bright);
|
||||
font-weight: 600;
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
/* List view - Enhanced cards */
|
||||
.cyber-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cyber-list-item {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-left: 4px solid var(--cyber-primary);
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cyber-list-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--cyber-gradient-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cyber-list-item:hover {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.cyber-list-item:hover::after {
|
||||
opacity: 0.03;
|
||||
}
|
||||
|
||||
.cyber-list-item.active {
|
||||
border-left-color: var(--cyber-accent-cyan);
|
||||
background: rgba(6, 182, 212, 0.08);
|
||||
}
|
||||
|
||||
.cyber-list-item.offline {
|
||||
border-left-color: var(--cyber-text-dim);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cyber-list-icon {
|
||||
font-size: 36px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 10px;
|
||||
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.2));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cyber-list-item:hover .cyber-list-icon {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cyber-list-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cyber-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--cyber-text-bright);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cyber-list-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
color: var(--cyber-text-dim);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cyber-list-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cyber-list-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Buttons - Modern style */
|
||||
.cyber-btn {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
color: var(--cyber-text);
|
||||
padding: 10px 16px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cyber-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
color: var(--cyber-text-bright);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cyber-btn:active {
|
||||
transform: translateY(-2px) scale(0.95);
|
||||
}
|
||||
|
||||
.cyber-btn.primary {
|
||||
border-color: var(--cyber-primary);
|
||||
color: var(--cyber-primary);
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
.cyber-btn.primary:hover {
|
||||
background: var(--cyber-gradient-primary);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.cyber-btn.danger {
|
||||
border-color: var(--cyber-danger);
|
||||
color: var(--cyber-danger);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.cyber-btn.danger:hover {
|
||||
background: var(--cyber-danger);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Badges - Enhanced with gradients */
|
||||
.cyber-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cyber-badge.success {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
color: var(--cyber-success);
|
||||
}
|
||||
|
||||
.cyber-badge.warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
color: var(--cyber-warning);
|
||||
}
|
||||
|
||||
.cyber-badge.danger {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: var(--cyber-danger);
|
||||
}
|
||||
|
||||
.cyber-badge.info {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: 1px solid rgba(99, 102, 241, 0.4);
|
||||
color: var(--cyber-primary);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(200%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scan lines effect - Subtle */
|
||||
.cyber-scanlines {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cyber-scanlines::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(99, 102, 241, 0.03) 0px,
|
||||
transparent 1px,
|
||||
transparent 2px,
|
||||
rgba(99, 102, 241, 0.03) 3px
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Text glow effect */
|
||||
.cyber-text-glow {
|
||||
text-shadow:
|
||||
0 0 10px rgba(99, 102, 241, 0.5),
|
||||
0 0 20px rgba(99, 102, 241, 0.3),
|
||||
0 0 30px rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Status indicators */
|
||||
.cyber-status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
animation: pulse 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cyber-status-dot.online {
|
||||
background: var(--cyber-success);
|
||||
box-shadow: 0 0 12px var(--cyber-success);
|
||||
}
|
||||
|
||||
.cyber-status-dot.offline {
|
||||
background: var(--cyber-text-dim);
|
||||
animation: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cyber-status-dot.warning {
|
||||
background: var(--cyber-warning);
|
||||
box-shadow: 0 0 12px var(--cyber-warning);
|
||||
}
|
||||
|
||||
/* Scrollbar - SecuBox styled */
|
||||
.cyberpunk-mode ::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.cyberpunk-mode ::-webkit-scrollbar-track {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cyberpunk-mode ::-webkit-scrollbar-thumb {
|
||||
background: var(--cyber-gradient-primary);
|
||||
box-shadow: 0 0 12px rgba(99, 102, 241, 0.4);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.cyberpunk-mode ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--cyber-gradient-primary-reverse);
|
||||
box-shadow: 0 0 16px rgba(139, 92, 246, 0.6);
|
||||
}
|
||||
|
||||
/* Header with ASCII art - Enhanced */
|
||||
.cyber-header {
|
||||
background: rgba(26, 26, 46, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
margin: 24px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(99, 102, 241, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cyber-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--cyber-gradient-primary);
|
||||
}
|
||||
|
||||
.cyber-ascii-art {
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
background: var(--cyber-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cyber-header-title {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
background: var(--cyber-gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cyber-header-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--cyber-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.cyber-dual-console {
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cyber-header {
|
||||
margin: 16px;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.cyber-ascii-art {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.cyber-header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.cyber-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cyber-system-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -36,9 +36,27 @@ WidgetRendererInstance.prototype = {
|
||||
this.registerTemplate('default', {
|
||||
render: function(container, app, data) {
|
||||
container.innerHTML = '';
|
||||
|
||||
var status = data && data.status ? data.status : 'unknown';
|
||||
var statusClass = status === 'running' ? 'status-success' :
|
||||
status === 'stopped' ? 'status-warning' :
|
||||
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
||||
|
||||
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
||||
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
||||
|
||||
container.appendChild(E('div', { 'class': 'widget-default' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '📊'),
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Application'),
|
||||
E('div', { 'class': 'widget-header' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '📊'),
|
||||
E('div', { 'class': 'widget-title-wrapper' }, [
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Application'),
|
||||
version ? E('div', { 'class': 'widget-version' }, version) : null
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-text' }, [
|
||||
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
||||
]),
|
||||
E('div', { 'class': 'widget-status' },
|
||||
data && data.widget_enabled ? 'Widget Enabled' : 'No widget data'
|
||||
)
|
||||
@ -52,16 +70,25 @@ WidgetRendererInstance.prototype = {
|
||||
|
||||
var metrics = data && data.metrics ? data.metrics : [];
|
||||
var status = data && data.status ? data.status : 'unknown';
|
||||
var statusClass = status === 'ok' ? 'status-success' :
|
||||
status === 'warning' ? 'status-warning' :
|
||||
status === 'error' ? 'status-error' : 'status-unknown';
|
||||
var statusClass = status === 'running' ? 'status-success' :
|
||||
status === 'stopped' ? 'status-warning' :
|
||||
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
||||
|
||||
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
||||
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
||||
|
||||
container.appendChild(E('div', { 'class': 'widget-security' }, [
|
||||
E('div', { 'class': 'widget-header' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '🔒'),
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Security'),
|
||||
E('div', { 'class': 'widget-title-wrapper' }, [
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Security'),
|
||||
version ? E('div', { 'class': 'widget-version' }, version) : null
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-text' }, [
|
||||
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
||||
]),
|
||||
E('div', { 'class': 'widget-metrics' },
|
||||
metrics.map(function(metric) {
|
||||
return self.renderMetric(metric || {});
|
||||
@ -83,10 +110,25 @@ WidgetRendererInstance.prototype = {
|
||||
var connections = data && data.active_connections ? data.active_connections : 0;
|
||||
var bandwidth = data && data.bandwidth ? data.bandwidth : { up: 0, down: 0 };
|
||||
|
||||
var status = data && data.status ? data.status : 'unknown';
|
||||
var statusClass = status === 'running' ? 'status-success' :
|
||||
status === 'stopped' ? 'status-warning' :
|
||||
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
||||
|
||||
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
||||
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
||||
|
||||
container.appendChild(E('div', { 'class': 'widget-network' }, [
|
||||
E('div', { 'class': 'widget-header' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '🌐'),
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Network')
|
||||
E('div', { 'class': 'widget-title-wrapper' }, [
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Network'),
|
||||
version ? E('div', { 'class': 'widget-version' }, version) : null
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-text' }, [
|
||||
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
||||
]),
|
||||
E('div', { 'class': 'widget-metrics' }, [
|
||||
E('div', { 'class': 'metric-row' }, [
|
||||
@ -112,15 +154,24 @@ WidgetRendererInstance.prototype = {
|
||||
|
||||
var metrics = data && data.metrics ? data.metrics : [];
|
||||
var status = data && data.status ? data.status : 'unknown';
|
||||
var statusClass = status === 'healthy' ? 'status-success' :
|
||||
status === 'degraded' ? 'status-warning' :
|
||||
status === 'down' ? 'status-error' : 'status-unknown';
|
||||
var statusClass = status === 'running' ? 'status-success' :
|
||||
status === 'stopped' ? 'status-warning' :
|
||||
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
||||
|
||||
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
||||
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
||||
|
||||
container.appendChild(E('div', { 'class': 'widget-monitoring' }, [
|
||||
E('div', { 'class': 'widget-header' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '📈'),
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'),
|
||||
E('div', { 'class': 'widget-status-badge ' + statusClass }, status)
|
||||
E('div', { 'class': 'widget-title-wrapper' }, [
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'),
|
||||
version ? E('div', { 'class': 'widget-version' }, version) : null
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-text' }, [
|
||||
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
||||
]),
|
||||
E('div', { 'class': 'widget-metrics-grid' },
|
||||
metrics.map(function(metric) {
|
||||
@ -142,10 +193,25 @@ WidgetRendererInstance.prototype = {
|
||||
var metrics = data && data.metrics ? data.metrics : [];
|
||||
var services = data && data.services ? data.services : [];
|
||||
|
||||
var status = data && data.status ? data.status : 'unknown';
|
||||
var statusClass = status === 'running' ? 'status-success' :
|
||||
status === 'stopped' ? 'status-warning' :
|
||||
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
||||
|
||||
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
||||
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
||||
|
||||
container.appendChild(E('div', { 'class': 'widget-hosting' }, [
|
||||
E('div', { 'class': 'widget-header' }, [
|
||||
E('div', { 'class': 'widget-icon' }, app.icon || '🖥️'),
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Hosting')
|
||||
E('div', { 'class': 'widget-title-wrapper' }, [
|
||||
E('div', { 'class': 'widget-title' }, app.name || 'Hosting'),
|
||||
version ? E('div', { 'class': 'widget-version' }, version) : null
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
||||
]),
|
||||
E('div', { 'class': 'widget-status-text' }, [
|
||||
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
||||
]),
|
||||
E('div', { 'class': 'widget-services' },
|
||||
services.map(function(service) {
|
||||
|
||||
@ -507,3 +507,121 @@
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Widget Header with Version and Status */
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.widget-title-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--cyber-text, #1e293b);
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.widget-version {
|
||||
font-size: 0.75rem;
|
||||
color: var(--cyber-text-dim, #64748b);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Widget Status Indicator (Dot) */
|
||||
.widget-status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.widget-status-indicator.status-success {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2), 0 0 10px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.widget-status-indicator.status-warning {
|
||||
background: #f59e0b;
|
||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2), 0 0 10px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.widget-status-indicator.status-error {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2), 0 0 10px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.widget-status-indicator.status-unknown {
|
||||
background: #6b7280;
|
||||
box-shadow: 0 0 0 2px rgba(107, 116, 128, 0.2);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Widget Status Text Badge */
|
||||
.widget-status-text {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-badge.status-success {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.15));
|
||||
color: #059669;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.status-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(217, 119, 6, 0.15));
|
||||
color: #d97706;
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.status-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.15));
|
||||
color: #dc2626;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.status-unknown {
|
||||
background: rgba(107, 116, 128, 0.1);
|
||||
color: #6b7280;
|
||||
border: 1px solid rgba(107, 116, 128, 0.2);
|
||||
}
|
||||
|
||||
/* Enhanced widget icon */
|
||||
.widget-icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -42,6 +42,9 @@ return view.extend({
|
||||
var self = this;
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
|
||||
// Header
|
||||
E('div', { 'class': 'cyber-header cyber-scanlines' }, [
|
||||
E('div', { 'class': 'cyber-header-title cyber-text-glow' }, '⚙️ ADVANCED SETTINGS'),
|
||||
|
||||
@ -105,6 +105,8 @@ return view.extend({
|
||||
});
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-apps-manager' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
@ -371,9 +373,9 @@ return view.extend({
|
||||
E('div', { 'class': 'cyber-featured-app-tags' },
|
||||
(app.tags || []).slice(0, 2).map(function(tag) {
|
||||
return E('span', { 'class': 'cyber-featured-app-tag' }, tag);
|
||||
})
|
||||
}).filter(Boolean)
|
||||
),
|
||||
E('div', { 'class': 'cyber-featured-app-action' }, [
|
||||
E('div', { 'class': 'cyber-featured-app-action' },
|
||||
isInstalled ? [
|
||||
E('span', { 'style': 'color: var(--cyber-success);' }, '✓ Installed'),
|
||||
' → ',
|
||||
@ -388,7 +390,7 @@ return view.extend({
|
||||
E('span', {}, 'Install now'),
|
||||
' →'
|
||||
]
|
||||
])
|
||||
)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
@ -71,6 +71,8 @@ return view.extend({
|
||||
var enabledCount = sources.filter(function(s) { return s.enabled; }).length;
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-catalog-sources' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
|
||||
@ -31,16 +31,19 @@ return view.extend({
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
|
||||
var container = E('div', { 'class': 'control-center' });
|
||||
var container = E('div', { 'class': 'cyberpunk-mode control-center' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/admin.css') }),
|
||||
|
||||
// Page header
|
||||
var header = E('div', { 'class': 'page-header', 'style': 'margin-bottom: 2rem;' });
|
||||
var title = E('h2', {}, 'SecuBox Admin Control Center');
|
||||
var subtitle = E('p', { 'style': 'color: #6b7280; margin-top: 0.5rem;' },
|
||||
'Centralized management dashboard for components and system state');
|
||||
header.appendChild(title);
|
||||
header.appendChild(subtitle);
|
||||
container.appendChild(header);
|
||||
E('div', { 'class': 'cyber-header' }, [
|
||||
E('div', { 'class': 'cyber-header-title' }, '🎛️ CONTROL CENTER'),
|
||||
E('div', { 'class': 'cyber-header-subtitle' }, 'Centralized management dashboard for components and system state')
|
||||
])
|
||||
]);
|
||||
|
||||
// System Overview Panel
|
||||
var overviewPanel = this.renderSystemOverview(data.health, data.statistics);
|
||||
|
||||
@ -29,6 +29,10 @@ return view.extend({
|
||||
var self = this;
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode' }, [
|
||||
// Load cyberpunk CSS
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
|
||||
// ASCII Art Header
|
||||
E('div', { 'class': 'cyber-header cyber-scanlines' }, [
|
||||
E('pre', { 'class': 'cyber-ascii-art' },
|
||||
|
||||
@ -29,7 +29,9 @@ return view.extend({
|
||||
var stats = DataUtils.buildAppStats(apps, modules, alerts, updateInfo, API.getAppStatus);
|
||||
var healthSnapshot = DataUtils.normalizeHealth(health);
|
||||
|
||||
var container = E('div', { 'class': 'secubox-admin-dashboard' }, [
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-admin-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
@ -37,7 +39,10 @@ return view.extend({
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/widgets.css') }),
|
||||
|
||||
E('h2', {}, 'Admin Control Panel'),
|
||||
E('div', { 'class': 'cyber-header' }, [
|
||||
E('div', { 'class': 'cyber-header-title' }, '🎛️ ADMIN CONTROL PANEL'),
|
||||
E('div', { 'class': 'cyber-header-subtitle' }, 'System Overview · Applications · Health Monitoring')
|
||||
]),
|
||||
|
||||
// Stats grid
|
||||
E('div', { 'class': 'stats-grid' }, [
|
||||
|
||||
@ -15,14 +15,18 @@ return view.extend({
|
||||
var snapshot = DataUtils.normalizeHealth(health);
|
||||
this.currentHealth = snapshot;
|
||||
|
||||
var container = E('div', { 'class': 'secubox-health' }, [
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-health' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/admin.css') }),
|
||||
|
||||
E('h2', {}, 'System Health'),
|
||||
E('p', {}, 'Monitor system resources and performance'),
|
||||
E('div', { 'class': 'cyber-header' }, [
|
||||
E('div', { 'class': 'cyber-header-title' }, '💊 SYSTEM HEALTH'),
|
||||
E('div', { 'class': 'cyber-header-subtitle' }, 'Monitor system resources and performance')
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'health-cards' }, [
|
||||
this.renderMetricCard('CPU Usage', snapshot.cpuUsage || 0, '%', 'cpu'),
|
||||
|
||||
@ -14,14 +14,18 @@ return view.extend({
|
||||
var self = this;
|
||||
var logs = logsData.logs || '';
|
||||
|
||||
var container = E('div', { 'class': 'secubox-logs' }, [
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-logs' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/admin.css') }),
|
||||
|
||||
E('h2', {}, 'System Logs'),
|
||||
E('p', {}, 'View logs from system services and applications'),
|
||||
E('div', { 'class': 'cyber-header' }, [
|
||||
E('div', { 'class': 'cyber-header-title' }, '📋 SYSTEM LOGS'),
|
||||
E('div', { 'class': 'cyber-header-subtitle' }, 'View logs from system services and applications')
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'logs-controls' }, [
|
||||
E('select', {
|
||||
|
||||
@ -24,14 +24,18 @@ return view.extend({
|
||||
return status.installed;
|
||||
});
|
||||
|
||||
var container = E('div', { 'class': 'secubox-settings' }, [
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-settings' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/admin.css') }),
|
||||
|
||||
E('h2', {}, 'App Settings'),
|
||||
E('p', {}, 'Configure installed applications'),
|
||||
E('div', { 'class': 'cyber-header' }, [
|
||||
E('div', { 'class': 'cyber-header-title' }, '⚙️ APP SETTINGS'),
|
||||
E('div', { 'class': 'cyber-header-subtitle' }, 'Configure installed applications')
|
||||
]),
|
||||
|
||||
installedApps.length === 0 ?
|
||||
E('div', { 'class': 'alert alert-info' }, 'No installed apps') :
|
||||
|
||||
@ -86,6 +86,8 @@ return view.extend({
|
||||
console.log('[UPDATES-DEBUG] ========== RENDER PROCESSING ==========');
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode secubox-updates' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
'href': L.resource('secubox-admin/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet',
|
||||
|
||||
27
package/secubox/luci-app-secubox-security-threats/Makefile
Normal file
27
package/secubox/luci-app-secubox-security-threats/Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2026 CyberMind.fr
|
||||
# Licensed under Apache-2.0
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-secubox-security-threats
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
LUCI_TITLE:=SecuBox Security Threats Dashboard
|
||||
LUCI_DESCRIPTION:=Unified dashboard integrating netifyd DPI threats with CrowdSec intelligence for real-time threat monitoring and automated blocking
|
||||
LUCI_DEPENDS:=+luci-base +rpcd +netifyd +crowdsec +jq +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
# File permissions (CRITICAL: RPCD scripts MUST be executable 755)
|
||||
# Format: path:owner:group:mode
|
||||
# - RPCD scripts: 755 (executable by root, required for ubus calls)
|
||||
# - Config files: 644 (readable by all, writable by root)
|
||||
# - CSS/JS files: 644 (set automatically by luci.mk)
|
||||
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.secubox-security-threats:root:root:755
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
191
package/secubox/luci-app-secubox-security-threats/README.md
Normal file
191
package/secubox/luci-app-secubox-security-threats/README.md
Normal file
@ -0,0 +1,191 @@
|
||||
# SecuBox Security Threats Dashboard
|
||||
|
||||
## Overview
|
||||
|
||||
A unified LuCI dashboard that integrates **netifyd DPI security risks** with **CrowdSec threat intelligence** for comprehensive network threat monitoring and automated blocking.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Threat Detection**: Monitors netifyd's 52 security risk types
|
||||
- **CrowdSec Integration**: Correlates with CrowdSec alerts and decisions
|
||||
- **Risk Scoring**: Calculates 0-100 risk scores based on multiple factors
|
||||
- **Auto-blocking**: Configurable rules for automatic threat blocking
|
||||
- **Per-host Analysis**: Track threats by IP address
|
||||
- **Visual Dashboard**: Stats, charts, and real-time threat table
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
netifyd DPI Engine → RPCD Backend → ubus API → LuCI Dashboard
|
||||
↓
|
||||
CrowdSec LAPI
|
||||
↓
|
||||
nftables (blocking)
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `luci-base`: LuCI framework
|
||||
- `rpcd`: Remote Procedure Call daemon
|
||||
- `netifyd`: Deep Packet Inspection engine
|
||||
- `crowdsec`: Threat intelligence and blocking
|
||||
- `jq`: JSON processing
|
||||
- `jsonfilter`: UCI-compatible JSON filtering
|
||||
|
||||
## Installation
|
||||
|
||||
1. Build the package:
|
||||
```bash
|
||||
cd /path/to/openwrt
|
||||
make package/secubox/luci-app-secubox-security-threats/compile
|
||||
```
|
||||
|
||||
2. Install on router:
|
||||
```bash
|
||||
opkg install luci-app-secubox-security-threats_*.ipk
|
||||
```
|
||||
|
||||
3. Restart services:
|
||||
```bash
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Access Dashboard
|
||||
|
||||
Navigate to: **Admin → SecuBox → Security → Threat Monitor → Dashboard**
|
||||
|
||||
### Configure Auto-block Rules
|
||||
|
||||
Edit `/etc/config/secubox_security_threats`:
|
||||
|
||||
```uci
|
||||
config block_rule 'my_rule'
|
||||
option name 'Block Malware'
|
||||
option enabled '1'
|
||||
option threat_types 'malware'
|
||||
option duration '24h'
|
||||
option threshold '60'
|
||||
```
|
||||
|
||||
Apply changes:
|
||||
```bash
|
||||
uci commit secubox_security_threats
|
||||
```
|
||||
|
||||
### Manual Blocking
|
||||
|
||||
Via dashboard:
|
||||
1. Click "Block" button next to threat
|
||||
2. Confirm action
|
||||
3. IP will be blocked via CrowdSec
|
||||
|
||||
Via CLI:
|
||||
```bash
|
||||
ubus call luci.secubox-security-threats block_threat '{"ip":"192.168.1.100","duration":"4h","reason":"Test"}'
|
||||
```
|
||||
|
||||
### Whitelist Host
|
||||
|
||||
```bash
|
||||
ubus call luci.secubox-security-threats whitelist_host '{"ip":"192.168.1.100","reason":"Admin workstation"}'
|
||||
```
|
||||
|
||||
## Risk Scoring Algorithm
|
||||
|
||||
**Base Score (0-50):** risk_count × 10 (capped)
|
||||
|
||||
**Severity Weights:**
|
||||
- Malware indicators (MALICIOUS_JA3, DGA): +20
|
||||
- Web attacks (SQL injection, XSS): +15
|
||||
- Network anomalies (RISKY_ASN, DNS tunneling): +10
|
||||
- Protocol threats (BitTorrent, Mining): +5
|
||||
|
||||
**CrowdSec Correlation:**
|
||||
- Active decision: +30
|
||||
|
||||
**Severity Levels:**
|
||||
- Critical: ≥80
|
||||
- High: 60-79
|
||||
- Medium: 40-59
|
||||
- Low: <40
|
||||
|
||||
## Threat Categories
|
||||
|
||||
- **malware**: Malicious JA3, DGA domains, suspicious entropy
|
||||
- **web_attack**: SQL injection, XSS, RCE attempts
|
||||
- **anomaly**: DNS tunneling, risky ASNs, unidirectional traffic
|
||||
- **protocol**: BitTorrent, mining, Tor, unauthorized protocols
|
||||
- **tls_issue**: Certificate problems, weak ciphers
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend (ubus CLI)
|
||||
```bash
|
||||
# Test status
|
||||
ubus call luci.secubox-security-threats status
|
||||
|
||||
# Get active threats
|
||||
ubus call luci.secubox-security-threats get_active_threats
|
||||
|
||||
# Test blocking
|
||||
ubus call luci.secubox-security-threats block_threat '{"ip":"192.168.1.100","duration":"4h","reason":"Test"}'
|
||||
|
||||
# Verify in CrowdSec
|
||||
cscli decisions list
|
||||
```
|
||||
|
||||
### Frontend
|
||||
1. Navigate to dashboard in LuCI
|
||||
2. Verify stats cards display
|
||||
3. Verify threats table populates
|
||||
4. Test "Block" button
|
||||
5. Check real-time polling (10s refresh)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No threats detected
|
||||
- Check if netifyd is running: `ps | grep netifyd`
|
||||
- Verify netifyd data: `cat /var/run/netifyd/status.json`
|
||||
- Enable netifyd risk detection in config
|
||||
|
||||
### Auto-blocking not working
|
||||
- Check if auto-blocking is enabled: `uci get secubox_security_threats.global.auto_block_enabled`
|
||||
- Verify block rules are enabled: `uci show secubox_security_threats`
|
||||
- Check logs: `logread | grep security-threats`
|
||||
|
||||
### CrowdSec integration issues
|
||||
- Check if CrowdSec is running: `ps | grep crowdsec`
|
||||
- Test cscli: `cscli version`
|
||||
- Verify permissions: `ls -l /usr/bin/cscli`
|
||||
|
||||
## Files
|
||||
|
||||
**Backend:**
|
||||
- `/usr/libexec/rpcd/luci.secubox-security-threats` - RPCD backend (mode 755)
|
||||
- `/etc/config/secubox_security_threats` - UCI configuration
|
||||
|
||||
**Frontend:**
|
||||
- `/www/luci-static/resources/secubox-security-threats/api.js` - API wrapper
|
||||
- `/www/luci-static/resources/view/secubox-security-threats/dashboard.js` - Dashboard view
|
||||
|
||||
**Configuration:**
|
||||
- `/usr/share/luci/menu.d/luci-app-secubox-security-threats.json` - Menu
|
||||
- `/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json` - Permissions
|
||||
|
||||
**Runtime:**
|
||||
- `/tmp/secubox-threats-history.json` - Threat history (volatile)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
## Authors
|
||||
|
||||
CyberMind.fr - Gandalf
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0 (2026-01-07)
|
||||
@ -0,0 +1,255 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
// ==============================================================================
|
||||
// RPC Method Declarations
|
||||
// ==============================================================================
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'status',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callGetActiveThreats = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'get_active_threats',
|
||||
expect: { threats: [] }
|
||||
});
|
||||
|
||||
var callGetThreatHistory = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'get_threat_history',
|
||||
params: ['hours'],
|
||||
expect: { threats: [] }
|
||||
});
|
||||
|
||||
var callGetStatsByType = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'get_stats_by_type',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callGetStatsByHost = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'get_stats_by_host',
|
||||
expect: { hosts: [] }
|
||||
});
|
||||
|
||||
var callGetBlockedIPs = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'get_blocked_ips',
|
||||
expect: { blocked: [] }
|
||||
});
|
||||
|
||||
var callBlockThreat = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'block_threat',
|
||||
params: ['ip', 'duration', 'reason'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callWhitelistHost = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'whitelist_host',
|
||||
params: ['ip', 'reason'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callRemoveWhitelist = rpc.declare({
|
||||
object: 'luci.secubox-security-threats',
|
||||
method: 'remove_whitelist',
|
||||
params: ['ip'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
// ==============================================================================
|
||||
// Utility Functions
|
||||
// ==============================================================================
|
||||
|
||||
/**
|
||||
* Get color for severity level
|
||||
* @param {string} severity - Severity level (critical, high, medium, low)
|
||||
* @returns {string} Hex color code
|
||||
*/
|
||||
function getSeverityColor(severity) {
|
||||
var colors = {
|
||||
'critical': '#d32f2f', // Red
|
||||
'high': '#ff5722', // Deep Orange
|
||||
'medium': '#ff9800', // Orange
|
||||
'low': '#ffc107' // Amber
|
||||
};
|
||||
return colors[severity] || '#666';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon for threat category
|
||||
* @param {string} category - Threat category
|
||||
* @returns {string} Unicode emoji icon
|
||||
*/
|
||||
function getThreatIcon(category) {
|
||||
var icons = {
|
||||
'malware': '🦠',
|
||||
'web_attack': '⚔️',
|
||||
'anomaly': '⚠️',
|
||||
'protocol': '🚫',
|
||||
'tls_issue': '🔒',
|
||||
'other': '❓'
|
||||
};
|
||||
return icons[category] || '❓';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format risk flags for display
|
||||
* @param {Array} risks - Array of risk flag names
|
||||
* @returns {string} Formatted risk flags
|
||||
*/
|
||||
function formatRiskFlags(risks) {
|
||||
if (!risks || !Array.isArray(risks)) return 'N/A';
|
||||
|
||||
return risks.map(function(risk) {
|
||||
// Convert MALICIOUS_JA3 to "Malicious JA3"
|
||||
return risk.toString().split('_').map(function(word) {
|
||||
return word.charAt(0) + word.slice(1).toLowerCase();
|
||||
}).join(' ');
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable category label
|
||||
* @param {string} category - Category code
|
||||
* @returns {string} Display label
|
||||
*/
|
||||
function getCategoryLabel(category) {
|
||||
var labels = {
|
||||
'malware': 'Malware',
|
||||
'web_attack': 'Web Attack',
|
||||
'anomaly': 'Network Anomaly',
|
||||
'protocol': 'Protocol Threat',
|
||||
'tls_issue': 'TLS/Certificate',
|
||||
'other': 'Other'
|
||||
};
|
||||
return labels[category] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration string (4h, 24h, etc.)
|
||||
* @param {string} duration - Duration string
|
||||
* @returns {string} Formatted duration
|
||||
*/
|
||||
function formatDuration(duration) {
|
||||
if (!duration) return 'N/A';
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to localized string
|
||||
* @param {string} timestamp - ISO 8601 timestamp
|
||||
* @returns {string} Formatted timestamp
|
||||
*/
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
try {
|
||||
var date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
} catch(e) {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format relative time (e.g., "5 minutes ago")
|
||||
* @param {string} timestamp - ISO 8601 timestamp
|
||||
* @returns {string} Relative time string
|
||||
*/
|
||||
function formatRelativeTime(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
try {
|
||||
var date = new Date(timestamp);
|
||||
var now = new Date();
|
||||
var seconds = Math.floor((now - date) / 1000);
|
||||
|
||||
if (seconds < 60) return seconds + 's ago';
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
||||
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
||||
return Math.floor(seconds / 86400) + 'd ago';
|
||||
} catch(e) {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable size
|
||||
* @param {number} bytes - Byte count
|
||||
* @returns {string} Formatted size (e.g., "1.5 MB")
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
var k = 1024;
|
||||
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get badge HTML for severity
|
||||
* @param {string} severity - Severity level
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function getSeverityBadge(severity) {
|
||||
var color = getSeverityColor(severity);
|
||||
var label = severity.charAt(0).toUpperCase() + severity.slice(1);
|
||||
return '<span style="display: inline-block; padding: 2px 8px; border-radius: 3px; background: ' + color + '; color: white; font-size: 0.85em; font-weight: bold;">' + label + '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite data fetcher for dashboard
|
||||
* @returns {Promise} Promise resolving to dashboard data
|
||||
*/
|
||||
function getDashboardData() {
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callGetActiveThreats(),
|
||||
callGetStatsByType(),
|
||||
callGetBlockedIPs()
|
||||
]).then(function(results) {
|
||||
return {
|
||||
status: results[0] || {},
|
||||
threats: results[1].threats || [],
|
||||
stats: results[2] || {},
|
||||
blocked: results[3].blocked || []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// Exports
|
||||
// ==============================================================================
|
||||
|
||||
return baseclass.extend({
|
||||
// RPC Methods
|
||||
getStatus: callStatus,
|
||||
getActiveThreats: callGetActiveThreats,
|
||||
getThreatHistory: callGetThreatHistory,
|
||||
getStatsByType: callGetStatsByType,
|
||||
getStatsByHost: callGetStatsByHost,
|
||||
getBlockedIPs: callGetBlockedIPs,
|
||||
blockThreat: callBlockThreat,
|
||||
whitelistHost: callWhitelistHost,
|
||||
removeWhitelist: callRemoveWhitelist,
|
||||
|
||||
// Utility Functions
|
||||
getSeverityColor: getSeverityColor,
|
||||
getThreatIcon: getThreatIcon,
|
||||
formatRiskFlags: formatRiskFlags,
|
||||
getCategoryLabel: getCategoryLabel,
|
||||
formatDuration: formatDuration,
|
||||
formatTimestamp: formatTimestamp,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
formatBytes: formatBytes,
|
||||
getSeverityBadge: getSeverityBadge,
|
||||
|
||||
// Composite Fetchers
|
||||
getDashboardData: getDashboardData
|
||||
});
|
||||
@ -0,0 +1,306 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require secubox-security-threats/api as API';
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return API.getDashboardData();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
data = data || {};
|
||||
var threats = data.threats || [];
|
||||
var status = data.status || {};
|
||||
var stats = data.stats || {};
|
||||
var blocked = data.blocked || [];
|
||||
|
||||
// Calculate statistics
|
||||
var threatStats = {
|
||||
total: threats.length,
|
||||
critical: threats.filter(function(t) { return t.severity === 'critical'; }).length,
|
||||
high: threats.filter(function(t) { return t.severity === 'high'; }).length,
|
||||
medium: threats.filter(function(t) { return t.severity === 'medium'; }).length,
|
||||
low: threats.filter(function(t) { return t.severity === 'low'; }).length,
|
||||
avg_score: threats.length > 0 ?
|
||||
Math.round(threats.reduce(function(sum, t) { return sum + t.risk_score; }, 0) / threats.length) : 0
|
||||
};
|
||||
|
||||
// Build view elements
|
||||
var statusBanner = this.renderStatusBanner(status);
|
||||
var statsGrid = this.renderStatsGrid(threatStats, blocked.length);
|
||||
var threatDist = this.renderThreatDistribution(stats);
|
||||
var riskGauge = this.renderRiskGauge(threatStats.avg_score);
|
||||
var threatsTable = this.renderThreatsTable(threats.slice(0, 10));
|
||||
|
||||
// Setup auto-refresh polling (every 10 seconds)
|
||||
poll.add(L.bind(function() {
|
||||
this.handleRefresh();
|
||||
}, this), 10);
|
||||
|
||||
// Return the complete view
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Security Threats Dashboard')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Real-time threat detection integrating netifyd DPI and CrowdSec intelligence')),
|
||||
statusBanner,
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Overview')),
|
||||
statsGrid
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;' }, [
|
||||
threatDist,
|
||||
riskGauge
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Recent Threats')),
|
||||
threatsTable
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderStatusBanner: function(status) {
|
||||
var services = [];
|
||||
var hasIssue = false;
|
||||
|
||||
if (!status.netifyd_running) {
|
||||
services.push('netifyd is not running');
|
||||
hasIssue = true;
|
||||
}
|
||||
if (!status.crowdsec_running) {
|
||||
services.push('CrowdSec is not running');
|
||||
hasIssue = true;
|
||||
}
|
||||
|
||||
if (!hasIssue) {
|
||||
return E('div', {
|
||||
'class': 'alert-message',
|
||||
'style': 'background: #4caf50; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;'
|
||||
}, [
|
||||
E('strong', {}, '✓ All systems operational'),
|
||||
E('span', { 'style': 'margin-left: 1rem;' }, 'netifyd + CrowdSec integration active')
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', {
|
||||
'class': 'alert-message',
|
||||
'style': 'background: #ff9800; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;'
|
||||
}, [
|
||||
E('strong', {}, '⚠ Service Issues: '),
|
||||
E('span', {}, services.join(', '))
|
||||
]);
|
||||
},
|
||||
|
||||
renderStatsGrid: function(stats, blockedCount) {
|
||||
return E('div', {
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;'
|
||||
}, [
|
||||
this.renderStatCard(_('Active Threats'), stats.total, '#2196f3', ''),
|
||||
this.renderStatCard(_('Critical'), stats.critical, '#d32f2f', 'Immediate attention required'),
|
||||
this.renderStatCard(_('High Risk'), stats.high, '#ff5722', 'Review recommended'),
|
||||
this.renderStatCard(_('Avg Risk Score'), stats.avg_score + '/100', '#ff9800', 'Overall threat level'),
|
||||
this.renderStatCard(_('Blocked IPs'), blockedCount, '#9c27b0', 'Via CrowdSec')
|
||||
]);
|
||||
},
|
||||
|
||||
renderStatCard: function(label, value, color, description) {
|
||||
var children = [
|
||||
E('div', { 'style': 'font-size: 0.85rem; color: #666; margin-bottom: 0.5rem;' }, label),
|
||||
E('div', { 'style': 'font-size: 2rem; font-weight: bold; color: ' + color + ';' }, value)
|
||||
];
|
||||
|
||||
if (description) {
|
||||
children.push(E('div', { 'style': 'font-size: 0.75rem; color: #999; margin-top: 0.25rem;' }, description));
|
||||
}
|
||||
|
||||
return E('div', {
|
||||
'style': 'background: #f5f5f5; padding: 1rem; border-left: 4px solid ' + color + '; border-radius: 4px;'
|
||||
}, children);
|
||||
},
|
||||
|
||||
renderThreatDistribution: function(stats) {
|
||||
var categories = [
|
||||
{ label: 'Malware', value: stats.malware || 0, color: '#d32f2f', icon: '🦠' },
|
||||
{ label: 'Web Attack', value: stats.web_attack || 0, color: '#ff5722', icon: '⚔️' },
|
||||
{ label: 'Anomaly', value: stats.anomaly || 0, color: '#ff9800', icon: '⚠️' },
|
||||
{ label: 'Protocol', value: stats.protocol || 0, color: '#9c27b0', icon: '🚫' },
|
||||
{ label: 'TLS Issue', value: stats.tls_issue || 0, color: '#3f51b5', icon: '🔒' }
|
||||
];
|
||||
|
||||
var total = categories.reduce(function(sum, cat) { return sum + cat.value; }, 0);
|
||||
|
||||
return E('div', {}, [
|
||||
E('h4', {}, _('Threat Distribution')),
|
||||
E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px;' }, [
|
||||
total === 0 ?
|
||||
E('div', { 'style': 'text-align: center; color: #999; padding: 2rem;' }, _('No threats detected')) :
|
||||
E('div', {}, categories.filter(function(cat) {
|
||||
return cat.value > 0;
|
||||
}).map(L.bind(function(cat) {
|
||||
var percentage = total > 0 ? Math.round((cat.value / total) * 100) : 0;
|
||||
return E('div', { 'style': 'margin-bottom: 1rem;' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25rem;' }, [
|
||||
E('span', {}, cat.icon + ' ' + cat.label),
|
||||
E('span', { 'style': 'font-weight: bold;' }, cat.value + ' (' + percentage + '%)')
|
||||
]),
|
||||
E('div', {
|
||||
'style': 'background: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden;'
|
||||
}, [
|
||||
E('div', {
|
||||
'style': 'background: ' + cat.color + '; height: 100%; width: ' + percentage + '%;'
|
||||
})
|
||||
])
|
||||
]);
|
||||
}, this)))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderRiskGauge: function(avgScore) {
|
||||
var level, color, description;
|
||||
if (avgScore >= 80) {
|
||||
level = 'CRITICAL';
|
||||
color = '#d32f2f';
|
||||
description = 'Immediate action required';
|
||||
} else if (avgScore >= 60) {
|
||||
level = 'HIGH';
|
||||
color = '#ff5722';
|
||||
description = 'Review threats promptly';
|
||||
} else if (avgScore >= 40) {
|
||||
level = 'MEDIUM';
|
||||
color = '#ff9800';
|
||||
description = 'Monitor situation';
|
||||
} else {
|
||||
level = 'LOW';
|
||||
color = '#4caf50';
|
||||
description = 'Normal security posture';
|
||||
}
|
||||
|
||||
return E('div', {}, [
|
||||
E('h4', {}, _('Risk Level')),
|
||||
E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px; text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 3rem; font-weight: bold; color: ' + color + '; margin: 1rem 0;' }, avgScore),
|
||||
E('div', { 'style': 'font-size: 1.2rem; font-weight: bold; color: ' + color + '; margin-bottom: 0.5rem;' }, level),
|
||||
E('div', { 'style': 'color: #666; font-size: 0.9rem;' }, description),
|
||||
E('div', {
|
||||
'style': 'margin-top: 1rem; height: 10px; background: linear-gradient(to right, #4caf50, #ff9800, #ff5722, #d32f2f); border-radius: 5px; position: relative;'
|
||||
}, [
|
||||
E('div', {
|
||||
'style': 'position: absolute; top: -5px; left: ' + avgScore + '%; width: 2px; height: 20px; background: #000;'
|
||||
})
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderThreatsTable: function(threats) {
|
||||
if (threats.length === 0) {
|
||||
return E('div', {
|
||||
'style': 'text-align: center; padding: 2rem; color: #999; background: #f5f5f5; border-radius: 4px;'
|
||||
}, _('No threats detected. Your network is secure.'));
|
||||
}
|
||||
|
||||
var rows = threats.map(L.bind(function(threat) {
|
||||
return E('tr', {}, [
|
||||
E('td', {}, [
|
||||
E('div', {}, threat.ip),
|
||||
E('div', { 'style': 'font-size: 0.85em; color: #666;' }, API.formatRelativeTime(threat.timestamp))
|
||||
]),
|
||||
E('td', {}, threat.mac),
|
||||
E('td', {}, [
|
||||
E('div', {}, API.getThreatIcon(threat.category) + ' ' + API.getCategoryLabel(threat.category)),
|
||||
E('div', { 'style': 'font-size: 0.85em; color: #666;' }, threat.netifyd.application || 'unknown')
|
||||
]),
|
||||
E('td', { 'innerHTML': API.getSeverityBadge(threat.severity) }),
|
||||
E('td', { 'style': 'font-weight: bold;' }, threat.risk_score),
|
||||
E('td', {}, [
|
||||
E('div', { 'style': 'font-size: 0.85em; max-width: 200px; overflow: hidden; text-overflow: ellipsis;' },
|
||||
API.formatRiskFlags(threat.netifyd.risks))
|
||||
]),
|
||||
E('td', {}, threat.crowdsec.has_decision ?
|
||||
E('span', { 'style': 'color: #d32f2f; font-weight: bold;' }, '✓ Blocked') :
|
||||
E('span', { 'style': 'color: #999;' }, '-')),
|
||||
E('td', {}, [
|
||||
threat.crowdsec.has_decision ?
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'disabled': 'disabled'
|
||||
}, _('Blocked')) :
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': L.bind(function(ev) {
|
||||
this.handleBlock(threat.ip);
|
||||
}, this)
|
||||
}, _('Block'))
|
||||
])
|
||||
]);
|
||||
}, this));
|
||||
|
||||
return E('div', { 'class': 'table-wrapper' }, [
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('IP Address')),
|
||||
E('th', { 'class': 'th' }, _('MAC')),
|
||||
E('th', { 'class': 'th' }, _('Category / App')),
|
||||
E('th', { 'class': 'th' }, _('Severity')),
|
||||
E('th', { 'class': 'th' }, _('Risk Score')),
|
||||
E('th', { 'class': 'th' }, _('netifyd Risks')),
|
||||
E('th', { 'class': 'th' }, _('CrowdSec')),
|
||||
E('th', { 'class': 'th' }, _('Actions'))
|
||||
])
|
||||
].concat(rows))
|
||||
]);
|
||||
},
|
||||
|
||||
handleBlock: function(ip) {
|
||||
ui.showModal(_('Block IP Address'), [
|
||||
E('p', {}, _('Are you sure you want to block %s?').format(ip)),
|
||||
E('p', {}, _('This will add a CrowdSec decision and block all traffic from this IP.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.showModal(_('Blocking IP...'), E('p', { 'class': 'spinning' }, _('Please wait...')));
|
||||
|
||||
API.blockThreat(ip, '4h', 'Manual block from Security Threats Dashboard').then(L.bind(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', _('IP %s blocked successfully').format(ip)), 'success');
|
||||
this.handleRefresh();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Failed to block IP: %s').format(result.error || 'Unknown error')), 'error');
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error: %s').format(err.message)), 'error');
|
||||
});
|
||||
}, this)
|
||||
}, _('Block for 4 hours'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
return API.getDashboardData().then(L.bind(function(data) {
|
||||
// Update view with new data
|
||||
var container = document.querySelector('.cbi-map');
|
||||
if (container) {
|
||||
var newView = this.render(data);
|
||||
dom.content(container, newView);
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
console.error('Failed to refresh dashboard:', err);
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,59 @@
|
||||
# SecuBox Security Threats Dashboard Configuration
|
||||
# Auto-blocking rules and whitelist configuration
|
||||
|
||||
config global 'global'
|
||||
option enabled '1'
|
||||
option history_retention_days '7'
|
||||
option refresh_interval '10'
|
||||
option auto_block_enabled '1'
|
||||
option log_threats '1'
|
||||
|
||||
# High-priority: Block malware indicators
|
||||
config block_rule 'malware_high'
|
||||
option name 'Block Malware Indicators'
|
||||
option enabled '1'
|
||||
option threat_types 'malware'
|
||||
option risk_flags 'MALICIOUS_JA3,SUSPICIOUS_DGA_DOMAIN,SUSPICIOUS_ENTROPY,POSSIBLE_EXPLOIT'
|
||||
option action 'ban'
|
||||
option duration '24h'
|
||||
option threshold '60'
|
||||
option description 'Automatically block hosts with malware signatures (JA3, DGA domains, suspicious entropy)'
|
||||
|
||||
# Medium-priority: Block web attacks
|
||||
config block_rule 'web_attacks'
|
||||
option name 'Block Web Attacks'
|
||||
option enabled '1'
|
||||
option threat_types 'web_attack'
|
||||
option risk_flags 'URL_POSSIBLE_SQL_INJECTION,URL_POSSIBLE_XSS,URL_POSSIBLE_RCE_INJECTION'
|
||||
option action 'ban'
|
||||
option duration '12h'
|
||||
option threshold '40'
|
||||
option description 'Block SQL injection, XSS, and RCE attempts'
|
||||
|
||||
# Low-priority: Block protocol threats (disabled by default)
|
||||
config block_rule 'protocol_threats'
|
||||
option name 'Block Unauthorized Protocols'
|
||||
option enabled '0'
|
||||
option threat_types 'protocol'
|
||||
option risk_flags ''
|
||||
option action 'ban'
|
||||
option duration '4h'
|
||||
option threshold '20'
|
||||
option description 'Block unauthorized protocols like BitTorrent, Mining, Tor (disabled by default)'
|
||||
|
||||
# Network anomalies (disabled by default - may generate false positives)
|
||||
config block_rule 'network_anomalies'
|
||||
option name 'Block Network Anomalies'
|
||||
option enabled '0'
|
||||
option threat_types 'anomaly'
|
||||
option risk_flags 'RISKY_ASN,RISKY_DOMAIN,DNS_SUSPICIOUS_TRAFFIC'
|
||||
option action 'ban'
|
||||
option duration '6h'
|
||||
option threshold '50'
|
||||
option description 'Block connections from risky ASNs/domains and suspicious DNS traffic'
|
||||
|
||||
# Example whitelist entry (commented out)
|
||||
# config whitelist 'admin_workstation'
|
||||
# option ip '192.168.1.100'
|
||||
# option reason 'Admin workstation - never block'
|
||||
# option added_at '2026-01-07T15:00:00Z'
|
||||
@ -0,0 +1,536 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SecuBox Security Threats Dashboard RPCD backend
|
||||
# Copyright (C) 2026 CyberMind.fr - Gandalf
|
||||
#
|
||||
# Integrates netifyd DPI security risks with CrowdSec threat intelligence
|
||||
# for comprehensive network threat monitoring and automated blocking
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
HISTORY_FILE="/tmp/secubox-threats-history.json"
|
||||
CSCLI="/usr/bin/cscli"
|
||||
SECCUBOX_LOG="/usr/sbin/secubox-log"
|
||||
|
||||
secubox_log() {
|
||||
[ -x "$SECCUBOX_LOG" ] || return
|
||||
"$SECCUBOX_LOG" --tag "security-threats" --message "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Initialize storage
|
||||
init_storage() {
|
||||
[ ! -f "$HISTORY_FILE" ] && echo '[]' > "$HISTORY_FILE"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# DATA COLLECTION
|
||||
# ==============================================================================
|
||||
|
||||
# Get netifyd flows (socket first, fallback to file)
|
||||
get_netifyd_flows() {
|
||||
if [ -S /var/run/netifyd/netifyd.sock ]; then
|
||||
echo "status" | nc -U /var/run/netifyd/netifyd.sock 2>/dev/null
|
||||
elif [ -f /var/run/netifyd/status.json ]; then
|
||||
cat /var/run/netifyd/status.json
|
||||
else
|
||||
echo '{}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Filter flows with security risks
|
||||
filter_risky_flows() {
|
||||
local flows="$1"
|
||||
# Extract flows with risks[] array (length > 0)
|
||||
echo "$flows" | jq -c '.flows[]? | select(.risks != null and (.risks | length) > 0)' 2>/dev/null
|
||||
}
|
||||
|
||||
# Get CrowdSec decisions (active bans)
|
||||
get_crowdsec_decisions() {
|
||||
[ ! -x "$CSCLI" ] && echo '[]' && return
|
||||
$CSCLI decisions list -o json 2>/dev/null || echo '[]'
|
||||
}
|
||||
|
||||
# Get CrowdSec alerts (recent detections)
|
||||
get_crowdsec_alerts() {
|
||||
[ ! -x "$CSCLI" ] && echo '[]' && return
|
||||
$CSCLI alerts list -o json --limit 100 2>/dev/null || echo '[]'
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# CLASSIFICATION
|
||||
# ==============================================================================
|
||||
|
||||
# Classify netifyd risk by category
|
||||
classify_netifyd_risk() {
|
||||
local risk_name="$1"
|
||||
|
||||
# Map ND_RISK_* to categories
|
||||
case "$risk_name" in
|
||||
*MALICIOUS_JA3*|*SUSPICIOUS_DGA*|*SUSPICIOUS_ENTROPY*|*POSSIBLE_EXPLOIT*|*PERIODIC_FLOW*)
|
||||
echo "malware";;
|
||||
*SQL_INJECTION*|*XSS*|*RCE_INJECTION*|*HTTP_SUSPICIOUS*)
|
||||
echo "web_attack";;
|
||||
*DNS_FRAGMENTED*|*DNS_LARGE_PACKET*|*DNS_SUSPICIOUS*|*RISKY_ASN*|*RISKY_DOMAIN*|*UNIDIRECTIONAL*|*MALFORMED_PACKET*)
|
||||
echo "anomaly";;
|
||||
*BitTorrent*|*Mining*|*Tor*|*PROXY*|*SOCKS*)
|
||||
echo "protocol";;
|
||||
*TLS_*|*CERTIFICATE_*)
|
||||
echo "tls_issue";;
|
||||
*)
|
||||
echo "other";;
|
||||
esac
|
||||
}
|
||||
|
||||
# Calculate risk score (0-100)
|
||||
calculate_risk_score() {
|
||||
local risk_count="$1"
|
||||
local has_crowdsec="$2"
|
||||
local risk_types="$3" # comma-separated
|
||||
|
||||
local score=$((risk_count * 10)) # Base: 10 per risk
|
||||
[ "$score" -gt 50 ] && score=50 # Cap base at 50
|
||||
|
||||
# Severity weights
|
||||
echo "$risk_types" | grep -q "MALICIOUS_JA3\|SUSPICIOUS_DGA\|POSSIBLE_EXPLOIT" && score=$((score + 20))
|
||||
echo "$risk_types" | grep -q "SQL_INJECTION\|XSS\|RCE" && score=$((score + 15))
|
||||
echo "$risk_types" | grep -q "RISKY_ASN\|RISKY_DOMAIN" && score=$((score + 10))
|
||||
echo "$risk_types" | grep -q "BitTorrent\|Mining\|Tor" && score=$((score + 5))
|
||||
|
||||
# CrowdSec correlation bonus
|
||||
[ "$has_crowdsec" = "1" ] && score=$((score + 30))
|
||||
|
||||
# Cap at 100
|
||||
[ "$score" -gt 100 ] && score=100
|
||||
echo "$score"
|
||||
}
|
||||
|
||||
# Determine severity level
|
||||
get_threat_severity() {
|
||||
local score="$1"
|
||||
if [ "$score" -ge 80 ]; then
|
||||
echo "critical"
|
||||
elif [ "$score" -ge 60 ]; then
|
||||
echo "high"
|
||||
elif [ "$score" -ge 40 ]; then
|
||||
echo "medium"
|
||||
else
|
||||
echo "low"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# CORRELATION ENGINE
|
||||
# ==============================================================================
|
||||
|
||||
# Correlate netifyd risks with CrowdSec data
|
||||
correlate_threats() {
|
||||
local netifyd_flows="$1"
|
||||
local crowdsec_decisions="$2"
|
||||
local crowdsec_alerts="$3"
|
||||
|
||||
# Create lookup tables with jq
|
||||
local decisions_by_ip=$(echo "$crowdsec_decisions" | jq -c 'INDEX(.value)' 2>/dev/null || echo '{}')
|
||||
local alerts_by_ip=$(echo "$crowdsec_alerts" | jq -c 'group_by(.source.ip) | map({(.[0].source.ip): .}) | add // {}' 2>/dev/null || echo '{}')
|
||||
|
||||
# Process each risky flow
|
||||
echo "$netifyd_flows" | while read -r flow; do
|
||||
[ -z "$flow" ] && continue
|
||||
|
||||
local ip=$(echo "$flow" | jq -r '.src_ip // "unknown"')
|
||||
[ "$ip" = "unknown" ] && continue
|
||||
|
||||
local mac=$(echo "$flow" | jq -r '.src_mac // "N/A"')
|
||||
local risks=$(echo "$flow" | jq -r '.risks | map(tostring) | join(",")' 2>/dev/null || echo "")
|
||||
local risk_count=$(echo "$flow" | jq '.risks | length' 2>/dev/null || echo 0)
|
||||
|
||||
# Lookup CrowdSec data
|
||||
local decision=$(echo "$decisions_by_ip" | jq -c ".[\"$ip\"] // null")
|
||||
local has_decision=$([[ "$decision" != "null" ]] && echo 1 || echo 0)
|
||||
local alert=$(echo "$alerts_by_ip" | jq -c ".[\"$ip\"] // null")
|
||||
|
||||
# Calculate metrics
|
||||
local risk_score=$(calculate_risk_score "$risk_count" "$has_decision" "$risks")
|
||||
local severity=$(get_threat_severity "$risk_score")
|
||||
local first_risk=$(echo "$risks" | cut -d, -f1)
|
||||
local category=$(classify_netifyd_risk "$first_risk")
|
||||
|
||||
# Build unified threat JSON
|
||||
jq -n \
|
||||
--arg ip "$ip" \
|
||||
--arg mac "$mac" \
|
||||
--arg timestamp "$(date -Iseconds)" \
|
||||
--argjson score "$risk_score" \
|
||||
--arg severity "$severity" \
|
||||
--arg category "$category" \
|
||||
--argjson netifyd "$(echo "$flow" | jq '{
|
||||
application: .detected_application // "unknown",
|
||||
protocol: .detected_protocol // "unknown",
|
||||
risks: [.risks[] | tostring],
|
||||
risk_count: (.risks | length),
|
||||
bytes: .total_bytes // 0,
|
||||
packets: .total_packets // 0
|
||||
}')" \
|
||||
--argjson crowdsec "$(jq -n \
|
||||
--argjson decision "$decision" \
|
||||
--argjson alert "$alert" \
|
||||
'{
|
||||
has_decision: ($decision != null),
|
||||
decision: $decision,
|
||||
has_alert: ($alert != null),
|
||||
alert_count: (if $alert != null then ($alert | length) else 0 end),
|
||||
scenarios: (if $alert != null then ($alert | map(.scenario) | join(",")) else "" end)
|
||||
}')" \
|
||||
'{
|
||||
ip: $ip,
|
||||
mac: $mac,
|
||||
timestamp: $timestamp,
|
||||
risk_score: $score,
|
||||
severity: $severity,
|
||||
category: $category,
|
||||
netifyd: $netifyd,
|
||||
crowdsec: $crowdsec
|
||||
}' 2>/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# AUTO-BLOCKING
|
||||
# ==============================================================================
|
||||
|
||||
# Execute block via CrowdSec
|
||||
execute_block() {
|
||||
local ip="$1"
|
||||
local duration="$2"
|
||||
local reason="$3"
|
||||
|
||||
[ ! -x "$CSCLI" ] && return 1
|
||||
|
||||
# Call cscli to add decision
|
||||
if $CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" >/dev/null 2>&1; then
|
||||
secubox_log "Auto-blocked $ip for $duration ($reason)"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check single rule match
|
||||
check_rule_match() {
|
||||
local section="$1"
|
||||
local threat_category="$2"
|
||||
local threat_risks="$3"
|
||||
local threat_score="$4"
|
||||
local threat_ip="$5"
|
||||
|
||||
local enabled=$(uci -q get "secubox_security_threats.${section}.enabled")
|
||||
[ "$enabled" != "1" ] && return 1
|
||||
|
||||
local rule_types=$(uci -q get "secubox_security_threats.${section}.threat_types")
|
||||
echo "$rule_types" | grep -qw "$threat_category" || return 1
|
||||
|
||||
local threshold=$(uci -q get "secubox_security_threats.${section}.threshold" 2>/dev/null || echo 1)
|
||||
[ "$threat_score" -lt "$threshold" ] && return 1
|
||||
|
||||
# Rule matched - execute block
|
||||
local duration=$(uci -q get "secubox_security_threats.${section}.duration" || echo "4h")
|
||||
local name=$(uci -q get "secubox_security_threats.${section}.name" || echo "Security threat")
|
||||
|
||||
execute_block "$threat_ip" "$duration" "Auto-blocked: $name"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Check if threat should be auto-blocked
|
||||
check_block_rules() {
|
||||
local threat="$1"
|
||||
|
||||
local ip=$(echo "$threat" | jq -r '.ip')
|
||||
local category=$(echo "$threat" | jq -r '.category')
|
||||
local score=$(echo "$threat" | jq -r '.risk_score')
|
||||
local risks=$(echo "$threat" | jq -r '.netifyd.risks | join(",")')
|
||||
|
||||
# Check whitelist first
|
||||
local whitelist_section="whitelist_${ip//./_}"
|
||||
uci -q get "secubox_security_threats.${whitelist_section}" >/dev/null 2>&1 && return 1
|
||||
|
||||
# Check if auto-blocking is enabled globally
|
||||
local auto_block_enabled=$(uci -q get secubox_security_threats.global.auto_block_enabled 2>/dev/null || echo 1)
|
||||
[ "$auto_block_enabled" != "1" ] && return 1
|
||||
|
||||
# Iterate block rules from UCI
|
||||
config_load secubox_security_threats
|
||||
config_foreach check_rule_match block_rule "$category" "$risks" "$score" "$ip"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# STATISTICS
|
||||
# ==============================================================================
|
||||
|
||||
# Get stats by type (category)
|
||||
get_stats_by_type() {
|
||||
local threats="$1"
|
||||
|
||||
echo "$threats" | jq -s '{
|
||||
malware: [.[] | select(.category == "malware")] | length,
|
||||
web_attack: [.[] | select(.category == "web_attack")] | length,
|
||||
anomaly: [.[] | select(.category == "anomaly")] | length,
|
||||
protocol: [.[] | select(.category == "protocol")] | length,
|
||||
tls_issue: [.[] | select(.category == "tls_issue")] | length,
|
||||
other: [.[] | select(.category == "other")] | length
|
||||
}' 2>/dev/null
|
||||
}
|
||||
|
||||
# Get stats by host (IP)
|
||||
get_stats_by_host() {
|
||||
local threats="$1"
|
||||
|
||||
echo "$threats" | jq -s 'group_by(.ip) | map({
|
||||
ip: .[0].ip,
|
||||
mac: .[0].mac,
|
||||
threat_count: length,
|
||||
avg_risk_score: (map(.risk_score) | add / length | floor),
|
||||
highest_severity: (map(.severity) | sort | reverse | .[0]),
|
||||
first_seen: (map(.timestamp) | sort | .[0]),
|
||||
last_seen: (map(.timestamp) | sort | reverse | .[0]),
|
||||
categories: (map(.category) | unique | join(","))
|
||||
})' 2>/dev/null
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# UBUS INTERFACE
|
||||
# ==============================================================================
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
# List available methods
|
||||
json_init
|
||||
json_add_object "status"
|
||||
json_close_object
|
||||
json_add_object "get_active_threats"
|
||||
json_close_object
|
||||
json_add_object "get_threat_history"
|
||||
json_add_string "hours" "int"
|
||||
json_close_object
|
||||
json_add_object "get_stats_by_type"
|
||||
json_close_object
|
||||
json_add_object "get_stats_by_host"
|
||||
json_close_object
|
||||
json_add_object "get_blocked_ips"
|
||||
json_close_object
|
||||
json_add_object "block_threat"
|
||||
json_add_string "ip" "string"
|
||||
json_add_string "duration" "string"
|
||||
json_add_string "reason" "string"
|
||||
json_close_object
|
||||
json_add_object "whitelist_host"
|
||||
json_add_string "ip" "string"
|
||||
json_add_string "reason" "string"
|
||||
json_close_object
|
||||
json_add_object "remove_whitelist"
|
||||
json_add_string "ip" "string"
|
||||
json_close_object
|
||||
json_dump
|
||||
;;
|
||||
|
||||
call)
|
||||
case "$2" in
|
||||
status)
|
||||
json_init
|
||||
json_add_boolean "enabled" 1
|
||||
json_add_string "module" "secubox-security-threats"
|
||||
json_add_string "version" "1.0.0"
|
||||
json_add_boolean "netifyd_running" $(pgrep -x netifyd >/dev/null && echo 1 || echo 0)
|
||||
json_add_boolean "crowdsec_running" $(pgrep crowdsec >/dev/null && echo 1 || echo 0)
|
||||
json_add_boolean "cscli_available" $([ -x "$CSCLI" ] && echo 1 || echo 0)
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_active_threats)
|
||||
# Main correlation workflow
|
||||
local netifyd_data=$(get_netifyd_flows)
|
||||
local risky_flows=$(filter_risky_flows "$netifyd_data")
|
||||
|
||||
# Only fetch CrowdSec data if available
|
||||
local decisions='[]'
|
||||
local alerts='[]'
|
||||
if [ -x "$CSCLI" ]; then
|
||||
decisions=$(get_crowdsec_decisions)
|
||||
alerts=$(get_crowdsec_alerts)
|
||||
fi
|
||||
|
||||
# Correlate threats
|
||||
local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts")
|
||||
|
||||
# Check auto-block rules for each threat
|
||||
if [ -n "$threats" ]; then
|
||||
echo "$threats" | while read -r threat; do
|
||||
[ -z "$threat" ] && continue
|
||||
check_block_rules "$threat" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Output as JSON array
|
||||
json_init
|
||||
json_add_array "threats"
|
||||
if [ -n "$threats" ]; then
|
||||
echo "$threats" | jq -s 'sort_by(.risk_score) | reverse' | jq -c '.[]' | while read -r threat; do
|
||||
echo "$threat"
|
||||
done
|
||||
fi
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_threat_history)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var hours hours
|
||||
hours=${hours:-24}
|
||||
|
||||
init_storage
|
||||
|
||||
# Filter history by time
|
||||
local cutoff_time=$(date -d "$hours hours ago" -Iseconds 2>/dev/null || date -Iseconds)
|
||||
|
||||
json_init
|
||||
json_add_array "threats"
|
||||
if [ -f "$HISTORY_FILE" ]; then
|
||||
jq -c --arg cutoff "$cutoff_time" '.[] | select(.timestamp >= $cutoff)' "$HISTORY_FILE" 2>/dev/null | while read -r threat; do
|
||||
echo "$threat"
|
||||
done
|
||||
fi
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_stats_by_type)
|
||||
local netifyd_data=$(get_netifyd_flows)
|
||||
local risky_flows=$(filter_risky_flows "$netifyd_data")
|
||||
local decisions=$(get_crowdsec_decisions)
|
||||
local alerts=$(get_crowdsec_alerts)
|
||||
local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts")
|
||||
|
||||
local stats=$(get_stats_by_type "$threats")
|
||||
|
||||
echo "$stats"
|
||||
;;
|
||||
|
||||
get_stats_by_host)
|
||||
local netifyd_data=$(get_netifyd_flows)
|
||||
local risky_flows=$(filter_risky_flows "$netifyd_data")
|
||||
local decisions=$(get_crowdsec_decisions)
|
||||
local alerts=$(get_crowdsec_alerts)
|
||||
local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts")
|
||||
|
||||
json_init
|
||||
json_add_array "hosts"
|
||||
if [ -n "$threats" ]; then
|
||||
get_stats_by_host "$threats" | jq -c '.[]' | while read -r host; do
|
||||
echo "$host"
|
||||
done
|
||||
fi
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_blocked_ips)
|
||||
if [ -x "$CSCLI" ]; then
|
||||
local decisions=$(get_crowdsec_decisions)
|
||||
echo "{\"blocked\":$decisions}"
|
||||
else
|
||||
echo '{"blocked":[]}'
|
||||
fi
|
||||
;;
|
||||
|
||||
block_threat)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var ip ip
|
||||
json_get_var duration duration
|
||||
json_get_var reason reason
|
||||
|
||||
if [ -z "$ip" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "IP address required"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
duration=${duration:-4h}
|
||||
reason=${reason:-"Manual block from Security Threats Dashboard"}
|
||||
|
||||
if execute_block "$ip" "$duration" "$reason"; then
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "IP $ip blocked for $duration"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Failed to block IP (check if CrowdSec is running)"
|
||||
json_dump
|
||||
fi
|
||||
;;
|
||||
|
||||
whitelist_host)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var ip ip
|
||||
json_get_var reason reason
|
||||
|
||||
if [ -z "$ip" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "IP address required"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
reason=${reason:-"Whitelisted from Security Threats Dashboard"}
|
||||
local section="whitelist_${ip//./_}"
|
||||
|
||||
uci set "secubox_security_threats.${section}=whitelist"
|
||||
uci set "secubox_security_threats.${section}.ip=$ip"
|
||||
uci set "secubox_security_threats.${section}.reason=$reason"
|
||||
uci set "secubox_security_threats.${section}.added_at=$(date -Iseconds)"
|
||||
uci commit secubox_security_threats
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "IP $ip added to whitelist"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
remove_whitelist)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var ip ip
|
||||
|
||||
if [ -z "$ip" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "IP address required"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local section="whitelist_${ip//./_}"
|
||||
uci delete "secubox_security_threats.${section}" 2>/dev/null
|
||||
uci commit secubox_security_threats
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "IP $ip removed from whitelist"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
json_init
|
||||
json_add_boolean "error" 1
|
||||
json_add_string "message" "Unknown method: $2"
|
||||
json_dump
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,20 @@
|
||||
{
|
||||
"admin/secubox/security/threats": {
|
||||
"title": "Threat Monitor",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-secubox-security-threats"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/threats/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "secubox-security-threats/dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
{
|
||||
"luci-app-secubox-security-threats": {
|
||||
"description": "Grant access to SecuBox Security Threats Dashboard",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.secubox-security-threats": [
|
||||
"status",
|
||||
"get_active_threats",
|
||||
"get_threat_history",
|
||||
"get_stats_by_type",
|
||||
"get_stats_by_host",
|
||||
"get_blocked_ips"
|
||||
],
|
||||
"luci.crowdsec-dashboard": [
|
||||
"decisions",
|
||||
"alerts",
|
||||
"status"
|
||||
],
|
||||
"luci.netifyd-dashboard": [
|
||||
"status",
|
||||
"get_flows",
|
||||
"get_devices"
|
||||
]
|
||||
},
|
||||
"uci": ["secubox_security_threats", "netifyd", "crowdsec"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.secubox-security-threats": [
|
||||
"block_threat",
|
||||
"whitelist_host",
|
||||
"remove_whitelist"
|
||||
],
|
||||
"luci.crowdsec-dashboard": [
|
||||
"ban",
|
||||
"unban"
|
||||
]
|
||||
},
|
||||
"uci": ["secubox_security_threats"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,23 +82,26 @@ const DevStatusWidget = {
|
||||
}
|
||||
},
|
||||
|
||||
// Per-module status overview
|
||||
moduleStatus: [
|
||||
{ name: 'SecuBox Central Hub', version: '0.7.0-r6', note: 'Dashboard central + Appstore (5 apps)' },
|
||||
{ name: 'System Hub', version: '0.5.1-r2', note: 'Centre de contrôle' },
|
||||
{ name: 'Traffic Shaper', version: '0.4.0-r1', note: 'CAKE / fq_codel / HTB' },
|
||||
{ name: 'CrowdSec Dashboard', version: '0.5.0-r1', note: 'Détection d\'intrusions' },
|
||||
{ name: 'Netdata Dashboard', version: '0.5.0-r1', note: 'Monitoring temps réel' },
|
||||
{ name: 'Netifyd Dashboard', version: '0.4.0-r1', note: 'Intelligence applicative' },
|
||||
{ name: 'Network Modes', version: '0.5.0-r1', note: '5 topologies réseau' },
|
||||
{ name: 'WireGuard Dashboard', version: '0.4.0-r1', note: 'VPN + QR codes' },
|
||||
{ name: 'Auth Guardian', version: '0.4.0-r1', note: 'OAuth / vouchers' },
|
||||
{ name: 'Client Guardian', version: '0.4.0-r1', note: 'Portail captif + contrôle d\'accès' },
|
||||
{ name: 'Bandwidth Manager', version: '0.4.0-r1', note: 'QoS + quotas' },
|
||||
{ name: 'Media Flow', version: '0.4.0-r1', note: 'DPI streaming' },
|
||||
{ name: 'CDN Cache', version: '0.5.0-r1', note: 'Cache contenu local' },
|
||||
{ name: 'VHost Manager', version: '0.4.1-r3', note: 'Reverse proxy / SSL' },
|
||||
{ name: 'KSM Manager', version: '0.4.0-r1', note: 'Gestion clés / HSM' }
|
||||
// Per-module status overview (will be populated dynamically)
|
||||
moduleStatus: [],
|
||||
|
||||
// Static module definitions (fallback if API fails)
|
||||
staticModuleStatus: [
|
||||
{ name: 'SecuBox Central Hub', version: '0.7.0-r6', note: 'Dashboard central + Appstore (5 apps)', id: 'secubox-admin' },
|
||||
{ name: 'System Hub', version: '0.5.1-r2', note: 'Centre de contrôle', id: 'system-hub' },
|
||||
{ name: 'Traffic Shaper', version: '0.4.0-r1', note: 'CAKE / fq_codel / HTB', id: 'traffic-shaper' },
|
||||
{ name: 'CrowdSec Dashboard', version: '0.5.0-r1', note: 'Détection d\'intrusions', id: 'crowdsec' },
|
||||
{ name: 'Netdata Dashboard', version: '0.5.0-r1', note: 'Monitoring temps réel', id: 'netdata' },
|
||||
{ name: 'Netifyd Dashboard', version: '0.4.0-r1', note: 'Intelligence applicative', id: 'netifyd' },
|
||||
{ name: 'Network Modes', version: '0.5.0-r1', note: '5 topologies réseau', id: 'network-modes' },
|
||||
{ name: 'WireGuard Dashboard', version: '0.4.0-r1', note: 'VPN + QR codes', id: 'wireguard' },
|
||||
{ name: 'Auth Guardian', version: '0.4.0-r1', note: 'OAuth / vouchers', id: 'auth-guardian' },
|
||||
{ name: 'Client Guardian', version: '0.4.0-r1', note: 'Portail captif + contrôle d\'accès', id: 'client-guardian' },
|
||||
{ name: 'Bandwidth Manager', version: '0.4.0-r1', note: 'QoS + quotas', id: 'bandwidth-manager' },
|
||||
{ name: 'Media Flow', version: '0.4.0-r1', note: 'DPI streaming', id: 'media-flow' },
|
||||
{ name: 'CDN Cache', version: '0.5.0-r1', note: 'Cache contenu local', id: 'cdn-cache' },
|
||||
{ name: 'VHost Manager', version: '0.4.1-r3', note: 'Reverse proxy / SSL', id: 'vhost-manager' },
|
||||
{ name: 'KSM Manager', version: '0.4.0-r1', note: 'Gestion clés / HSM', id: 'ksm-manager' }
|
||||
],
|
||||
|
||||
// Overall project statistics
|
||||
@ -159,6 +162,92 @@ const DevStatusWidget = {
|
||||
}
|
||||
],
|
||||
|
||||
/**
|
||||
* Fetch and synchronize module versions from system
|
||||
*/
|
||||
async syncModuleVersions() {
|
||||
try {
|
||||
// Try to fetch from secubox via ubus
|
||||
const appsData = await L.resolveDefault(
|
||||
L.Request.post(L.env.ubus_rpc_session ? '/ubus/' : '/ubus', {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 1,
|
||||
'method': 'call',
|
||||
'params': [
|
||||
L.env.ubus_rpc_session,
|
||||
'luci.secubox',
|
||||
'get_apps',
|
||||
{}
|
||||
]
|
||||
}),
|
||||
null
|
||||
);
|
||||
|
||||
if (!appsData || !appsData.json || !appsData.json().result || !appsData.json().result[1]) {
|
||||
console.warn('[DevStatus] API not available, using static data');
|
||||
this.moduleStatus = this.staticModuleStatus;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = appsData.json().result[1];
|
||||
const apps = result.apps || [];
|
||||
|
||||
// Also get modules status
|
||||
const modulesData = await L.resolveDefault(
|
||||
L.Request.post(L.env.ubus_rpc_session ? '/ubus/' : '/ubus', {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 2,
|
||||
'method': 'call',
|
||||
'params': [
|
||||
L.env.ubus_rpc_session,
|
||||
'luci.secubox',
|
||||
'get_modules',
|
||||
{}
|
||||
]
|
||||
}),
|
||||
null
|
||||
);
|
||||
|
||||
const modules = modulesData && modulesData.json() && modulesData.json().result && modulesData.json().result[1] ?
|
||||
modulesData.json().result[1].modules || {} : {};
|
||||
|
||||
// Map apps to module status
|
||||
this.moduleStatus = this.staticModuleStatus.map(staticModule => {
|
||||
const app = apps.find(a => a.id === staticModule.id || a.name === staticModule.name);
|
||||
|
||||
let installed = false;
|
||||
let running = false;
|
||||
|
||||
if (app && app.packages && app.packages.required && app.packages.required[0]) {
|
||||
const pkgName = app.packages.required[0];
|
||||
const moduleInfo = modules[pkgName];
|
||||
if (moduleInfo) {
|
||||
installed = moduleInfo.enabled || false;
|
||||
running = moduleInfo.running || false;
|
||||
}
|
||||
}
|
||||
|
||||
if (app) {
|
||||
return {
|
||||
name: staticModule.name,
|
||||
version: app.pkg_version || app.version || staticModule.version,
|
||||
note: staticModule.note,
|
||||
id: staticModule.id,
|
||||
installed: installed,
|
||||
running: running
|
||||
};
|
||||
}
|
||||
|
||||
return staticModule;
|
||||
});
|
||||
|
||||
console.log('[DevStatus] Module versions synchronized:', this.moduleStatus.length, 'modules');
|
||||
} catch (error) {
|
||||
console.error('[DevStatus] Failed to sync module versions:', error);
|
||||
this.moduleStatus = this.staticModuleStatus;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate overall progress
|
||||
*/
|
||||
@ -184,13 +273,27 @@ const DevStatusWidget = {
|
||||
/**
|
||||
* Render the widget
|
||||
*/
|
||||
render(containerId) {
|
||||
async render(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`Container #${containerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div class="dev-status-widget">
|
||||
<div class="dsw-loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading development status...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch and sync module versions
|
||||
await this.syncModuleVersions();
|
||||
|
||||
// Render with fresh data
|
||||
const overallProgress = this.getModulesOverallProgress();
|
||||
const currentPhase = this.getCurrentPhase();
|
||||
|
||||
@ -351,11 +454,22 @@ const DevStatusWidget = {
|
||||
const statusLabel = status === 'completed'
|
||||
? `Prêt pour v${this.targetVersion}`
|
||||
: `Progression vers v${this.targetVersion}`;
|
||||
|
||||
// Runtime status indicators
|
||||
const runtimeStatus = module.running ? 'running' : (module.installed ? 'stopped' : 'not-installed');
|
||||
const runtimeIcon = module.running ? '🟢' : (module.installed ? '🟠' : '⚫');
|
||||
const runtimeLabel = module.running ? 'Running' : (module.installed ? 'Installed' : 'Not Installed');
|
||||
|
||||
return `
|
||||
<div class="dsw-module-card dsw-module-${status}">
|
||||
<div class="dsw-module-header">
|
||||
<span class="dsw-module-name">${module.name}</span>
|
||||
<span class="dsw-module-version">${this.formatVersion(module.version)}</span>
|
||||
<div class="dsw-module-badges">
|
||||
<span class="dsw-module-version">${this.formatVersion(module.version)}</span>
|
||||
<span class="dsw-module-runtime-badge dsw-runtime-${runtimeStatus}" title="${runtimeLabel}">
|
||||
${runtimeIcon}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dsw-module-status-row">
|
||||
<span class="dsw-module-status-indicator">${status === 'completed' ? '✅' : '🔄'}</span>
|
||||
@ -577,6 +691,33 @@ const DevStatusWidget = {
|
||||
color: var(--sb-text, #f1f5f9);
|
||||
}
|
||||
|
||||
.dsw-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dsw-loading .spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(99, 102, 241, 0.1);
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.dsw-loading p {
|
||||
color: var(--sb-text-muted, #94a3b8);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.dsw-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -983,16 +1124,60 @@ const DevStatusWidget = {
|
||||
.dsw-module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dsw-module-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dsw-module-version {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--sb-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.dsw-module-runtime-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: help;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.dsw-module-runtime-badge:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.dsw-runtime-running {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
|
||||
animation: pulse-green 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dsw-runtime-stopped {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.dsw-runtime-not-installed {
|
||||
background: rgba(107, 116, 128, 0.15);
|
||||
box-shadow: 0 0 0 2px rgba(107, 116, 128, 0.2);
|
||||
}
|
||||
|
||||
@keyframes pulse-green {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.dsw-module-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1406,18 +1406,36 @@ save_settings() {
|
||||
|
||||
# Get components (leverages secubox module detection)
|
||||
get_components() {
|
||||
# Call secubox backend to get modules, which are the system components
|
||||
local result=$(ubus call luci.secubox getModules 2>/dev/null)
|
||||
# Call secubox backend to get apps list
|
||||
local apps_result=$(ubus call luci.secubox list_apps 2>/dev/null)
|
||||
|
||||
if [ -n "$result" ]; then
|
||||
# Pass through the secubox modules as components
|
||||
echo "$result"
|
||||
if [ -n "$apps_result" ]; then
|
||||
# Transform apps to components format
|
||||
echo "$apps_result" | jq '{
|
||||
modules: [
|
||||
.apps[] | {
|
||||
id: .id,
|
||||
name: .name,
|
||||
version: (.pkg_version // .version // "1.0.0"),
|
||||
category: (.category // "system"),
|
||||
description: (.description // "No description"),
|
||||
icon: (.icon // "📦"),
|
||||
package: (.packages.required[0] // ""),
|
||||
installed: false,
|
||||
running: false,
|
||||
color: (
|
||||
if .category == "security" then "#ef4444"
|
||||
elif .category == "monitoring" then "#10b981"
|
||||
elif .category == "network" then "#3b82f6"
|
||||
else "#64748b"
|
||||
end
|
||||
)
|
||||
}
|
||||
]
|
||||
}'
|
||||
else
|
||||
# Fallback if secubox is not available
|
||||
json_init
|
||||
json_add_array "modules"
|
||||
json_close_array
|
||||
json_dump
|
||||
echo '{"modules":[]}'
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@ -864,6 +864,52 @@ case "$1" in
|
||||
if [ "$widget_enabled" = "true" ]; then
|
||||
json_add_boolean "widget_enabled" true
|
||||
|
||||
# Get version information from catalog
|
||||
catalog_version=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].version" 2>/dev/null)
|
||||
pkg_version=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].pkg_version" 2>/dev/null)
|
||||
|
||||
[ -n "$catalog_version" ] && json_add_string "catalog_version" "$catalog_version"
|
||||
[ -n "$pkg_version" ] && json_add_string "pkg_version" "$pkg_version"
|
||||
|
||||
# Get installed version from opkg
|
||||
installed_version=""
|
||||
if [ -n "$pkg_version" ]; then
|
||||
package_name=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].packages.required[0]" 2>/dev/null)
|
||||
if [ -n "$package_name" ]; then
|
||||
installed_version=$(opkg info "$package_name" 2>/dev/null | awk '/^Version:/ {print $2}')
|
||||
fi
|
||||
fi
|
||||
[ -n "$installed_version" ] && json_add_string "installed_version" "$installed_version"
|
||||
|
||||
# Check if installed and running
|
||||
json_add_boolean "installed" false
|
||||
json_add_boolean "running" false
|
||||
json_add_string "status" "unknown"
|
||||
|
||||
# Check installation status via ubus
|
||||
if command -v ubus >/dev/null 2>&1; then
|
||||
modules_json=$(ubus call luci.secubox get_modules 2>/dev/null)
|
||||
if [ -n "$modules_json" ]; then
|
||||
package_name=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].packages.required[0]" 2>/dev/null)
|
||||
if [ -n "$package_name" ]; then
|
||||
module_enabled=$(echo "$modules_json" | jsonfilter -e "@.modules['$package_name'].enabled" 2>/dev/null)
|
||||
module_running=$(echo "$modules_json" | jsonfilter -e "@.modules['$package_name'].running" 2>/dev/null)
|
||||
|
||||
[ "$module_enabled" = "true" ] && json_add_boolean "installed" true
|
||||
[ "$module_running" = "true" ] && json_add_boolean "running" true
|
||||
|
||||
# Set status based on state
|
||||
if [ "$module_running" = "true" ]; then
|
||||
json_add_string "status" "running"
|
||||
elif [ "$module_enabled" = "true" ]; then
|
||||
json_add_string "status" "stopped"
|
||||
else
|
||||
json_add_string "status" "not_installed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get metrics from catalog definition
|
||||
# This would call app-specific data sources (ubus, files, etc.)
|
||||
# For now, return placeholder structure
|
||||
|
||||
@ -92,7 +92,6 @@ get_active_catalog() {
|
||||
# List all modules
|
||||
list_modules() {
|
||||
local format="${1:-table}"
|
||||
local modules=()
|
||||
local cache_ready=0
|
||||
|
||||
mkdir -p "$CATALOG_DIR"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user