From 87f72db8addc43356b1f66211e5eff1f532e15f3 Mon Sep 17 00:00:00 2001 From: Smittix Date: Wed, 14 Jan 2026 14:39:39 +0000 Subject: [PATCH] Add click-to-expand device details and fix score card updates Features: - Click any device to see detailed breakdown of why it was scored - Modal shows score circle, risk level, recommended action - Lists all indicators that contributed to the score - Shows device-specific information (MAC, RSSI, etc.) - Includes disclaimer about findings Fixes: - Score cards (High Interest, Needs Review, etc.) now update in real-time - High-interest devices (score 6+) populate the Detected Threats panel - Added updateTscmThreatCounts() calls when devices are added UI: - Device items now have cursor:pointer to indicate clickability - Added CSS for modal, score circle, indicator list, etc. Co-Authored-By: Claude Opus 4.5 --- templates/index.html | 350 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 347 insertions(+), 3 deletions(-) diff --git a/templates/index.html b/templates/index.html index 9d28c2c..90502cf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -75,6 +75,14 @@ + + +
@@ -2199,6 +2207,169 @@ border-radius: 4px; margin-top: 8px; } + /* TSCM Device Details Modal */ + .tscm-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: 10000; + } + .tscm-modal { + background: var(--panel-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + position: relative; + } + .tscm-modal-close { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: var(--text-muted); + font-size: 24px; + cursor: pointer; + z-index: 1; + } + .tscm-modal-close:hover { color: #fff; } + .device-detail-header { + padding: 16px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + } + .device-detail-header h3 { + margin: 0; + font-size: 16px; + } + .device-detail-header.classification-red { background: rgba(255, 51, 51, 0.15); } + .device-detail-header.classification-yellow { background: rgba(255, 204, 0, 0.15); } + .device-detail-header.classification-green { background: rgba(0, 204, 0, 0.15); } + .device-detail-protocol { + font-size: 10px; + padding: 3px 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + text-transform: uppercase; + } + .device-detail-score { + display: flex; + align-items: center; + padding: 16px; + gap: 16px; + border-bottom: 1px solid var(--border-color); + } + .score-circle { + width: 70px; + height: 70px; + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 3px solid; + } + .score-circle.high { border-color: #ff3333; background: rgba(255, 51, 51, 0.1); } + .score-circle.medium { border-color: #ffcc00; background: rgba(255, 204, 0, 0.1); } + .score-circle.low { border-color: #00cc00; background: rgba(0, 204, 0, 0.1); } + .score-circle .score-value { + font-size: 24px; + font-weight: 700; + } + .score-circle.high .score-value { color: #ff3333; } + .score-circle.medium .score-value { color: #ffcc00; } + .score-circle.low .score-value { color: #00cc00; } + .score-circle .score-label { + font-size: 8px; + color: var(--text-muted); + text-transform: uppercase; + } + .score-breakdown { + flex: 1; + font-size: 12px; + line-height: 1.6; + } + .device-detail-section { + padding: 16px; + border-bottom: 1px solid var(--border-color); + } + .device-detail-section h4 { + margin: 0 0 12px 0; + font-size: 12px; + color: var(--text-muted); + text-transform: uppercase; + } + .device-detail-table { + width: 100%; + font-size: 12px; + } + .device-detail-table td { + padding: 4px 0; + } + .device-detail-table td:first-child { + color: var(--text-muted); + width: 40%; + } + .indicator-list { + display: flex; + flex-direction: column; + gap: 8px; + } + .indicator-item { + display: flex; + gap: 10px; + padding: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + font-size: 11px; + } + .indicator-type { + background: rgba(255, 153, 51, 0.2); + color: #ff9933; + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + white-space: nowrap; + } + .indicator-desc { + color: var(--text-color); + } + .device-reasons-list { + margin: 0; + padding-left: 20px; + font-size: 12px; + } + .device-reasons-list li { + margin-bottom: 4px; + } + .device-detail-disclaimer { + padding: 12px 16px; + font-size: 10px; + color: var(--text-muted); + background: rgba(74, 158, 255, 0.1); + border-top: 1px solid rgba(74, 158, 255, 0.3); + } + .tscm-threat-action { + margin-top: 6px; + font-size: 10px; + color: #ff9933; + text-transform: uppercase; + font-weight: 600; + } + .tscm-device-item { + cursor: pointer; + } .tscm-threat-list { display: flex; flex-direction: column; @@ -10066,6 +10237,11 @@ if (!exists) { tscmWifiDevices.push(device); updateTscmDisplays(); + updateTscmThreatCounts(); + // Add to high interest if score >= 6 + if (device.score >= 6) { + addHighInterestDevice(device, 'wifi'); + } } } @@ -10075,6 +10251,11 @@ if (!exists) { tscmBtDevices.push(device); updateTscmDisplays(); + updateTscmThreatCounts(); + // Add to high interest if score >= 6 + if (device.score >= 6) { + addHighInterestDevice(device, 'bluetooth'); + } } } @@ -10085,6 +10266,52 @@ if (!exists) { tscmRfSignals.push(signal); updateTscmDisplays(); + updateTscmThreatCounts(); + // Add to high interest if score >= 6 + if (signal.score >= 6) { + addHighInterestDevice(signal, 'rf'); + } + } + } + + // Track high-interest devices for the threats panel + let tscmHighInterestDevices = []; + function addHighInterestDevice(device, protocol) { + const id = device.mac || device.bssid || device.frequency; + const exists = tscmHighInterestDevices.some(d => d.id === id); + if (!exists) { + tscmHighInterestDevices.push({ + id: id, + protocol: protocol, + name: device.name || device.ssid || `${device.frequency} MHz`, + score: device.score, + classification: device.classification, + indicators: device.indicators || [], + recommended_action: device.recommended_action, + device: device + }); + updateHighInterestPanel(); + } + } + + function updateHighInterestPanel() { + const panel = document.getElementById('tscmThreatList'); + if (tscmHighInterestDevices.length === 0) { + panel.innerHTML = '
No high-interest devices detected
'; + } else { + panel.innerHTML = '
' + tscmHighInterestDevices.map(d => ` +
+
+ ${d.protocol.toUpperCase()} + Score: ${d.score} +
+
+ ${escapeHtml(d.name)}
+ ${d.indicators.slice(0, 2).map(i => i.desc || i.type).join(' | ')} +
+
${d.recommended_action || 'investigate'}
+
+ `).join('') + '
'; } } @@ -10193,6 +10420,123 @@ return `Score: ${score}`; } + // Store all devices for lookup + function getAllTscmDevices() { + const devices = {}; + tscmWifiDevices.forEach(d => { devices[`wifi:${d.bssid}`] = {...d, protocol: 'wifi'}; }); + tscmBtDevices.forEach(d => { devices[`bluetooth:${d.mac}`] = {...d, protocol: 'bluetooth'}; }); + tscmRfSignals.forEach(d => { devices[`rf:${d.frequency}`] = {...d, protocol: 'rf'}; }); + return devices; + } + + function showDeviceDetails(id, protocol) { + const devices = getAllTscmDevices(); + const key = `${protocol}:${id}`; + const device = devices[key]; + + if (!device) { + console.warn('Device not found:', key); + return; + } + + const modal = document.getElementById('tscmDeviceModal'); + const content = document.getElementById('tscmDeviceModalContent'); + + // Build detailed view + let html = ` +
+

${getClassificationIcon(device.classification)} ${escapeHtml(device.name || device.ssid || device.mac || device.bssid || device.frequency + ' MHz')}

+ ${protocol.toUpperCase()} +
+ +
+
+ ${device.score || 0} + SCORE +
+
+ Risk Level: ${device.classification === 'high_interest' ? 'HIGH INTEREST' : device.classification === 'review' ? 'NEEDS REVIEW' : 'INFORMATIONAL'}
+ Recommended Action: ${device.recommended_action || 'Monitor'} +
+
+ +
+

Device Information

+ + `; + + // Add device-specific fields + if (protocol === 'wifi') { + html += ` + + + + + + `; + } else if (protocol === 'bluetooth') { + html += ` + + + + + + `; + } else if (protocol === 'rf') { + html += ` + + + + + `; + } + html += `
BSSID${device.bssid || 'Unknown'}
SSID${escapeHtml(device.ssid || '[Hidden]')}
Channel${device.channel || 'Unknown'}
Signal${device.signal || '--'} dBm
Security${device.security || 'Unknown'}
MAC Address${device.mac || 'Unknown'}
Name${escapeHtml(device.name || 'Unknown')}
Type${device.device_type || 'Unknown'}
RSSI${device.rssi || '--'} dBm
Audio Capable${device.is_audio_capable ? 'Yes' : 'No'}
Frequency${device.frequency?.toFixed(3) || 'Unknown'} MHz
Band${device.band || 'Unknown'}
Power${device.power?.toFixed(1) || '--'} dBm
Signal Strength+${(device.signal_strength || 0).toFixed(1)} dB above noise
`; + + // Add indicators section + if (device.indicators && device.indicators.length > 0) { + html += ` +
+

Risk Indicators (Why This Score)

+
+ ${device.indicators.map(i => ` +
+ ${i.type} + ${escapeHtml(i.desc || '')} +
+ `).join('')} +
+
+ `; + } + + // Add reasons section + if (device.reasons && device.reasons.length > 0) { + html += ` +
+

Detection Notes

+
    + ${device.reasons.map(r => `
  • ${escapeHtml(r)}
  • `).join('')} +
+
+ `; + } + + // Add disclaimer + html += ` +
+ Disclaimer: This analysis identifies indicators and anomalies. + It does NOT confirm surveillance activity. Professional verification required. +
+ `; + + content.innerHTML = html; + modal.style.display = 'flex'; + } + + function closeTscmDeviceModal() { + document.getElementById('tscmDeviceModal').style.display = 'none'; + } + function updateTscmDisplays() { // Update WiFi list const wifiList = document.getElementById('tscmWifiList'); @@ -10202,7 +10546,7 @@ // Sort by score (highest first) const sorted = [...tscmWifiDevices].sort((a, b) => (b.score || 0) - (a.score || 0)); wifiList.innerHTML = sorted.map(d => ` -
+
${getClassificationIcon(d.classification)} @@ -10230,7 +10574,7 @@ // Sort by score (highest first) const sorted = [...tscmBtDevices].sort((a, b) => (b.score || 0) - (a.score || 0)); btList.innerHTML = sorted.map(d => ` -
+
${getClassificationIcon(d.classification)} @@ -10259,7 +10603,7 @@ // Sort by score (highest first) const sorted = [...tscmRfSignals].sort((a, b) => (b.score || 0) - (a.score || 0)); rfList.innerHTML = sorted.map(s => ` -
+
${getClassificationIcon(s.classification)}