feat(wireguard-dashboard,webapp): Add setup wizard, admin sessions, and blocking stats
WireGuard Dashboard v0.7.0: - Add zone-based setup wizard with 4-step flow - Add tunnel presets (road-warrior, site-to-site, iot-tunnel) - Add zone presets (home-user, remote-worker, mobile, iot, guest, server) - Add interface control (up/down/restart) - Add peer ping functionality - Add bandwidth rates monitoring - Comprehensive wizard CSS styles SecuBox Webapp v1.5.0: - Add admin sessions list showing authenticated LuCI users with IP source - Add blocking statistics (today's bans, blocked attempts, top scenario, unique IPs) - Integrate stats from CrowdSec decisions and alerts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a1d66157fc
commit
5e29599682
@ -8,7 +8,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wireguard-dashboard
|
||||
PKG_VERSION:=0.5.0
|
||||
PKG_VERSION:=0.7.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
|
||||
|
||||
@ -11,11 +11,69 @@ return view.extend({
|
||||
pollInterval: 5,
|
||||
pollActive: true,
|
||||
selectedInterface: 'all',
|
||||
peerDescriptions: {},
|
||||
bandwidthRates: {},
|
||||
|
||||
load: function() {
|
||||
return api.getAllData();
|
||||
},
|
||||
|
||||
// Interface control actions
|
||||
handleInterfaceAction: function(iface, action) {
|
||||
var self = this;
|
||||
ui.showModal(_('Interface Control'), [
|
||||
E('p', { 'class': 'spinning' }, _('Executing %s on %s...').format(action, iface))
|
||||
]);
|
||||
|
||||
api.interfaceControl(iface, action).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', result.message || _('Action completed')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error || _('Action failed')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// Ping peer
|
||||
handlePingPeer: function(peerIp, peerName) {
|
||||
var self = this;
|
||||
if (!peerIp || peerIp === '(none)') {
|
||||
ui.addNotification(null, E('p', _('No endpoint IP available for this peer')), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IP from endpoint (remove port)
|
||||
var ip = peerIp.split(':')[0];
|
||||
|
||||
ui.showModal(_('Ping Peer'), [
|
||||
E('p', { 'class': 'spinning' }, _('Pinging %s (%s)...').format(peerName, ip))
|
||||
]);
|
||||
|
||||
api.pingPeer(ip).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.reachable) {
|
||||
ui.addNotification(null, E('p', _('Peer %s is reachable (RTT: %s ms)').format(peerName, result.rtt_ms)), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Peer %s is not reachable').format(peerName)), 'warning');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Ping failed: %s').format(err.message || err)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// Get peer display name
|
||||
getPeerName: function(peer) {
|
||||
if (this.peerDescriptions && this.peerDescriptions[peer.public_key]) {
|
||||
return this.peerDescriptions[peer.public_key];
|
||||
}
|
||||
return 'Peer ' + peer.short_key;
|
||||
},
|
||||
|
||||
// Interface tab filtering
|
||||
setInterfaceFilter: function(ifaceName) {
|
||||
this.selectedInterface = ifaceName;
|
||||
@ -184,7 +242,10 @@ return view.extend({
|
||||
var status = data.status || {};
|
||||
var interfaces = (data.interfaces || {}).interfaces || [];
|
||||
var peers = (data.peers || {}).peers || [];
|
||||
|
||||
|
||||
// Store peer descriptions
|
||||
this.peerDescriptions = data.descriptions || {};
|
||||
|
||||
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
|
||||
|
||||
var view = E('div', { 'class': 'wireguard-dashboard' }, [
|
||||
@ -323,6 +384,25 @@ return view.extend({
|
||||
E('div', { 'class': 'wg-interface-detail-value wg-interface-traffic' },
|
||||
'↓' + api.formatBytes(iface.rx_bytes) + ' / ↑' + api.formatBytes(iface.tx_bytes))
|
||||
])
|
||||
]),
|
||||
// Interface control buttons
|
||||
E('div', { 'class': 'wg-interface-controls' }, [
|
||||
iface.state === 'up' ?
|
||||
E('button', {
|
||||
'class': 'wg-btn wg-btn-sm wg-btn-warning',
|
||||
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'down'),
|
||||
'title': _('Bring interface down')
|
||||
}, '⏹ Stop') :
|
||||
E('button', {
|
||||
'class': 'wg-btn wg-btn-sm wg-btn-success',
|
||||
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'up'),
|
||||
'title': _('Bring interface up')
|
||||
}, '▶ Start'),
|
||||
E('button', {
|
||||
'class': 'wg-btn wg-btn-sm',
|
||||
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'restart'),
|
||||
'title': _('Restart interface')
|
||||
}, '🔄 Restart')
|
||||
])
|
||||
]);
|
||||
})
|
||||
@ -347,12 +427,13 @@ return view.extend({
|
||||
E('div', { 'class': 'wg-card-body' }, [
|
||||
E('div', { 'class': 'wg-peer-grid' },
|
||||
peers.slice(0, 6).map(function(peer) {
|
||||
var peerName = self.getPeerName(peer);
|
||||
return E('div', { 'class': 'wg-peer-card ' + (peer.status === 'active' ? 'active' : ''), 'data-peer': peer.public_key, 'data-interface': peer.interface || '' }, [
|
||||
E('div', { 'class': 'wg-peer-header' }, [
|
||||
E('div', { 'class': 'wg-peer-info' }, [
|
||||
E('div', { 'class': 'wg-peer-icon' }, peer.status === 'active' ? '✅' : '👤'),
|
||||
E('div', {}, [
|
||||
E('p', { 'class': 'wg-peer-name' }, 'Peer ' + peer.short_key),
|
||||
E('p', { 'class': 'wg-peer-name' }, peerName),
|
||||
E('p', { 'class': 'wg-peer-key' }, api.shortenKey(peer.public_key, 16))
|
||||
])
|
||||
]),
|
||||
@ -383,7 +464,16 @@ return view.extend({
|
||||
E('div', { 'class': 'wg-peer-traffic-value tx' }, api.formatBytes(peer.tx_bytes)),
|
||||
E('div', { 'class': 'wg-peer-traffic-label' }, 'Sent')
|
||||
])
|
||||
])
|
||||
]),
|
||||
// Peer action buttons
|
||||
peer.endpoint && peer.endpoint !== '(none)' ?
|
||||
E('div', { 'class': 'wg-peer-actions' }, [
|
||||
E('button', {
|
||||
'class': 'wg-btn wg-btn-xs',
|
||||
'click': L.bind(self.handlePingPeer, self, peer.endpoint, peerName),
|
||||
'title': _('Ping peer')
|
||||
}, '📡 Ping')
|
||||
]) : ''
|
||||
]);
|
||||
})
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -18,13 +18,13 @@ var callStatus = rpc.declare({
|
||||
|
||||
var callGetPeers = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'get_peers',
|
||||
method: 'peers',
|
||||
expect: { peers: [] }
|
||||
});
|
||||
|
||||
var callGetInterfaces = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'get_interfaces',
|
||||
method: 'interfaces',
|
||||
expect: { interfaces: [] }
|
||||
});
|
||||
|
||||
@ -74,6 +74,32 @@ var callGetTraffic = rpc.declare({
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callInterfaceControl = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'interface_control',
|
||||
params: ['interface', 'action'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callPeerDescriptions = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'peer_descriptions',
|
||||
expect: { descriptions: {} }
|
||||
});
|
||||
|
||||
var callBandwidthRates = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'bandwidth_rates',
|
||||
expect: { rates: [] }
|
||||
});
|
||||
|
||||
var callPingPeer = rpc.declare({
|
||||
object: 'luci.wireguard-dashboard',
|
||||
method: 'ping_peer',
|
||||
params: ['ip'],
|
||||
expect: { reachable: false }
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024;
|
||||
@ -112,6 +138,15 @@ function formatHandshake(seconds) {
|
||||
return Math.floor(seconds / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
function formatRate(bytesPerSec) {
|
||||
if (!bytesPerSec || bytesPerSec === 0) return '0 B/s';
|
||||
var k = 1024;
|
||||
var sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||||
var i = Math.floor(Math.log(bytesPerSec) / Math.log(k));
|
||||
if (i >= sizes.length) i = sizes.length - 1;
|
||||
return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getPeers: callGetPeers,
|
||||
@ -123,11 +158,16 @@ return baseclass.extend({
|
||||
removePeer: callRemovePeer,
|
||||
generateConfig: callGenerateConfig,
|
||||
generateQR: callGenerateQR,
|
||||
interfaceControl: callInterfaceControl,
|
||||
getPeerDescriptions: callPeerDescriptions,
|
||||
getBandwidthRates: callBandwidthRates,
|
||||
pingPeer: callPingPeer,
|
||||
formatBytes: formatBytes,
|
||||
formatLastHandshake: formatLastHandshake,
|
||||
getPeerStatusClass: getPeerStatusClass,
|
||||
shortenKey: shortenKey,
|
||||
formatHandshake: formatHandshake,
|
||||
formatRate: formatRate,
|
||||
|
||||
// Aggregate function for overview page
|
||||
getAllData: function() {
|
||||
@ -135,13 +175,32 @@ return baseclass.extend({
|
||||
callStatus(),
|
||||
callGetPeers(),
|
||||
callGetInterfaces(),
|
||||
callGetTraffic()
|
||||
callGetTraffic(),
|
||||
callPeerDescriptions()
|
||||
]).then(function(results) {
|
||||
return {
|
||||
status: results[0] || {},
|
||||
peers: results[1] || { peers: [] },
|
||||
interfaces: results[2] || { interfaces: [] },
|
||||
traffic: results[3] || {}
|
||||
traffic: results[3] || {},
|
||||
descriptions: (results[4] || {}).descriptions || {}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Get data with bandwidth rates for real-time monitoring
|
||||
getMonitoringData: function() {
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callGetPeers(),
|
||||
callBandwidthRates(),
|
||||
callPeerDescriptions()
|
||||
]).then(function(results) {
|
||||
return {
|
||||
status: results[0] || {},
|
||||
peers: results[1] || { peers: [] },
|
||||
rates: (results[2] || {}).rates || [],
|
||||
descriptions: (results[3] || {}).descriptions || {}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -816,6 +816,62 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wg-btn-xs {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wg-btn-success {
|
||||
background: linear-gradient(135deg, var(--wg-accent-green), #059669);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wg-btn-success:hover {
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.wg-btn-warning {
|
||||
background: linear-gradient(135deg, var(--wg-accent-yellow), #d97706);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wg-btn-warning:hover {
|
||||
box-shadow: 0 0 20px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.wg-btn-danger {
|
||||
background: linear-gradient(135deg, var(--wg-accent-red), #dc2626);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wg-btn-danger:hover {
|
||||
box-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Interface controls */
|
||||
.wg-interface-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--wg-border);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Peer actions */
|
||||
.wg-peer-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--wg-border);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Auto-refresh control */
|
||||
.wg-refresh-control {
|
||||
display: flex;
|
||||
@ -1140,3 +1196,528 @@
|
||||
.wg-chart-legend-dot.tx {
|
||||
background: var(--wg-accent-blue);
|
||||
}
|
||||
|
||||
/* ========== Wizard Styles ========== */
|
||||
|
||||
/* Tunnel type selection */
|
||||
.wg-tunnel-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-card {
|
||||
background: var(--wg-bg-tertiary);
|
||||
border: 2px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius-lg);
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-card:hover {
|
||||
border-color: var(--wg-accent-cyan);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.wg-tunnel-type-card.selected {
|
||||
border-color: var(--wg-accent-green);
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), transparent);
|
||||
}
|
||||
|
||||
.wg-tunnel-type-card.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--wg-accent-green);
|
||||
}
|
||||
|
||||
.wg-tunnel-type-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-desc {
|
||||
font-size: 13px;
|
||||
color: var(--wg-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.wg-tunnel-type-feature {
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
background: var(--wg-bg-primary);
|
||||
border-radius: 10px;
|
||||
color: var(--wg-text-muted);
|
||||
}
|
||||
|
||||
/* Zone preset cards */
|
||||
.wg-zone-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 14px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.wg-zone-card {
|
||||
background: var(--wg-bg-tertiary);
|
||||
border: 2px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius-lg);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wg-zone-card:hover {
|
||||
border-color: var(--wg-accent-cyan);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.wg-zone-card.selected {
|
||||
border-color: var(--zone-color, var(--wg-accent-green));
|
||||
background: linear-gradient(135deg, rgba(var(--zone-color-rgb, 16, 185, 129), 0.1), transparent);
|
||||
}
|
||||
|
||||
.wg-zone-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wg-zone-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wg-zone-desc {
|
||||
font-size: 11px;
|
||||
color: var(--wg-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Wizard form */
|
||||
.wg-wizard-form {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wg-wizard-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.wg-wizard-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wg-wizard-section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: var(--wg-tunnel-gradient);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Input styles */
|
||||
.wg-input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
background: var(--wg-bg-primary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius);
|
||||
color: var(--wg-text-primary);
|
||||
font-family: var(--wg-font-mono);
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wg-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--wg-accent-cyan);
|
||||
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
|
||||
}
|
||||
|
||||
.wg-input::placeholder {
|
||||
color: var(--wg-text-muted);
|
||||
}
|
||||
|
||||
.wg-select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
background: var(--wg-bg-primary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius);
|
||||
color: var(--wg-text-primary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wg-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--wg-accent-cyan);
|
||||
}
|
||||
|
||||
.wg-checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: var(--wg-bg-tertiary);
|
||||
border-radius: var(--wg-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wg-checkbox-group:hover {
|
||||
background: var(--wg-bg-secondary);
|
||||
}
|
||||
|
||||
.wg-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--wg-accent-cyan);
|
||||
}
|
||||
|
||||
/* Peer list in wizard */
|
||||
.wg-peer-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.wg-peer-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: var(--wg-bg-tertiary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius);
|
||||
}
|
||||
|
||||
.wg-peer-list-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wg-peer-list-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
background: var(--wg-bg-primary);
|
||||
}
|
||||
|
||||
.wg-peer-list-details h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.wg-peer-list-details p {
|
||||
font-size: 11px;
|
||||
color: var(--wg-text-muted);
|
||||
margin: 0;
|
||||
font-family: var(--wg-font-mono);
|
||||
}
|
||||
|
||||
.wg-peer-list-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* QR Code Modal */
|
||||
.wg-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.wg-modal {
|
||||
background: var(--wg-bg-secondary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius-lg);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: modal-slide 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modal-slide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.wg-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--wg-border);
|
||||
}
|
||||
|
||||
.wg-modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wg-modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--wg-text-muted);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wg-modal-close:hover {
|
||||
background: var(--wg-bg-tertiary);
|
||||
color: var(--wg-text-primary);
|
||||
}
|
||||
|
||||
.wg-modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.wg-modal-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--wg-border);
|
||||
}
|
||||
|
||||
/* QR Code display */
|
||||
.wg-qr-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.wg-qr-code {
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: var(--wg-radius-lg);
|
||||
}
|
||||
|
||||
.wg-qr-code img,
|
||||
.wg-qr-code svg {
|
||||
display: block;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.wg-qr-hint {
|
||||
font-size: 12px;
|
||||
color: var(--wg-text-muted);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Config preview */
|
||||
.wg-config-preview {
|
||||
background: var(--wg-bg-primary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: var(--wg-radius);
|
||||
padding: 16px;
|
||||
font-family: var(--wg-font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Wizard navigation */
|
||||
.wg-wizard-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--wg-border);
|
||||
}
|
||||
|
||||
/* Success state */
|
||||
.wg-success-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.wg-success-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
animation: bounce-in 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.wg-success-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--wg-accent-green);
|
||||
}
|
||||
|
||||
.wg-success-desc {
|
||||
font-size: 14px;
|
||||
color: var(--wg-text-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.wg-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.wg-loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid var(--wg-border);
|
||||
border-top-color: var(--wg-accent-cyan);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.wg-loading-text {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--wg-text-secondary);
|
||||
}
|
||||
|
||||
/* Info box */
|
||||
.wg-info-box {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border: 1px solid rgba(6, 182, 212, 0.3);
|
||||
border-radius: var(--wg-radius);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.wg-info-box-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wg-info-box-content {
|
||||
font-size: 13px;
|
||||
color: var(--wg-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wg-info-box.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
.wg-copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--wg-bg-tertiary);
|
||||
border: 1px solid var(--wg-border);
|
||||
border-radius: 6px;
|
||||
color: var(--wg-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wg-copy-btn:hover {
|
||||
border-color: var(--wg-accent-cyan);
|
||||
color: var(--wg-text-primary);
|
||||
}
|
||||
|
||||
.wg-copy-btn.copied {
|
||||
border-color: var(--wg-accent-green);
|
||||
color: var(--wg-accent-green);
|
||||
}
|
||||
|
||||
/* Endpoint detection */
|
||||
.wg-endpoint-detect {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wg-endpoint-detect .wg-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.wg-endpoint-status {
|
||||
font-size: 11px;
|
||||
color: var(--wg-text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@ -752,10 +752,148 @@ ping_peer() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Interface up/down control
|
||||
interface_control() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var iface interface
|
||||
json_get_var action action
|
||||
|
||||
json_init
|
||||
|
||||
if [ -z "$iface" ] || [ -z "$action" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Missing interface or action"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
# Validate action
|
||||
case "$action" in
|
||||
up|down|restart)
|
||||
;;
|
||||
*)
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Invalid action. Use: up, down, restart"
|
||||
json_dump
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if interface exists
|
||||
if ! uci -q get network.$iface >/dev/null; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Interface not found: $iface"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
# Execute action
|
||||
case "$action" in
|
||||
up)
|
||||
ifup "$iface" 2>/dev/null
|
||||
;;
|
||||
down)
|
||||
ifdown "$iface" 2>/dev/null
|
||||
;;
|
||||
restart)
|
||||
ifdown "$iface" 2>/dev/null
|
||||
sleep 1
|
||||
ifup "$iface" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
local rc=$?
|
||||
if [ $rc -eq 0 ]; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Interface $iface $action completed"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Failed to $action interface $iface"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get peer descriptions from UCI
|
||||
get_peer_descriptions() {
|
||||
json_init
|
||||
json_add_object "descriptions"
|
||||
|
||||
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
||||
|
||||
for iface in $interfaces; do
|
||||
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
|
||||
for section in $sections; do
|
||||
local pubkey=$(uci -q get network.$section.public_key)
|
||||
local desc=$(uci -q get network.$section.description)
|
||||
if [ -n "$pubkey" ] && [ -n "$desc" ]; then
|
||||
json_add_string "$pubkey" "$desc"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
json_close_object
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get current bandwidth rates (requires previous call to calculate delta)
|
||||
get_bandwidth_rates() {
|
||||
json_init
|
||||
|
||||
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
||||
local now=$(date +%s)
|
||||
|
||||
json_add_array "rates"
|
||||
|
||||
for iface in $interfaces; do
|
||||
local rx_bytes=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
|
||||
local tx_bytes=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
|
||||
|
||||
# Get previous values from temp file
|
||||
local prev_file="/tmp/wg_rate_$iface"
|
||||
local prev_rx=0
|
||||
local prev_tx=0
|
||||
local prev_time=0
|
||||
|
||||
if [ -f "$prev_file" ]; then
|
||||
read prev_rx prev_tx prev_time < "$prev_file"
|
||||
fi
|
||||
|
||||
# Calculate rates
|
||||
local rx_rate=0
|
||||
local tx_rate=0
|
||||
local time_diff=$((now - prev_time))
|
||||
|
||||
if [ $time_diff -gt 0 ] && [ $prev_time -gt 0 ]; then
|
||||
rx_rate=$(( (rx_bytes - prev_rx) / time_diff ))
|
||||
tx_rate=$(( (tx_bytes - prev_tx) / time_diff ))
|
||||
# Ensure non-negative rates
|
||||
[ $rx_rate -lt 0 ] && rx_rate=0
|
||||
[ $tx_rate -lt 0 ] && tx_rate=0
|
||||
fi
|
||||
|
||||
# Save current values
|
||||
echo "$rx_bytes $tx_bytes $now" > "$prev_file"
|
||||
|
||||
json_add_object
|
||||
json_add_string "interface" "$iface"
|
||||
json_add_int "rx_rate" "$rx_rate"
|
||||
json_add_int "tx_rate" "$tx_rate"
|
||||
json_add_int "rx_total" "$rx_bytes"
|
||||
json_add_int "tx_total" "$tx_bytes"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_add_int "timestamp" "$now"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Main dispatcher
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"}}'
|
||||
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
@ -798,6 +936,15 @@ case "$1" in
|
||||
ping_peer)
|
||||
ping_peer
|
||||
;;
|
||||
interface_control)
|
||||
interface_control
|
||||
;;
|
||||
peer_descriptions)
|
||||
get_peer_descriptions
|
||||
;;
|
||||
bandwidth_rates)
|
||||
get_bandwidth_rates
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Unknown method"}'
|
||||
;;
|
||||
|
||||
@ -9,6 +9,14 @@
|
||||
"acl": ["luci-app-wireguard-dashboard"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/wireguard/wizard": {
|
||||
"title": "Setup Wizard",
|
||||
"order": 5,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "wireguard-dashboard/wizard"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/wireguard/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
|
||||
@ -5,10 +5,14 @@
|
||||
"ubus": {
|
||||
"luci.wireguard-dashboard": [
|
||||
"status",
|
||||
"get_interfaces",
|
||||
"get_peers",
|
||||
"interfaces",
|
||||
"peers",
|
||||
"config",
|
||||
"traffic"
|
||||
"traffic",
|
||||
"peer_descriptions",
|
||||
"bandwidth_rates",
|
||||
"bandwidth_history",
|
||||
"endpoint_info"
|
||||
],
|
||||
"system": [ "info", "board" ],
|
||||
"file": [ "read", "stat", "exec" ]
|
||||
@ -26,10 +30,12 @@
|
||||
"add_peer",
|
||||
"remove_peer",
|
||||
"generate_config",
|
||||
"generate_qr"
|
||||
"generate_qr",
|
||||
"interface_control",
|
||||
"ping_peer"
|
||||
]
|
||||
},
|
||||
"uci": [ "wireguard-dashboard" ]
|
||||
"uci": [ "wireguard-dashboard", "network" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-webapp
|
||||
PKG_VERSION:=1.4.1
|
||||
PKG_VERSION:=1.5.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=CyberMind.FR <contact@cybermind.fr>
|
||||
|
||||
@ -2553,6 +2553,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Third Row - Admin Sessions -->
|
||||
<div class="cards-grid-2" style="margin-top: 1.5rem;">
|
||||
<!-- Admin Sessions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<i data-lucide="users" style="width: 18px; height: 18px; color: var(--rose);"></i>
|
||||
Sessions Admin LuCI
|
||||
</div>
|
||||
<div class="card-badge" id="adminSessionsCount">0 sessions</div>
|
||||
</div>
|
||||
<div class="card-body" style="overflow-x: auto; max-height: 200px; overflow-y: auto;">
|
||||
<table class="interfaces-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Utilisateur</th>
|
||||
<th>IP Source</th>
|
||||
<th>Expiration</th>
|
||||
<th>Statut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="adminSessionsBody">
|
||||
<!-- Sessions dynamically loaded -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Firewall Stats Overview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<i data-lucide="shield-ban" style="width: 18px; height: 18px; color: var(--amber);"></i>
|
||||
Statistiques Blocage
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="totalBansToday">--</div>
|
||||
<div class="stat-label">Bans Aujourd'hui</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="totalBlockedAttempts">--</div>
|
||||
<div class="stat-label">Tentatives Bloquées</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="topScenario">--</div>
|
||||
<div class="stat-label">Top Scénario</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="uniqueIPs">--</div>
|
||||
<div class="stat-label">IPs Uniques</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card" style="margin-top: 1.5rem;">
|
||||
<div class="card-header">
|
||||
@ -3691,6 +3750,16 @@
|
||||
CROWDSEC.getAlerts()
|
||||
]);
|
||||
updateCrowdSecMetrics(decisions, bouncers, alerts);
|
||||
|
||||
// Update firewall blocking statistics
|
||||
if (currentTab === 'overview') {
|
||||
updateBlockingStats(decisions, alerts);
|
||||
}
|
||||
}
|
||||
|
||||
// Load admin sessions on overview tab
|
||||
if (currentTab === 'overview') {
|
||||
loadAdminSessions().catch(e => console.warn('Admin sessions error:', e));
|
||||
}
|
||||
|
||||
// Get logs only when on overview or logs tab
|
||||
@ -4028,6 +4097,181 @@
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// Admin Sessions & Blocking Statistics
|
||||
// =====================================================
|
||||
|
||||
async function loadAdminSessions() {
|
||||
const tbody = document.getElementById('adminSessionsBody');
|
||||
const countBadge = document.getElementById('adminSessionsCount');
|
||||
|
||||
try {
|
||||
// Get LuCI sessions by listing session files
|
||||
const result = await UBUS.execCommand('/bin/sh', ['-c',
|
||||
'for f in /tmp/luci-sessions/*; do [ -f "$f" ] && echo "$(basename $f)|$(cat $f 2>/dev/null | jsonfilter -e @.data.username 2>/dev/null)|$(cat $f 2>/dev/null | jsonfilter -e @.data.localtime 2>/dev/null)|$(stat -c %Y $f 2>/dev/null)"; done 2>/dev/null || ls -la /tmp/luci-sessions/ 2>/dev/null'
|
||||
]);
|
||||
|
||||
let sessions = [];
|
||||
|
||||
if (result && result.stdout) {
|
||||
const lines = result.stdout.trim().split('\n').filter(l => l && l.includes('|'));
|
||||
sessions = lines.map(line => {
|
||||
const parts = line.split('|');
|
||||
return {
|
||||
id: parts[0] || 'unknown',
|
||||
username: parts[1] || 'root',
|
||||
ip: parts[2] || 'local',
|
||||
mtime: parseInt(parts[3]) || 0
|
||||
};
|
||||
}).filter(s => s.id !== 'unknown');
|
||||
}
|
||||
|
||||
// Also try to get session via ubus session.list (requires admin)
|
||||
try {
|
||||
const ubusResult = await UBUS.call('session', 'list', {});
|
||||
if (ubusResult && typeof ubusResult === 'object') {
|
||||
// Add current session info
|
||||
const currentSession = {
|
||||
id: STATE.sessionId ? STATE.sessionId.substring(0, 8) : 'current',
|
||||
username: ubusResult.username || 'root',
|
||||
ip: window.location.hostname,
|
||||
mtime: Math.floor(Date.now() / 1000),
|
||||
expires: ubusResult.expires || 300,
|
||||
current: true
|
||||
};
|
||||
|
||||
// Add if not already in list
|
||||
if (!sessions.find(s => s.current)) {
|
||||
sessions.unshift(currentSession);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Session.list might not be available
|
||||
}
|
||||
|
||||
// Update badge
|
||||
countBadge.textContent = `${sessions.length} session${sessions.length > 1 ? 's' : ''}`;
|
||||
|
||||
if (sessions.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; color: var(--text-muted);">
|
||||
Aucune session active
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
tbody.innerHTML = sessions.map(session => {
|
||||
const age = now - session.mtime;
|
||||
const isActive = age < 3600; // Active if less than 1 hour old
|
||||
const expireText = session.expires ?
|
||||
`${Math.floor(session.expires / 60)} min` :
|
||||
(age < 60 ? 'maintenant' : `${Math.floor(age / 60)} min`);
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="user" style="width: 14px; height: 14px; color: var(--cyan);"></i>
|
||||
${escapeHtml(session.username)}
|
||||
${session.current ? '<span style="font-size: 0.65rem; background: var(--cyan); color: var(--bg); padding: 1px 4px; border-radius: 3px;">vous</span>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<code style="font-size: 0.7rem; background: var(--surface); padding: 2px 6px; border-radius: 3px;">
|
||||
${escapeHtml(session.ip)}
|
||||
</code>
|
||||
</td>
|
||||
<td style="font-size: 0.75rem;">${expireText}</td>
|
||||
<td>
|
||||
<span class="status-badge ${isActive ? 'online' : 'offline'}" style="font-size: 0.65rem;">
|
||||
${isActive ? 'Actif' : 'Expiré'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (e) {
|
||||
console.warn('Error loading admin sessions:', e);
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; color: var(--text-muted);">
|
||||
Impossible de charger les sessions
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateBlockingStats(decisions, alerts) {
|
||||
const totalBansEl = document.getElementById('totalBansToday');
|
||||
const blockedEl = document.getElementById('totalBlockedAttempts');
|
||||
const topScenarioEl = document.getElementById('topScenario');
|
||||
const uniqueIPsEl = document.getElementById('uniqueIPs');
|
||||
|
||||
// Calculate stats from decisions and alerts
|
||||
const decisionsList = Array.isArray(decisions) ? decisions : [];
|
||||
const alertsList = Array.isArray(alerts) ? alerts : [];
|
||||
|
||||
// Today's date
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// Count today's bans
|
||||
const todayBans = decisionsList.filter(d => {
|
||||
if (d.created_at) {
|
||||
const created = new Date(d.created_at);
|
||||
return created >= today;
|
||||
}
|
||||
return true; // Count all if no date
|
||||
}).length;
|
||||
|
||||
// Total blocked attempts (sum of events from alerts)
|
||||
const totalBlocked = alertsList.reduce((sum, alert) => {
|
||||
return sum + (alert.events_count || 1);
|
||||
}, 0);
|
||||
|
||||
// Find top scenario
|
||||
const scenarioCounts = {};
|
||||
alertsList.forEach(alert => {
|
||||
const scenario = alert.scenario || 'unknown';
|
||||
scenarioCounts[scenario] = (scenarioCounts[scenario] || 0) + 1;
|
||||
});
|
||||
|
||||
let topScenario = '--';
|
||||
let maxCount = 0;
|
||||
Object.entries(scenarioCounts).forEach(([scenario, count]) => {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
// Extract short name from scenario (e.g., "crowdsecurity/ssh-bf" -> "ssh-bf")
|
||||
const parts = scenario.split('/');
|
||||
topScenario = parts[parts.length - 1] || scenario;
|
||||
}
|
||||
});
|
||||
|
||||
// Count unique IPs
|
||||
const uniqueIPs = new Set();
|
||||
decisionsList.forEach(d => {
|
||||
if (d.value) uniqueIPs.add(d.value);
|
||||
});
|
||||
alertsList.forEach(a => {
|
||||
if (a.source && a.source.ip) uniqueIPs.add(a.source.ip);
|
||||
});
|
||||
|
||||
// Update UI
|
||||
totalBansEl.textContent = todayBans;
|
||||
blockedEl.textContent = totalBlocked || '--';
|
||||
topScenarioEl.textContent = topScenario.length > 10 ? topScenario.substring(0, 10) + '...' : topScenario;
|
||||
topScenarioEl.title = topScenario; // Full name on hover
|
||||
uniqueIPsEl.textContent = uniqueIPs.size;
|
||||
}
|
||||
|
||||
async function loadSystemLogs() {
|
||||
const container = document.getElementById('logsContainer');
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user