style(device-intel): Migrate dashboard views to KISS theme

- dashboard.js: KISS stats grid, source chips, type cards, recent devices table
- devices.js: KISS filter bar, device table with inline actions, edit/detail modals
- emulators.js: KISS emulator cards with status badges, mini tables
- mesh.js: KISS peer cards grid, remote devices table

Removes external CSS loading (cssLink pattern) and di-* class prefixes.
Uses KissTheme.E(), kiss-* classes, and CSS variables throughout.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-12 12:11:42 +01:00
parent 04325c789f
commit 7a9de56ba1
4 changed files with 385 additions and 343 deletions

View File

@ -5,60 +5,35 @@
'require device-intel/api as api';
'require secubox/kiss-theme';
return view.extend({
css: null,
/**
* Device Intelligence Dashboard - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({
load: function() {
return Promise.all([
api.getSummary(),
api.getDevices(),
api.getDeviceTypes(),
L.require('device-intel/common.css')
.catch(function() { return null; })
api.getDeviceTypes()
]);
},
render: function(data) {
var self = this;
var K = KissTheme;
var summary = data[0] || {};
var devResult = data[1] || {};
var typesResult = data[2] || {};
var devices = devResult.devices || [];
var types = typesResult.types || [];
// Include CSS
var cssLink = E('link', {
rel: 'stylesheet',
href: L.resource('device-intel/common.css')
});
// Count at-risk devices
var atRiskCount = devices.filter(function(d) {
return d.risk_score > 0;
}).length;
// ── Summary Stat Cards ──
var stats = E('div', { 'class': 'di-stats' }, [
this.statCard(summary.total || 0, _('Total Devices'), '#3b82f6'),
this.statCard(summary.online || 0, _('Online'), '#22c55e'),
this.statCard(summary.mesh_peers || 0, _('Mesh Peers'), '#6366f1'),
this.statCard(devices.filter(function(d) {
return d.risk_score > 0;
}).length, _('At Risk'), '#ef4444')
]);
// ── Data Source Status ──
var sources = summary.sources || {};
var sourceBar = E('div', { 'class': 'di-source-bar' }, [
this.sourceChip('MAC Guardian', sources.mac_guardian),
this.sourceChip('Client Guardian', sources.client_guardian),
this.sourceChip('DHCP Leases', sources.dhcp),
this.sourceChip('P2P Mesh', sources.p2p)
]);
// ── Emulator Status ──
var emus = summary.emulators || {};
var emuBar = E('div', { 'class': 'di-source-bar' }, [
this.sourceChip('USB', emus.usb),
this.sourceChip('MQTT', emus.mqtt),
this.sourceChip('Zigbee', emus.zigbee)
]);
// ── Device Type Distribution ──
// Build type counts
var typeCounts = {};
devices.forEach(function(d) {
var t = d.device_type || 'unknown';
@ -68,122 +43,149 @@ return view.extend({
var typeMap = {};
types.forEach(function(t) { typeMap[t.id] = t; });
var typeCards = Object.keys(typeCounts).sort(function(a, b) {
return typeCounts[b] - typeCounts[a];
}).map(function(tid) {
var info = typeMap[tid] || { name: tid, color: '#6c757d' };
return E('div', {
'class': 'di-type-card',
'style': '--type-color:' + (info.color || '#6c757d')
}, [
E('span', { 'class': 'count' }, String(typeCounts[tid])),
E('span', { 'class': 'name' }, info.name || tid)
]);
});
var typeGrid = E('div', { 'class': 'di-type-grid' }, typeCards);
// ── Zone Distribution ──
// Build zone counts
var zoneCounts = {};
devices.forEach(function(d) {
var z = d.cg_zone || 'unzoned';
zoneCounts[z] = (zoneCounts[z] || 0) + 1;
});
var zoneItems = Object.keys(zoneCounts).map(function(z) {
return E('span', {
'class': 'di-source-chip',
'style': 'background:#e0e7ff; color:#3730a3;'
}, z + ' (' + zoneCounts[z] + ')');
});
// ── Recent Devices (last 5 seen) ──
// Recent devices (last 5 seen)
var recent = devices
.filter(function(d) { return d.last_seen; })
.sort(function(a, b) { return (b.last_seen || 0) - (a.last_seen || 0); })
.slice(0, 5);
var recentRows = recent.map(function(d) {
return E('tr', {}, [
E('td', {}, [
E('span', {
'class': 'di-online-dot ' + (d.online ? 'online' : 'offline')
}),
d.label || d.hostname || d.mac
var sources = summary.sources || {};
var emus = summary.emulators || {};
var content = K.E('div', {}, [
// Page Header
K.E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;' }, [
K.E('div', {}, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
K.E('span', {}, '🔍'),
'Device Intelligence'
]),
K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
'Network device discovery and monitoring')
]),
E('td', {}, d.ip || '-'),
E('td', {}, d.device_type || '-'),
E('td', {}, d.vendor || '-')
]);
});
K.E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'padding: 10px 16px; font-size: 14px;',
'click': function() {
api.refresh().then(function() {
window.location.href = window.location.pathname + '?' + Date.now();
});
}
}, '🔄 Refresh')
]),
var recentTable = E('table', { 'class': 'di-device-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('Device')),
E('th', {}, _('IP')),
E('th', {}, _('Type')),
E('th', {}, _('Vendor'))
])),
E('tbody', {}, recentRows.length > 0 ? recentRows :
[E('tr', {}, E('td', { 'colspan': '4', 'style': 'text-align:center; color:#6c757d;' },
_('No recent devices')))])
]);
// Stats Grid
K.E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'gap: 16px; margin-bottom: 20px;' }, [
self.statCard(K, summary.total || 0, 'Total Devices', 'var(--kiss-blue, #3b82f6)', '📱'),
self.statCard(K, summary.online || 0, 'Online', 'var(--kiss-green, #22c55e)', '🟢'),
self.statCard(K, summary.mesh_peers || 0, 'Mesh Peers', 'var(--kiss-purple, #6366f1)', '🔗'),
self.statCard(K, atRiskCount, 'At Risk', 'var(--kiss-red, #ef4444)', '⚠️')
]),
var content = [
cssLink,
E('h2', {}, _('Device Intelligence')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5em;' }, [
E('h3', { 'style': 'margin:0;' }, _('Overview')),
E('button', {
'class': 'cbi-button',
'click': function() {
api.refresh().then(function() {
window.location.href = window.location.pathname + '?' + Date.now();
});
}
}, _('Refresh'))
// Data Sources Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['📡 ', 'Data Sources']),
K.E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px;' }, [
self.sourceChip(K, 'MAC Guardian', sources.mac_guardian),
self.sourceChip(K, 'Client Guardian', sources.client_guardian),
self.sourceChip(K, 'DHCP Leases', sources.dhcp),
self.sourceChip(K, 'P2P Mesh', sources.p2p)
]),
stats
K.E('div', { 'style': 'font-size: 13px; color: var(--kiss-muted); margin-bottom: 8px;' }, 'Emulators'),
K.E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px;' }, [
self.sourceChip(K, 'USB', emus.usb),
self.sourceChip(K, 'MQTT', emus.mqtt),
self.sourceChip(K, 'Zigbee', emus.zigbee)
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Data Sources')),
sourceBar,
E('h4', { 'style': 'margin-top:0.75em;' }, _('Emulators')),
emuBar
// Device Types Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['🏷️ ', 'Device Types']),
K.E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 12px;' },
Object.keys(typeCounts).sort(function(a, b) {
return typeCounts[b] - typeCounts[a];
}).map(function(tid) {
var info = typeMap[tid] || { name: tid, color: '#6c757d' };
return K.E('div', {
'style': 'background: var(--kiss-bg2, #111827); border: 1px solid ' + (info.color || '#6c757d') + '; border-radius: 8px; padding: 12px 16px; text-align: center; min-width: 80px;'
}, [
K.E('div', { 'style': 'font-size: 24px; font-weight: bold; color: ' + (info.color || '#6c757d') }, String(typeCounts[tid])),
K.E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted); margin-top: 4px;' }, info.name || tid)
]);
})
)
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Device Types')),
typeGrid
// Zones Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['🗺️ ', 'Zones']),
K.E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px;' },
Object.keys(zoneCounts).map(function(z) {
return K.E('span', {
'style': 'background: var(--kiss-purple, #6366f1); color: #fff; padding: 6px 12px; border-radius: 6px; font-size: 13px;'
}, z + ' (' + zoneCounts[z] + ')');
})
)
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Zones')),
E('div', { 'class': 'di-source-bar' }, zoneItems)
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Recent Devices')),
recentTable
// Recent Devices Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['🕐 ', 'Recent Devices']),
K.E('table', { 'class': 'kiss-table' }, [
K.E('thead', {}, K.E('tr', {}, [
K.E('th', {}, 'Device'),
K.E('th', {}, 'IP'),
K.E('th', {}, 'Type'),
K.E('th', {}, 'Vendor')
])),
K.E('tbody', {},
recent.length > 0
? recent.map(function(d) {
return K.E('tr', {}, [
K.E('td', {}, [
K.E('span', {
'style': 'display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; background: ' + (d.online ? 'var(--kiss-green, #22c55e)' : 'var(--kiss-muted, #64748b)') + ';'
}),
d.label || d.hostname || d.mac
]),
K.E('td', {}, d.ip || '-'),
K.E('td', {}, d.device_type || '-'),
K.E('td', {}, d.vendor || '-')
]);
})
: [K.E('tr', {}, K.E('td', { 'colspan': '4', 'style': 'text-align: center; color: var(--kiss-muted); padding: 20px;' }, 'No recent devices'))]
)
])
])
];
]);
return KissTheme.wrap(content, 'admin/secubox/services/device-intel');
},
statCard: function(value, label, color) {
return E('div', { 'class': 'di-stat-card' }, [
E('div', { 'class': 'value', 'style': 'color:' + color }, String(value)),
E('div', { 'class': 'label' }, label)
statCard: function(K, value, label, color, icon) {
return K.E('div', {
'style': 'background: var(--kiss-card, #161e2e); border: 1px solid var(--kiss-line, #1e293b); border-radius: 12px; padding: 20px; text-align: center;'
}, [
K.E('div', { 'style': 'font-size: 20px; margin-bottom: 8px;' }, icon),
K.E('div', { 'style': 'font-size: 32px; font-weight: bold; color: ' + color }, String(value)),
K.E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted); margin-top: 4px;' }, label)
]);
},
sourceChip: function(name, active) {
return E('span', {
'class': 'di-source-chip ' + (active ? 'active' : 'inactive')
sourceChip: function(K, name, active) {
return K.E('span', {
'style': 'padding: 6px 12px; border-radius: 6px; font-size: 13px; ' +
(active
? 'background: var(--kiss-green, #22c55e); color: #000;'
: 'background: var(--kiss-bg2, #111827); color: var(--kiss-muted); border: 1px solid var(--kiss-line, #1e293b);')
}, name);
},

View File

@ -5,6 +5,11 @@
'require device-intel/api as api';
'require secubox/kiss-theme';
/**
* Device Intel - Devices List - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({
load: function() {
return Promise.all([
@ -19,84 +24,95 @@ return view.extend({
var devices = devResult.devices || [];
var types = typesResult.types || [];
var self = this;
var cssLink = E('link', {
rel: 'stylesheet',
href: L.resource('device-intel/common.css')
});
var K = KissTheme;
// Build type lookup
var typeMap = {};
types.forEach(function(t) { typeMap[t.id] = t; });
var inputStyle = 'padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px;';
// ── Filter Bar ──
var filterInput, typeFilter, statusFilter;
var filterBar = E('div', { 'class': 'di-filter-bar' }, [
filterInput = E('input', {
var filterBar = K.E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; align-items: center;' }, [
filterInput = K.E('input', {
'type': 'text',
'placeholder': _('Search MAC, hostname, IP, vendor...'),
'style': 'width:250px;',
'placeholder': 'Search MAC, hostname, IP, vendor...',
'style': inputStyle + ' width: 280px;',
'input': function() { self.applyFilters(devices, typeMap); }
}),
typeFilter = E('select', {
typeFilter = K.E('select', {
'style': inputStyle,
'change': function() { self.applyFilters(devices, typeMap); }
}, [
E('option', { value: '' }, _('All Types'))
K.E('option', { value: '' }, 'All Types')
].concat(types.map(function(t) {
return E('option', { value: t.id }, t.name);
return K.E('option', { value: t.id }, t.name);
})).concat([
E('option', { value: 'unknown' }, _('Unknown'))
K.E('option', { value: 'unknown' }, 'Unknown')
])),
statusFilter = E('select', {
statusFilter = K.E('select', {
'style': inputStyle,
'change': function() { self.applyFilters(devices, typeMap); }
}, [
E('option', { value: '' }, _('All Status')),
E('option', { value: 'online' }, _('Online')),
E('option', { value: 'offline' }, _('Offline'))
K.E('option', { value: '' }, 'All Status'),
K.E('option', { value: 'online' }, 'Online'),
K.E('option', { value: 'offline' }, 'Offline')
]),
E('button', {
'class': 'cbi-button',
K.E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'padding: 10px 16px;',
'click': function() {
api.refresh().then(function() {
window.location.href = window.location.pathname + '?' + Date.now();
});
}
}, _('Refresh'))
}, '🔄 Refresh')
]);
// Store filter elements for later
this._filterInput = filterInput;
this._typeFilter = typeFilter;
this._statusFilter = statusFilter;
this._typeMap = typeMap;
// ── Device Table ──
var tbody = E('tbody', { 'id': 'di-device-tbody' });
var tbody = K.E('tbody', { 'id': 'di-device-tbody' });
this.renderDeviceRows(tbody, devices, typeMap);
var table = E('table', { 'class': 'di-device-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, ''),
E('th', {}, _('Device')),
E('th', {}, _('MAC')),
E('th', {}, _('IP')),
E('th', {}, _('Vendor')),
E('th', {}, _('Type')),
E('th', {}, _('Zone')),
E('th', {}, _('Source')),
E('th', {}, _('Actions'))
var table = K.E('table', { 'class': 'kiss-table' }, [
K.E('thead', {}, K.E('tr', {}, [
K.E('th', { 'style': 'width: 40px;' }, ''),
K.E('th', {}, 'Device'),
K.E('th', {}, 'MAC'),
K.E('th', {}, 'IP'),
K.E('th', {}, 'Vendor'),
K.E('th', {}, 'Type'),
K.E('th', {}, 'Zone'),
K.E('th', {}, 'Source'),
K.E('th', { 'style': 'width: 140px;' }, 'Actions')
])),
tbody
]);
var content = [
cssLink,
E('h2', {}, _('Devices')),
E('div', { 'class': 'cbi-section' }, [
var content = K.E('div', {}, [
// Page Header
K.E('div', { 'style': 'margin-bottom: 20px;' }, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
K.E('span', {}, '📱'),
'Devices'
]),
K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
'Network devices discovered across all data sources')
]),
// Filter and Table Card
K.E('div', { 'class': 'kiss-card' }, [
filterBar,
table
])
];
]);
return KissTheme.wrap(content, 'admin/secubox/services/device-intel/devices');
},
@ -135,56 +151,55 @@ return view.extend({
renderDeviceRows: function(tbody, devices, typeMap) {
var self = this;
var K = KissTheme;
dom.content(tbody, devices.map(function(d) {
var typeInfo = typeMap[d.device_type] || {};
return E('tr', { 'data-mac': d.mac }, [
E('td', {}, E('span', {
'class': 'di-online-dot ' + (d.online ? 'online' : 'offline'),
'title': d.online ? _('Online') : _('Offline')
return K.E('tr', { 'data-mac': d.mac }, [
K.E('td', {}, K.E('span', {
'style': 'display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ' + (d.online ? 'var(--kiss-green, #22c55e)' : 'var(--kiss-muted, #64748b)') + ';',
'title': d.online ? 'Online' : 'Offline'
})),
E('td', {}, [
E('strong', {}, d.label || d.hostname || '-'),
K.E('td', {}, [
K.E('strong', {}, d.label || d.hostname || '-'),
d.emulator_source
? E('small', { 'style': 'display:block; color:#6c757d;' },
? K.E('small', { 'style': 'display: block; color: var(--kiss-muted); font-size: 11px;' },
d.emulator_source)
: null
].filter(Boolean)),
E('td', { 'style': 'font-family:monospace; font-size:0.85em;' }, d.mac || '-'),
E('td', {}, d.ip || '-'),
E('td', {}, (function() {
K.E('td', { 'style': 'font-family: monospace; font-size: 12px;' }, d.mac || '-'),
K.E('td', {}, d.ip || '-'),
K.E('td', {}, (function() {
var v = self.vendorDisplay(d);
return [v[0], v[1]].join('');
})()),
E('td', {}, [
K.E('td', {}, [
typeInfo.name
? E('span', {
'style': 'border-left:3px solid ' + (typeInfo.color || '#6c757d') +
'; padding-left:0.5em;'
? K.E('span', {
'style': 'border-left: 3px solid ' + (typeInfo.color || '#6c757d') + '; padding-left: 8px;'
}, typeInfo.name)
: E('span', { 'style': 'color:#6c757d;' }, d.device_type || '-')
: K.E('span', { 'style': 'color: var(--kiss-muted);' }, d.device_type || '-')
]),
E('td', {}, d.cg_zone || '-'),
E('td', {}, d.source_node || 'local'),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'padding:0.2em 0.5em; font-size:0.8em;',
K.E('td', {}, d.cg_zone || '-'),
K.E('td', {}, d.source_node || 'local'),
K.E('td', {}, [
K.E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'padding: 4px 10px; font-size: 12px; margin-right: 6px;',
'click': function() { self.handleEditDevice(d, typeMap); }
}, _('Edit')),
' ',
E('button', {
'class': 'cbi-button',
'style': 'padding:0.2em 0.5em; font-size:0.8em;',
}, 'Edit'),
K.E('button', {
'class': 'kiss-btn',
'style': 'padding: 4px 10px; font-size: 12px;',
'click': function() { self.handleShowDetail(d); }
}, _('Detail'))
}, 'Detail')
])
]);
}));
if (devices.length === 0) {
dom.content(tbody, E('tr', {},
E('td', { 'colspan': '9', 'style': 'text-align:center; color:#6c757d; padding:2em;' },
_('No devices found'))));
dom.content(tbody, K.E('tr', {},
K.E('td', { 'colspan': '9', 'style': 'text-align: center; color: var(--kiss-muted); padding: 40px;' },
'No devices found')));
}
},
@ -215,26 +230,23 @@ return view.extend({
handleEditDevice: function(device, typeMap) {
var typeSelect, labelInput;
var types = Object.keys(typeMap);
var inputStyle = 'width: 100%; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--kiss-line, #1e293b); background: var(--kiss-bg2, #111827); color: var(--kiss-text, #e2e8f0); font-size: 14px; margin-top: 6px;';
ui.showModal(_('Edit Device: ') + (device.label || device.hostname || device.mac), [
E('div', { 'style': 'display:flex; flex-direction:column; gap:0.8em;' }, [
E('label', {}, [
E('strong', {}, _('Label')),
ui.showModal('✏️ Edit Device: ' + (device.label || device.hostname || device.mac), [
E('div', { 'style': 'display: flex; flex-direction: column; gap: 16px;' }, [
E('div', {}, [
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Label'),
labelInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'value': device.label || '',
'placeholder': device.hostname || device.mac,
'style': 'margin-left:0.5em;'
'style': inputStyle
})
]),
E('label', {}, [
E('strong', {}, _('Device Type')),
typeSelect = E('select', {
'class': 'cbi-input-select',
'style': 'margin-left:0.5em;'
}, [
E('option', { value: '' }, _('-- Auto --'))
E('div', {}, [
E('label', { 'style': 'font-size: 12px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Device Type'),
typeSelect = E('select', { 'style': inputStyle }, [
E('option', { value: '' }, '-- Auto --')
].concat(types.map(function(tid) {
var opt = E('option', { value: tid }, typeMap[tid].name);
if (device.device_type === tid) opt.selected = true;
@ -242,11 +254,15 @@ return view.extend({
})))
])
]),
E('div', { 'class': 'right', 'style': 'margin-top:1em;' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
' ',
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'class': 'kiss-btn',
'style': 'padding: 10px 20px;',
'click': ui.hideModal
}, 'Cancel'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'style': 'padding: 10px 20px;',
'click': function() {
var newType = typeSelect.value;
var newLabel = labelInput.value.trim();
@ -254,15 +270,15 @@ return view.extend({
ui.hideModal();
api.setDeviceMeta(device.mac, newType, newLabel).then(function(res) {
if (res && res.success) {
ui.addNotification(null, E('p', {}, _('Device updated.')), 'info');
ui.addNotification(null, E('p', {}, 'Device updated.'), 'info');
window.location.href = window.location.pathname + '?' + Date.now();
} else {
ui.addNotification(null, E('p', {},
_('Update failed: ') + (res ? res.error : '')), 'danger');
'Update failed: ' + (res ? res.error : '')), 'danger');
}
});
}
}, _('Save'))
}, '💾 Save')
])
]);
},
@ -274,7 +290,7 @@ return view.extend({
['Hostname', device.hostname || '-'],
['Label', device.label || '-'],
['Vendor', device.vendor || '-'],
['Online', device.online ? 'Yes' : 'No'],
['Online', device.online ? '🟢 Yes' : 'No'],
['Connection', device.connection_type || '-'],
['Interface', device.iface || '-'],
['Randomized MAC', device.randomized ? 'Yes' : 'No'],
@ -290,17 +306,21 @@ return view.extend({
['Source Node', device.source_node || 'local']
];
ui.showModal(_('Device Detail: ') + (device.label || device.hostname || device.mac), [
E('table', { 'style': 'width:100%;' },
ui.showModal('🔍 Device Detail: ' + (device.label || device.hostname || device.mac), [
E('table', { 'style': 'width: 100%; border-collapse: collapse;' },
rows.map(function(r) {
return E('tr', {}, [
E('td', { 'style': 'font-weight:bold; padding:0.3em 1em 0.3em 0;' }, r[0]),
E('td', { 'style': 'padding:0.3em 0;' }, r[1])
E('td', { 'style': 'font-weight: bold; padding: 8px 16px 8px 0; color: var(--kiss-muted); border-bottom: 1px solid var(--kiss-line, #1e293b);' }, r[0]),
E('td', { 'style': 'padding: 8px 0; border-bottom: 1px solid var(--kiss-line, #1e293b);' }, r[1])
]);
})
),
E('div', { 'class': 'right', 'style': 'margin-top:1em;' },
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close')))
E('div', { 'style': 'text-align: right; margin-top: 20px;' },
E('button', {
'class': 'kiss-btn',
'style': 'padding: 10px 20px;',
'click': ui.hideModal
}, 'Close'))
]);
},

View File

@ -5,6 +5,11 @@
'require device-intel/api as api';
'require secubox/kiss-theme';
/**
* Device Intel - Emulator Modules - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({
load: function() {
return Promise.all([
@ -14,118 +19,119 @@ return view.extend({
},
render: function(data) {
var self = this;
var K = KissTheme;
var emuResult = data[0] || {};
var devResult = data[1] || {};
var devices = devResult.devices || [];
var cssLink = E('link', {
rel: 'stylesheet',
href: L.resource('device-intel/common.css')
});
// Filter devices by emulator source
var usbDevices = devices.filter(function(d) { return d.emulator_source === 'usb'; });
var mqttDevices = devices.filter(function(d) { return d.emulator_source === 'mqtt'; });
var zigbeeDevices = devices.filter(function(d) { return d.emulator_source === 'zigbee'; });
// ── USB Card ──
var usb = emuResult.usb || {};
var usbCard = this.emuCard('USB Peripherals', usb.enabled, [
E('div', { 'style': 'display:flex; gap:2em; margin-bottom:0.75em;' }, [
E('span', {}, _('System USB devices: ') + E('strong', {}, String(usb.device_count || 0)).textContent),
E('span', {}, _('Discovered: ') + E('strong', {}, String(usbDevices.length)).textContent)
]),
this.deviceMiniTable(usbDevices, ['hostname', 'vendor', 'device_type'])
]);
// ── MQTT Card ──
var mqtt = emuResult.mqtt || {};
var mqttCard = this.emuCard('MQTT Broker', mqtt.enabled, [
E('div', { 'style': 'display:flex; gap:2em; margin-bottom:0.75em; flex-wrap:wrap;' }, [
E('span', {}, _('Broker: ') + (mqtt.broker_host || '127.0.0.1') + ':' + (mqtt.broker_port || 1883)),
E('span', {}, [
_('Status: '),
mqtt.broker_running
? E('span', { 'style': 'color:#22c55e;' }, _('Running'))
: E('span', { 'style': 'color:#ef4444;' }, _('Not Found'))
]),
E('span', {}, _('Clients discovered: ') + E('strong', {}, String(mqttDevices.length)).textContent)
]),
this.deviceMiniTable(mqttDevices, ['hostname', 'vendor', 'device_type'])
]);
// ── Zigbee Card ──
var zigbee = emuResult.zigbee || {};
var zigbeeCard = this.emuCard('Zigbee Coordinator', zigbee.enabled, [
E('div', { 'style': 'display:flex; gap:2em; margin-bottom:0.75em; flex-wrap:wrap;' }, [
E('span', {}, _('Adapter: ') + (zigbee.adapter || 'zigbee2mqtt')),
E('span', {}, _('Dongle: ') + (zigbee.coordinator || '/dev/ttyUSB0')),
E('span', {}, [
_('Dongle: '),
zigbee.dongle_present
? E('span', { 'style': 'color:#22c55e;' }, _('Present'))
: E('span', { 'style': 'color:#ef4444;' }, _('Not Found'))
]),
E('span', {}, _('Paired devices: ') + E('strong', {}, String(zigbeeDevices.length)).textContent)
]),
this.deviceMiniTable(zigbeeDevices, ['hostname', 'vendor', 'device_type'])
]);
var content = E('div', {}, [
cssLink,
E('h2', {}, _('Emulator Modules')),
E('p', { 'style': 'color:#6c757d; margin-bottom:1em;' },
_('Pluggable device discovery modules for USB, MQTT, and Zigbee peripherals.')),
usbCard,
mqttCard,
zigbeeCard,
E('div', { 'style': 'text-align:right; margin-top:1em;' },
E('a', {
var content = K.E('div', {}, [
// Page Header
K.E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;' }, [
K.E('div', {}, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
K.E('span', {}, '🔌'),
'Emulator Modules'
]),
K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
'Pluggable device discovery modules for USB, MQTT, and Zigbee peripherals')
]),
K.E('a', {
'href': L.url('admin/secubox/device-intel/settings'),
'class': 'cbi-button cbi-button-neutral'
}, _('Configure Emulators')))
'class': 'kiss-btn kiss-btn-blue',
'style': 'padding: 10px 16px; text-decoration: none;'
}, '⚙️ Configure')
]),
// USB Card
self.emuCard(K, '🔌 USB Peripherals', usb.enabled, [
K.E('div', { 'style': 'display: flex; gap: 24px; margin-bottom: 16px; flex-wrap: wrap;' }, [
K.E('span', {}, ['System USB devices: ', K.E('strong', {}, String(usb.device_count || 0))]),
K.E('span', {}, ['Discovered: ', K.E('strong', { 'style': 'color: var(--kiss-green);' }, String(usbDevices.length))])
]),
self.deviceMiniTable(K, usbDevices, ['hostname', 'vendor', 'device_type'])
]),
// MQTT Card
self.emuCard(K, '📡 MQTT Broker', mqtt.enabled, [
K.E('div', { 'style': 'display: flex; gap: 24px; margin-bottom: 16px; flex-wrap: wrap;' }, [
K.E('span', {}, 'Broker: ' + (mqtt.broker_host || '127.0.0.1') + ':' + (mqtt.broker_port || 1883)),
K.E('span', {}, [
'Status: ',
mqtt.broker_running
? K.E('span', { 'style': 'color: var(--kiss-green, #22c55e);' }, '● Running')
: K.E('span', { 'style': 'color: var(--kiss-red, #ef4444);' }, '● Not Found')
]),
K.E('span', {}, ['Clients discovered: ', K.E('strong', { 'style': 'color: var(--kiss-green);' }, String(mqttDevices.length))])
]),
self.deviceMiniTable(K, mqttDevices, ['hostname', 'vendor', 'device_type'])
]),
// Zigbee Card
self.emuCard(K, '📻 Zigbee Coordinator', zigbee.enabled, [
K.E('div', { 'style': 'display: flex; gap: 24px; margin-bottom: 16px; flex-wrap: wrap;' }, [
K.E('span', {}, 'Adapter: ' + (zigbee.adapter || 'zigbee2mqtt')),
K.E('span', {}, 'Dongle: ' + (zigbee.coordinator || '/dev/ttyUSB0')),
K.E('span', {}, [
'Status: ',
zigbee.dongle_present
? K.E('span', { 'style': 'color: var(--kiss-green, #22c55e);' }, '● Present')
: K.E('span', { 'style': 'color: var(--kiss-red, #ef4444);' }, '● Not Found')
]),
K.E('span', {}, ['Paired devices: ', K.E('strong', { 'style': 'color: var(--kiss-green);' }, String(zigbeeDevices.length))])
]),
self.deviceMiniTable(K, zigbeeDevices, ['hostname', 'vendor', 'device_type'])
])
]);
return KissTheme.wrap(content, 'admin/secubox/services/device-intel/emulators');
},
emuCard: function(title, enabled, content) {
return E('div', { 'class': 'di-emu-card' }, [
E('h4', {}, [
title,
E('span', {
'class': 'status ' + (enabled ? 'enabled' : 'disabled'),
'style': 'margin-left:0.75em;'
}, enabled ? _('Enabled') : _('Disabled'))
emuCard: function(K, title, enabled, contentItems) {
return K.E('div', { 'class': 'kiss-card', 'style': 'margin-bottom: 16px;' }, [
K.E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;' }, [
K.E('div', { 'class': 'kiss-card-title', 'style': 'margin: 0;' }, title),
enabled
? K.E('span', { 'style': 'background: var(--kiss-green, #22c55e); color: #000; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: bold;' }, 'Enabled')
: K.E('span', { 'style': 'background: var(--kiss-muted, #64748b); color: #fff; padding: 4px 12px; border-radius: 6px; font-size: 12px;' }, 'Disabled')
])
].concat(content));
].concat(contentItems));
},
deviceMiniTable: function(devices, fields) {
deviceMiniTable: function(K, devices, fields) {
if (devices.length === 0) {
return E('p', { 'style': 'color:#6c757d; font-style:italic;' },
_('No devices discovered from this emulator.'));
return K.E('p', { 'style': 'color: var(--kiss-muted); font-style: italic; text-align: center; padding: 20px;' },
'No devices discovered from this emulator.');
}
var headers = {
'hostname': _('Name'),
'vendor': _('Model/Vendor'),
'device_type': _('Type'),
'mac': _('ID'),
'ip': _('IP')
'hostname': 'Name',
'vendor': 'Model/Vendor',
'device_type': 'Type',
'mac': 'ID',
'ip': 'IP'
};
return E('table', { 'class': 'di-device-table' }, [
E('thead', {}, E('tr', {},
return K.E('table', { 'class': 'kiss-table' }, [
K.E('thead', {}, K.E('tr', {},
fields.map(function(f) {
return E('th', {}, headers[f] || f);
return K.E('th', {}, headers[f] || f);
})
)),
E('tbody', {},
K.E('tbody', {},
devices.slice(0, 20).map(function(d) {
return E('tr', {},
return K.E('tr', {},
fields.map(function(f) {
return E('td', {}, String(d[f] || '-'));
return K.E('td', {}, String(d[f] || '-'));
})
);
})

View File

@ -5,6 +5,11 @@
'require device-intel/api as api';
'require secubox/kiss-theme';
/**
* Device Intel - Mesh Network - KISS Style
* Copyright (C) 2025 CyberMind.fr
*/
return view.extend({
load: function() {
return Promise.all([
@ -14,15 +19,10 @@ return view.extend({
},
render: function(data) {
var K = KissTheme;
var meshResult = data[0] || {};
var summary = data[1] || {};
var meshDevices = meshResult.devices || [];
var cssLink = E('link', {
rel: 'stylesheet',
href: L.resource('device-intel/common.css')
});
// Separate mesh peers from remote devices
var peers = meshDevices.filter(function(d) { return d.device_type === 'mesh_peer'; });
var remoteDevices = meshDevices.filter(function(d) { return d.device_type !== 'mesh_peer'; });
@ -30,26 +30,29 @@ return view.extend({
// ── Peer Cards ──
var peerCards;
if (peers.length > 0) {
peerCards = E('div', { 'class': 'di-stats' },
peerCards = K.E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'gap: 16px;' },
peers.map(function(p) {
return E('div', { 'class': 'di-stat-card' }, [
E('div', { 'style': 'display:flex; align-items:center; gap:0.5em; margin-bottom:0.5em;' }, [
E('span', {
'class': 'di-online-dot ' + (p.online ? 'online' : 'offline')
return K.E('div', {
'style': 'background: var(--kiss-bg2, #111827); border: 1px solid var(--kiss-line, #1e293b); border-radius: 12px; padding: 16px;'
}, [
K.E('div', { 'style': 'display: flex; align-items: center; gap: 10px; margin-bottom: 8px;' }, [
K.E('span', {
'style': 'display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ' + (p.online ? 'var(--kiss-green, #22c55e)' : 'var(--kiss-muted, #64748b)') + ';'
}),
E('strong', {}, p.hostname || p.mac)
K.E('strong', { 'style': 'font-size: 14px;' }, p.hostname || p.mac)
]),
E('div', { 'style': 'font-size:0.85em; color:#6c757d;' }, p.ip || '-')
K.E('div', { 'style': 'font-size: 13px; color: var(--kiss-muted);' }, p.ip || '-')
]);
})
);
} else {
peerCards = E('div', {
'style': 'text-align:center; padding:2em; color:#6c757d;'
peerCards = K.E('div', {
'style': 'text-align: center; padding: 40px 20px; color: var(--kiss-muted);'
}, [
E('p', {}, _('No mesh peers discovered.')),
E('p', { 'style': 'font-size:0.9em;' },
_('Ensure SecuBox P2P is running and peers are configured.'))
K.E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '🔗'),
K.E('p', { 'style': 'margin: 0;' }, 'No mesh peers discovered.'),
K.E('p', { 'style': 'font-size: 13px; margin-top: 8px;' },
'Ensure SecuBox P2P is running and peers are configured.')
]);
}
@ -57,48 +60,59 @@ return view.extend({
var remoteTable;
if (remoteDevices.length > 0) {
var rows = remoteDevices.map(function(d) {
return E('tr', {}, [
E('td', {}, [
E('span', { 'class': 'di-online-dot ' + (d.online ? 'online' : 'offline') }),
return K.E('tr', {}, [
K.E('td', {}, [
K.E('span', {
'style': 'display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; background: ' + (d.online ? 'var(--kiss-green, #22c55e)' : 'var(--kiss-muted, #64748b)') + ';'
}),
d.label || d.hostname || d.mac
]),
E('td', {}, d.ip || '-'),
E('td', {}, d.device_type || '-'),
E('td', {}, d.source_node || '-')
K.E('td', {}, d.ip || '-'),
K.E('td', {}, d.device_type || '-'),
K.E('td', {}, d.source_node || '-')
]);
});
remoteTable = E('table', { 'class': 'di-device-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('Device')),
E('th', {}, _('IP')),
E('th', {}, _('Type')),
E('th', {}, _('Source Node'))
remoteTable = K.E('table', { 'class': 'kiss-table' }, [
K.E('thead', {}, K.E('tr', {}, [
K.E('th', {}, 'Device'),
K.E('th', {}, 'IP'),
K.E('th', {}, 'Type'),
K.E('th', {}, 'Source Node')
])),
E('tbody', {}, rows)
K.E('tbody', {}, rows)
]);
} else {
remoteTable = E('p', { 'style': 'color:#6c757d; font-style:italic;' },
_('No remote devices available. Peer device inventory sharing is not yet active.'));
remoteTable = K.E('p', { 'style': 'color: var(--kiss-muted); font-style: italic; text-align: center; padding: 20px;' },
'No remote devices available. Peer device inventory sharing is not yet active.');
}
var content = E('div', {}, [
cssLink,
E('h2', {}, _('Mesh Network')),
var content = K.E('div', {}, [
// Page Header
K.E('div', { 'style': 'margin-bottom: 20px;' }, [
K.E('h2', { 'style': 'margin: 0; font-size: 24px; display: flex; align-items: center; gap: 10px;' }, [
K.E('span', {}, '🌐'),
'Mesh Network'
]),
K.E('p', { 'style': 'margin: 4px 0 0; color: var(--kiss-muted, #94a3b8); font-size: 14px;' },
'P2P mesh peers and shared device inventory')
]),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center;' }, [
E('h3', { 'style': 'margin:0;' }, _('Peers')),
E('span', { 'style': 'color:#6c757d;' },
String(peers.length) + _(' peer(s) discovered'))
// Peers Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;' }, [
K.E('div', { 'class': 'kiss-card-title', 'style': 'margin: 0;' }, ['🔗 ', 'Peers']),
K.E('span', { 'style': 'color: var(--kiss-muted); font-size: 13px;' },
String(peers.length) + ' peer(s) discovered')
]),
peerCards
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Remote Devices')),
E('p', { 'style': 'color:#6c757d; margin-bottom:1em; font-size:0.9em;' },
_('Devices reported by mesh peers. Requires device-intel on remote nodes.')),
// Remote Devices Card
K.E('div', { 'class': 'kiss-card' }, [
K.E('div', { 'class': 'kiss-card-title' }, ['📡 ', 'Remote Devices']),
K.E('p', { 'style': 'color: var(--kiss-muted); margin-bottom: 16px; font-size: 13px;' },
'Devices reported by mesh peers. Requires device-intel on remote nodes.'),
remoteTable
])
]);