From 62f89f0d573a4bf624f771c3bbc2736f7e250ce2 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 15 Mar 2026 17:37:53 +0100 Subject: [PATCH] fix(luci): Regenerate MAC Guardian dashboard with dark theme - Remove KissTheme dependency, use standard LuCI with inline styles - Dark theme matching SecuBox palette (#12121a, #1a1a24, #00d4aa, #00a0ff) - Simplified view with direct DOM rendering instead of form.Map - Cards grid layout for status, WiFi interfaces, DHCP, config - Inline Trust/Block action buttons with proper styling - Responsive tables for clients and alerts Co-Authored-By: Claude Opus 4.5 --- .../resources/view/mac-guardian/dashboard.js | 595 ++++++++---------- 1 file changed, 250 insertions(+), 345 deletions(-) diff --git a/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js b/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js index c182ec70..d1fd5755 100644 --- a/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js +++ b/package/secubox/luci-app-mac-guardian/htdocs/luci-static/resources/view/mac-guardian/dashboard.js @@ -1,389 +1,294 @@ 'use strict'; 'require view'; -'require form'; -'require uci'; +'require dom'; 'require rpc'; 'require ui'; -'require secubox/kiss-theme'; +'require uci'; var callStatus = rpc.declare({ object: 'luci.mac-guardian', method: 'status', - expect: { '': {} } + expect: {} }); var callGetClients = rpc.declare({ object: 'luci.mac-guardian', method: 'get_clients', - expect: { '': {} } + expect: {} }); var callGetEvents = rpc.declare({ object: 'luci.mac-guardian', method: 'get_events', params: ['count'], - expect: { '': {} } + expect: {} }); -var callScan = rpc.declare({ - object: 'luci.mac-guardian', - method: 'scan' -}); - -var callStart = rpc.declare({ - object: 'luci.mac-guardian', - method: 'start' -}); - -var callStop = rpc.declare({ - object: 'luci.mac-guardian', - method: 'stop' -}); - -var callRestart = rpc.declare({ - object: 'luci.mac-guardian', - method: 'restart' -}); - -var callTrust = rpc.declare({ - object: 'luci.mac-guardian', - method: 'trust', - params: ['mac'] -}); - -var callBlock = rpc.declare({ - object: 'luci.mac-guardian', - method: 'block', - params: ['mac'] -}); - -var callDhcpStatus = rpc.declare({ - object: 'luci.mac-guardian', - method: 'dhcp_status', - expect: { '': {} } -}); - -var callDhcpCleanup = rpc.declare({ - object: 'luci.mac-guardian', - method: 'dhcp_cleanup', - expect: { '': {} } -}); +var callScan = rpc.declare({ object: 'luci.mac-guardian', method: 'scan' }); +var callStart = rpc.declare({ object: 'luci.mac-guardian', method: 'start' }); +var callStop = rpc.declare({ object: 'luci.mac-guardian', method: 'stop' }); +var callTrust = rpc.declare({ object: 'luci.mac-guardian', method: 'trust', params: ['mac'] }); +var callBlock = rpc.declare({ object: 'luci.mac-guardian', method: 'block', params: ['mac'] }); +var callDhcpStatus = rpc.declare({ object: 'luci.mac-guardian', method: 'dhcp_status', expect: {} }); +var callDhcpCleanup = rpc.declare({ object: 'luci.mac-guardian', method: 'dhcp_cleanup', expect: {} }); function formatDate(ts) { if (!ts || ts === 0) return '-'; var d = new Date(ts * 1000); - var pad = function(n) { return n < 10 ? '0' + n : n; }; - return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + - ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + return d.toLocaleString(); +} + +function createCard(title, icon, content, borderColor) { + return E('div', { + 'style': 'background:#12121a;border-radius:8px;padding:16px;margin-bottom:16px;' + + 'border-left:4px solid ' + (borderColor || '#2a2a3a') + ';' + }, [ + E('div', { 'style': 'display:flex;align-items:center;gap:8px;margin-bottom:12px;' }, [ + E('span', { 'style': 'font-size:1.2rem;' }, icon), + E('span', { 'style': 'font-size:1rem;font-weight:600;color:#fff;' }, title) + ]), + E('div', {}, content) + ]); +} + +function createMetric(label, value, color) { + return E('div', { + 'style': 'background:#1a1a24;padding:10px 16px;border-radius:6px;text-align:center;min-width:70px;' + }, [ + E('div', { + 'style': 'font-size:1.4rem;font-weight:700;color:' + (color || '#00d4aa') + ';font-family:monospace;' + }, String(value)), + E('div', { + 'style': 'font-size:0.7rem;color:#808090;text-transform:uppercase;margin-top:2px;' + }, label) + ]); +} + +function createBtn(text, style, onclick) { + var colors = { + primary: 'background:rgba(0,160,255,0.2);color:#00a0ff;border-color:rgba(0,160,255,0.3);', + success: 'background:rgba(0,212,170,0.2);color:#00d4aa;border-color:rgba(0,212,170,0.3);', + danger: 'background:rgba(255,77,77,0.2);color:#ff4d4d;border-color:rgba(255,77,77,0.3);', + warning: 'background:rgba(255,165,0,0.2);color:#ffa500;border-color:rgba(255,165,0,0.3);' + }; + return E('button', { + 'style': 'padding:6px 14px;border:1px solid;border-radius:6px;font-size:13px;cursor:pointer;' + + 'margin-right:8px;' + (colors[style] || colors.primary), + 'click': onclick + }, text); } function statusBadge(status) { var colors = { - 'trusted': '#080', - 'suspect': '#c60', - 'blocked': '#c00', - 'unknown': '#888' + 'trusted': '#00d4aa', + 'suspect': '#ffa500', + 'blocked': '#ff4d4d', + 'unknown': '#808090' }; - var color = colors[status] || '#888'; - return '' + - status + ''; + var color = colors[status] || '#808090'; + return E('span', { + 'style': 'display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;' + + 'background:' + color + '22;color:' + color + ';' + }, status); } return view.extend({ load: function() { return Promise.all([ - uci.load('mac-guardian'), - callStatus(), - callGetClients(), - callGetEvents(10), - callDhcpStatus() + callStatus().catch(function() { return {}; }), + callGetClients().catch(function() { return { clients: [] }; }), + callGetEvents(15).catch(function() { return { events: [] }; }), + callDhcpStatus().catch(function() { return {}; }) ]); }, render: function(data) { - var status = data[1]; - var clientData = data[2]; - var eventData = data[3]; - var dhcpStatus = data[4] || {}; - var clients = (clientData && clientData.clients) ? clientData.clients : []; - var events = (eventData && eventData.events) ? eventData.events : []; - var m, s, o; + var status = data[0] || {}; + var clientData = data[1] || {}; + var eventData = data[2] || {}; + var dhcpStatus = data[3] || {}; + var clients = clientData.clients || []; + var events = eventData.events || []; + var cl = status.clients || {}; + var ifaces = status.interfaces || []; + var running = status.service_status === 'running'; - m = new form.Map('mac-guardian', _('MAC Guardian'), - _('WiFi MAC address security monitor. Detects randomized MACs, spoofing, and MAC floods.')); + var view = E('div', { 'style': 'max-width:1200px;margin:0 auto;padding:20px;' }, [ + // Header + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;' }, [ + E('h2', { + 'style': 'margin:0;font-size:1.5rem;background:linear-gradient(90deg,#00d4aa,#00a0ff);' + + '-webkit-background-clip:text;-webkit-text-fill-color:transparent;' + }, 'MAC Guardian'), + E('div', { 'style': 'display:flex;align-items:center;gap:8px;' }, [ + E('span', { + 'style': 'width:10px;height:10px;border-radius:50%;background:' + + (running ? '#00d4aa' : '#ff4d4d') + ';box-shadow:0 0 8px ' + + (running ? '#00d4aa' : '#ff4d4d') + ';' + }), + E('span', { 'style': 'color:' + (running ? '#00d4aa' : '#ff4d4d') + ';font-weight:600;' }, + running ? 'RUNNING' : 'STOPPED') + ]) + ]), - // ========================================== - // Status Section - // ========================================== - s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Status')); - s.anonymous = true; - - o = s.option(form.DummyValue, '_status'); - o.rawhtml = true; - o.cfgvalue = function() { - var svcColor = status.service_status === 'running' ? '#080' : '#c00'; - var svcLabel = status.service_status === 'running' ? 'Running' : 'Stopped'; - - var html = '
'; - - // Service card - html += '
'; - html += '

Service

'; - html += '

Status: ' + svcLabel + '

'; - html += '

Policy: ' + (status.policy || 'alert') + '

'; - html += '

Interval: ' + (status.scan_interval || 30) + 's

'; - html += '
'; - - // Clients card - var cl = status.clients || {}; - html += '
'; - html += '

Clients

'; - html += '

Total: ' + (cl.total || 0) + '

'; - html += '

Trusted: ' + (cl.trusted || 0) + '

'; - html += '

Suspect: ' + (cl.suspect || 0) + '

'; - html += '

Blocked: ' + (cl.blocked || 0) + '

'; - html += '
'; - - // Interfaces card - var ifaces = status.interfaces || []; - html += '
'; - html += '

WiFi Interfaces

'; - if (ifaces.length === 0) { - html += '

None detected

'; - } else { - for (var i = 0; i < ifaces.length; i++) { - html += '

' + ifaces[i].name + ' (' + ifaces[i].essid + ') - ' + ifaces[i].stations + ' STA

'; - } - } - html += '
'; - - // DHCP Protection card - var dhcpColor = dhcpStatus.enabled ? '#080' : '#888'; - var dhcpLabel = dhcpStatus.enabled ? 'Enabled' : 'Disabled'; - html += '
'; - html += '

DHCP Protection

'; - html += '

Status: ' + dhcpLabel + '

'; - html += '

Leases: ' + (dhcpStatus.leases || 0) + '

'; - html += '

Conflicts: ' + (dhcpStatus.conflicts || 0) + '

'; - html += '

Stale: ' + (dhcpStatus.stale || 0) + '

'; - html += '
'; - - html += '
'; - return html; - }; - - // Control buttons - o = s.option(form.Button, '_start', _('Start')); - o.inputtitle = _('Start'); - o.inputstyle = 'apply'; - o.onclick = function() { - return callStart().then(function() { window.location.reload(); }); - }; - - o = s.option(form.Button, '_stop', _('Stop')); - o.inputtitle = _('Stop'); - o.inputstyle = 'remove'; - o.onclick = function() { - return callStop().then(function() { window.location.reload(); }); - }; - - o = s.option(form.Button, '_scan', _('Scan Now')); - o.inputtitle = _('Scan'); - o.inputstyle = 'reload'; - o.onclick = function() { - ui.showModal(_('Scanning'), [ - E('p', { 'class': 'spinning' }, _('Scanning WiFi interfaces...')) - ]); - return callScan().then(function() { - ui.hideModal(); - window.location.reload(); - }); - }; - - o = s.option(form.Button, '_dhcp_cleanup', _('DHCP Cleanup')); - o.inputtitle = _('Clean Up'); - o.inputstyle = 'reload'; - o.onclick = function() { - ui.showModal(_('Cleaning'), [ - E('p', { 'class': 'spinning' }, _('Running DHCP lease maintenance...')) - ]); - return callDhcpCleanup().then(function() { - ui.hideModal(); - window.location.reload(); - }); - }; - - // ========================================== - // Clients Table - // ========================================== - s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Known Clients')); - s.anonymous = true; - - o = s.option(form.DummyValue, '_clients'); - o.rawhtml = true; - o.cfgvalue = function() { - if (clients.length === 0) { - return '

No clients detected yet. Run a scan or wait for devices to connect.

'; - } - - var html = '
'; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - - for (var i = 0; i < clients.length; i++) { - var c = clients[i]; - var macDisplay = c.mac; - if (c.randomized) { - macDisplay += ' R'; - } - - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - } - - html += '
MACVendorHostnameInterfaceFirst SeenLast SeenStatusActions
' + macDisplay + '' + (c.vendor || '-') + '' + (c.hostname || '-') + '' + (c.iface || '-') + '' + formatDate(c.first_seen) + '' + formatDate(c.last_seen) + '' + statusBadge(c.status) + ''; - if (c.status !== 'trusted') { - html += ''; - } - if (c.status !== 'blocked') { - html += ''; - } - html += '
'; - html += '
'; - return html; - }; - - // ========================================== - // Recent Alerts - // ========================================== - s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Recent Alerts')); - s.anonymous = true; - - o = s.option(form.DummyValue, '_events'); - o.rawhtml = true; - o.cfgvalue = function() { - if (events.length === 0) { - return '

No alerts recorded.

'; - } - - var html = '
'; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - - for (var i = events.length - 1; i >= 0; i--) { - try { - var ev = JSON.parse(events[i]); - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - } catch(e) { - continue; - } - } - - html += '
TimeEventMACInterfaceDetails
' + (ev.ts || '-') + '' + (ev.event || '-') + '' + (ev.mac || '-') + '' + (ev.iface || '-') + '' + (ev.details || '-') + '
'; - html += '
'; - return html; - }; - - // ========================================== - // Configuration - // ========================================== - s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Configuration')); - s.anonymous = true; - - o = s.option(form.Flag, 'enabled', _('Enabled'), - _('Enable MAC Guardian service')); - o.rmempty = false; - - o = s.option(form.Value, 'scan_interval', _('Scan Interval'), - _('Seconds between WiFi scans')); - o.datatype = 'uinteger'; - o.default = '30'; - o.placeholder = '30'; - - s = m.section(form.NamedSection, 'detection', 'detection', _('Detection')); - s.anonymous = true; - - o = s.option(form.Flag, 'random_mac', _('Detect Randomized MACs'), - _('Alert on locally-administered (randomized) MAC addresses')); - o.default = '1'; - - o = s.option(form.Flag, 'spoof_detection', _('Detect Spoofing'), - _('Alert when a MAC address appears on a different interface')); - o.default = '1'; - - o = s.option(form.Flag, 'mac_flip', _('Detect MAC Floods'), - _('Alert when many new MACs appear in a short window')); - o.default = '1'; - - s = m.section(form.NamedSection, 'enforcement', 'enforcement', _('Enforcement')); - s.anonymous = true; - - o = s.option(form.ListValue, 'policy', _('Policy'), - _('Action to take on detected threats')); - o.value('alert', _('Alert only')); - o.value('quarantine', _('Quarantine (drop traffic)')); - o.value('deny', _('Deny (drop + deauthenticate)')); - o.default = 'alert'; - - // ========================================== - // Bind action buttons - // ========================================== - var rendered = m.render(); - - return rendered.then(function(node) { - node.addEventListener('click', function(ev) { - var btn = ev.target.closest('[data-action]'); - if (!btn) return; - - var mac = btn.getAttribute('data-mac'); - var action = btn.getAttribute('data-action'); - - if (action === 'trust') { - callTrust(mac).then(function() { - ui.addNotification(null, E('p', _('MAC %s trusted').format(mac)), 'success'); - window.location.reload(); + // Control buttons + E('div', { 'style': 'margin-bottom:20px;' }, [ + createBtn('Start', 'success', function() { + callStart().then(function() { window.location.reload(); }); + }), + createBtn('Stop', 'danger', function() { + callStop().then(function() { window.location.reload(); }); + }), + createBtn('Scan Now', 'primary', function() { + ui.showModal('Scanning', [E('p', { 'class': 'spinning' }, 'Scanning WiFi interfaces...')]); + callScan().then(function() { ui.hideModal(); window.location.reload(); }); + }), + createBtn('DHCP Cleanup', 'warning', function() { + callDhcpCleanup().then(function() { + ui.addNotification(null, E('p', 'DHCP cleanup completed'), 'info'); }); - } else if (action === 'block') { - if (confirm(_('Block and deauthenticate %s?').format(mac))) { - callBlock(mac).then(function() { - ui.addNotification(null, E('p', _('MAC %s blocked').format(mac)), 'success'); - window.location.reload(); - }); - } - } - }); + }) + ]), - return KissTheme.wrap([node], 'admin/secubox/mac-guardian/dashboard'); - }); - } + // Stats Grid + E('div', { + 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:20px;' + }, [ + // Clients Card + createCard('Clients', '👥', E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:10px;' }, [ + createMetric('Total', cl.total || 0, '#00a0ff'), + createMetric('Trusted', cl.trusted || 0, '#00d4aa'), + createMetric('Suspect', cl.suspect || 0, '#ffa500'), + createMetric('Blocked', cl.blocked || 0, '#ff4d4d') + ]), '#00d4aa'), + + // WiFi Interfaces Card + createCard('WiFi Interfaces', '📡', E('div', {}, + ifaces.length === 0 ? + E('p', { 'style': 'color:#808090;margin:0;' }, 'No WiFi interfaces detected') : + ifaces.map(function(iface) { + return E('div', { 'style': 'display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #2a2a3a;' }, [ + E('span', { 'style': 'color:#fff;font-weight:500;' }, iface.name), + E('span', { 'style': 'color:#808090;' }, iface.essid), + E('span', { 'style': 'color:#00d4aa;font-family:monospace;' }, iface.stations + ' STA') + ]); + }) + ), '#00a0ff'), + + // DHCP Protection Card + createCard('DHCP Protection', '🛡️', E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:10px;' }, [ + createMetric('Status', dhcpStatus.enabled ? 'ON' : 'OFF', dhcpStatus.enabled ? '#00d4aa' : '#808090'), + createMetric('Leases', dhcpStatus.leases || 0, '#00a0ff'), + createMetric('Conflicts', dhcpStatus.conflicts || 0, (dhcpStatus.conflicts || 0) > 0 ? '#ffa500' : '#00d4aa'), + createMetric('Stale', dhcpStatus.stale || 0, (dhcpStatus.stale || 0) > 0 ? '#ffa500' : '#00d4aa') + ]), dhcpStatus.enabled ? '#00d4aa' : '#808090'), + + // Config Summary Card + createCard('Configuration', '⚙️', E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:10px;' }, [ + createMetric('Policy', status.policy || 'alert', '#00a0ff'), + createMetric('Interval', (status.scan_interval || 30) + 's', '#808090'), + createMetric('Random', status.detect_random ? 'ON' : 'OFF', status.detect_random ? '#00d4aa' : '#808090'), + createMetric('Spoof', status.detect_spoof ? 'ON' : 'OFF', status.detect_spoof ? '#00d4aa' : '#808090') + ]), '#808090') + ]), + + // Known Clients Table + createCard('Known Clients', '💻', clients.length === 0 ? + E('p', { 'style': 'color:#808090;margin:0;' }, 'No clients detected yet. Run a scan or wait for devices to connect.') : + E('div', { 'style': 'overflow-x:auto;' }, [ + E('table', { 'style': 'width:100%;border-collapse:collapse;font-size:0.85rem;' }, [ + E('thead', {}, [ + E('tr', { 'style': 'border-bottom:1px solid #2a2a3a;' }, [ + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'MAC'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Vendor'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Hostname'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Interface'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Last Seen'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Status'), + E('th', { 'style': 'padding:8px;text-align:center;color:#808090;font-weight:600;' }, 'Actions') + ]) + ]), + E('tbody', {}, clients.map(function(c) { + return E('tr', { 'style': 'border-bottom:1px solid #1a1a24;' }, [ + E('td', { 'style': 'padding:8px;font-family:monospace;color:#00a0ff;' }, [ + c.mac, + c.randomized ? E('span', { + 'style': 'margin-left:6px;color:#ffa500;font-weight:bold;', + 'title': 'Randomized MAC' + }, 'R') : '' + ]), + E('td', { 'style': 'padding:8px;color:#e0e0e0;' }, c.vendor || '-'), + E('td', { 'style': 'padding:8px;color:#e0e0e0;' }, c.hostname || '-'), + E('td', { 'style': 'padding:8px;color:#808090;' }, c.iface || '-'), + E('td', { 'style': 'padding:8px;color:#808090;font-size:0.8rem;' }, formatDate(c.last_seen)), + E('td', { 'style': 'padding:8px;' }, statusBadge(c.status)), + E('td', { 'style': 'padding:8px;text-align:center;' }, [ + c.status !== 'trusted' ? E('button', { + 'style': 'padding:3px 8px;border:1px solid rgba(0,212,170,0.3);border-radius:4px;' + + 'background:rgba(0,212,170,0.2);color:#00d4aa;font-size:11px;cursor:pointer;margin-right:4px;', + 'click': function() { + callTrust(c.mac).then(function() { + ui.addNotification(null, E('p', 'MAC ' + c.mac + ' trusted'), 'info'); + window.location.reload(); + }); + } + }, 'Trust') : '', + c.status !== 'blocked' ? E('button', { + 'style': 'padding:3px 8px;border:1px solid rgba(255,77,77,0.3);border-radius:4px;' + + 'background:rgba(255,77,77,0.2);color:#ff4d4d;font-size:11px;cursor:pointer;', + 'click': function() { + if (confirm('Block and deauthenticate ' + c.mac + '?')) { + callBlock(c.mac).then(function() { + ui.addNotification(null, E('p', 'MAC ' + c.mac + ' blocked'), 'info'); + window.location.reload(); + }); + } + } + }, 'Block') : '' + ]) + ]); + })) + ]) + ]) + , '#00a0ff'), + + // Recent Alerts Table + createCard('Recent Alerts', '⚠️', events.length === 0 ? + E('p', { 'style': 'color:#808090;margin:0;' }, 'No alerts recorded.') : + E('div', { 'style': 'overflow-x:auto;' }, [ + E('table', { 'style': 'width:100%;border-collapse:collapse;font-size:0.85rem;' }, [ + E('thead', {}, [ + E('tr', { 'style': 'border-bottom:1px solid #2a2a3a;' }, [ + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Time'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Event'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'MAC'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Interface'), + E('th', { 'style': 'padding:8px;text-align:left;color:#808090;font-weight:600;' }, 'Details') + ]) + ]), + E('tbody', {}, events.slice().reverse().map(function(line) { + try { + var ev = JSON.parse(line); + return E('tr', { 'style': 'border-bottom:1px solid #1a1a24;' }, [ + E('td', { 'style': 'padding:8px;color:#808090;white-space:nowrap;' }, ev.ts || '-'), + E('td', { 'style': 'padding:8px;color:#ffa500;font-weight:600;' }, ev.event || '-'), + E('td', { 'style': 'padding:8px;font-family:monospace;color:#00a0ff;' }, ev.mac || '-'), + E('td', { 'style': 'padding:8px;color:#808090;' }, ev.iface || '-'), + E('td', { 'style': 'padding:8px;color:#e0e0e0;font-size:0.8rem;' }, ev.details || '-') + ]); + } catch(e) { + return E('tr'); + } + })) + ]) + ]) + , '#ffa500') + ]); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null });