diff --git a/static/css/modes/tscm.css b/static/css/modes/tscm.css index abcc80a..550672e 100644 --- a/static/css/modes/tscm.css +++ b/static/css/modes/tscm.css @@ -723,3 +723,530 @@ 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } + +/* Meeting Window Banner */ +.tscm-meeting-banner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + margin-bottom: 12px; + background: linear-gradient(90deg, rgba(255, 51, 102, 0.2), rgba(255, 153, 51, 0.2)); + border: 1px solid rgba(255, 51, 102, 0.5); + border-radius: 6px; + animation: meeting-glow 2s ease-in-out infinite; +} +@keyframes meeting-glow { + 0%, 100% { box-shadow: 0 0 5px rgba(255, 51, 102, 0.3); } + 50% { box-shadow: 0 0 15px rgba(255, 51, 102, 0.5); } +} +.meeting-indicator { + display: flex; + align-items: center; + gap: 8px; +} +.meeting-pulse { + width: 10px; + height: 10px; + background: #ff3366; + border-radius: 50%; + animation: pulse-dot 1.5s ease-in-out infinite; +} +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.2); } +} +.meeting-text { + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + color: #ff3366; + text-transform: uppercase; +} +.meeting-info { + font-size: 11px; + color: var(--text-secondary); + display: flex; + gap: 12px; +} + +/* Capabilities Bar */ +.tscm-capabilities-bar { + display: flex; + align-items: center; + gap: 16px; + padding: 8px 12px; + margin-bottom: 12px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 11px; +} +.cap-item { + display: flex; + align-items: center; + gap: 4px; +} +.cap-icon { + font-size: 14px; +} +.cap-status { + color: var(--text-muted); + font-size: 10px; + text-transform: uppercase; +} +.cap-status.available { color: #00cc00; } +.cap-status.limited { color: #ffcc00; } +.cap-status.unavailable { color: #ff3333; } +.cap-limitations { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; + color: #ff9933; + font-size: 10px; +} +.cap-warn { + font-size: 12px; +} + +/* Baseline Health Indicator */ +.tscm-baseline-health { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + margin-bottom: 12px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + font-size: 11px; +} +.health-label { + color: var(--text-muted); +} +.health-name { + color: var(--text-primary); + font-weight: 600; +} +.health-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; +} +.health-badge.healthy { + background: rgba(0, 204, 0, 0.2); + color: #00cc00; +} +.health-badge.noisy { + background: rgba(255, 204, 0, 0.2); + color: #ffcc00; +} +.health-badge.stale { + background: rgba(255, 51, 51, 0.2); + color: #ff3333; +} +.health-age { + color: var(--text-muted); + font-size: 10px; + margin-left: auto; +} + +/* Advanced Modal Styles */ +.tscm-advanced-modal { + max-width: 600px; +} +.tscm-modal-header { + padding: 16px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} +.tscm-modal-header h3 { + margin: 0; + font-size: 16px; +} +.tscm-modal-body { + padding: 16px; + max-height: 60vh; + overflow-y: auto; +} +.tscm-modal-section { + margin-bottom: 16px; +} +.tscm-modal-section h4 { + margin: 0 0 8px 0; + font-size: 12px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Capabilities Detail */ +.cap-detail-item { + padding: 10px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + border-left: 3px solid var(--border-color); +} +.cap-detail-item.available { border-left-color: #00cc00; } +.cap-detail-item.limited { border-left-color: #ffcc00; } +.cap-detail-item.unavailable { border-left-color: #ff3333; } +.cap-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} +.cap-detail-name { + font-weight: 600; + font-size: 12px; +} +.cap-detail-status { + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; +} +.cap-detail-status.available { background: rgba(0, 204, 0, 0.2); color: #00cc00; } +.cap-detail-status.limited { background: rgba(255, 204, 0, 0.2); color: #ffcc00; } +.cap-detail-status.unavailable { background: rgba(255, 51, 51, 0.2); color: #ff3333; } +.cap-detail-limits { + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; +} +.cap-detail-limits li { + margin-bottom: 2px; +} + +/* Known Devices List */ +.known-device-item { + padding: 10px; + margin-bottom: 6px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + border-left: 3px solid #00cc00; + display: flex; + justify-content: space-between; + align-items: center; +} +.known-device-info { + flex: 1; +} +.known-device-name { + font-weight: 600; + font-size: 12px; +} +.known-device-id { + font-size: 10px; + color: var(--text-muted); + font-family: 'JetBrains Mono', monospace; +} +.known-device-actions { + display: flex; + gap: 6px; +} +.known-device-btn { + padding: 4px 8px; + font-size: 10px; + border: none; + border-radius: 3px; + cursor: pointer; +} +.known-device-btn.remove { + background: rgba(255, 51, 51, 0.2); + color: #ff3333; +} +.known-device-btn.remove:hover { + background: rgba(255, 51, 51, 0.4); +} + +/* Cases List */ +.case-item { + padding: 12px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 6px; + border-left: 3px solid var(--primary-color); + cursor: pointer; + transition: background 0.2s; +} +.case-item:hover { + background: rgba(74, 158, 255, 0.1); +} +.case-item.priority-high { border-left-color: #ff3333; } +.case-item.priority-normal { border-left-color: #4a9eff; } +.case-item.priority-low { border-left-color: #00cc00; } +.case-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} +.case-name { + font-weight: 600; + font-size: 13px; +} +.case-status { + font-size: 9px; + padding: 2px 6px; + border-radius: 3px; + text-transform: uppercase; +} +.case-status.open { background: rgba(0, 204, 0, 0.2); color: #00cc00; } +.case-status.closed { background: rgba(128, 128, 128, 0.2); color: #888; } +.case-meta { + font-size: 10px; + color: var(--text-muted); + display: flex; + gap: 12px; +} + +/* Playbook Styles */ +.playbook-item { + padding: 12px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 6px; + border-left: 3px solid #ff9933; +} +.playbook-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +.playbook-title { + font-weight: 600; + font-size: 13px; +} +.playbook-risk { + font-size: 9px; + padding: 2px 6px; + border-radius: 3px; + text-transform: uppercase; +} +.playbook-risk.high_interest { background: rgba(255, 51, 51, 0.2); color: #ff3333; } +.playbook-risk.needs_review { background: rgba(255, 204, 0, 0.2); color: #ffcc00; } +.playbook-risk.informational { background: rgba(0, 204, 0, 0.2); color: #00cc00; } +.playbook-desc { + font-size: 11px; + color: var(--text-secondary); + margin-bottom: 8px; +} +.playbook-steps { + font-size: 11px; +} +.playbook-step { + padding: 6px 8px; + margin-bottom: 4px; + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} +.playbook-step-num { + color: #ff9933; + font-weight: 600; + margin-right: 6px; +} + +/* Timeline Styles */ +.timeline-container { + padding: 12px; + background: rgba(0, 0, 0, 0.2); + border-radius: 6px; +} +.timeline-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.timeline-device-name { + font-weight: 600; + font-size: 14px; +} +.timeline-metrics { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 12px; +} +.timeline-metric { + padding: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + text-align: center; +} +.timeline-metric-value { + font-size: 16px; + font-weight: 700; + color: var(--accent-cyan); +} +.timeline-metric-label { + font-size: 9px; + color: var(--text-muted); + text-transform: uppercase; +} +.timeline-chart { + height: 60px; + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + position: relative; + overflow: hidden; +} +.timeline-bar { + position: absolute; + bottom: 0; + width: 3px; + background: var(--accent-cyan); + border-radius: 2px 2px 0 0; +} + +/* Proximity Badge */ +.proximity-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; +} +.proximity-badge.very_close { + background: rgba(255, 51, 51, 0.2); + color: #ff3333; +} +.proximity-badge.close { + background: rgba(255, 153, 51, 0.2); + color: #ff9933; +} +.proximity-badge.moderate { + background: rgba(255, 204, 0, 0.2); + color: #ffcc00; +} +.proximity-badge.far { + background: rgba(0, 204, 0, 0.2); + color: #00cc00; +} + +/* Add to Known Device Button */ +.add-known-btn { + padding: 4px 8px; + font-size: 10px; + background: rgba(0, 204, 0, 0.2); + color: #00cc00; + border: 1px solid rgba(0, 204, 0, 0.3); + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; +} +.add-known-btn:hover { + background: rgba(0, 204, 0, 0.3); +} + +/* Capabilities Grid */ +.capabilities-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 16px; +} +.cap-detail-item .cap-icon { + font-size: 24px; + display: block; + margin-bottom: 8px; +} +.cap-detail-item .cap-name { + font-weight: 600; + font-size: 12px; + display: block; + margin-bottom: 4px; +} +.cap-detail-item .cap-status { + font-size: 10px; + color: var(--text-muted); +} +.cap-detail-item .cap-detail { + font-size: 9px; + color: var(--text-muted); + display: block; + margin-top: 4px; + font-family: 'JetBrains Mono', monospace; +} +.cap-can-list, .cap-cannot-list { + list-style: none; + padding: 0; + margin: 0; +} +.cap-can-list li, .cap-cannot-list li { + padding: 6px 0; + font-size: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} +.cap-can-list li:last-child, .cap-cannot-list li:last-child { + border-bottom: none; +} + +/* Modal Header Classification Colors */ +.device-detail-header.classification-cyan { + background: linear-gradient(135deg, rgba(0, 204, 255, 0.2) 0%, rgba(0, 150, 200, 0.1) 100%); + border-bottom: 2px solid #00ccff; +} +.device-detail-header.classification-orange { + background: linear-gradient(135deg, rgba(255, 153, 51, 0.2) 0%, rgba(200, 120, 40, 0.1) 100%); + border-bottom: 2px solid #ff9933; +} +.device-detail-header.classification-green { + background: linear-gradient(135deg, rgba(0, 204, 0, 0.2) 0%, rgba(0, 150, 0, 0.1) 100%); + border-bottom: 2px solid #00cc00; +} + +/* Playbook Enhancements */ +.playbook-item { + cursor: pointer; + transition: all 0.2s; +} +.playbook-item:hover { + background: rgba(255, 153, 51, 0.1); +} +.playbook-category { + font-size: 9px; + padding: 2px 6px; + background: rgba(255, 153, 51, 0.2); + color: #ff9933; + border-radius: 3px; + text-transform: uppercase; +} +.playbook-meta { + font-size: 10px; + color: var(--text-muted); + margin-top: 8px; +} +.playbook-warning { + padding: 8px 12px; + background: rgba(255, 153, 51, 0.15); + border: 1px solid rgba(255, 153, 51, 0.3); + border-radius: 4px; + font-size: 11px; + margin-top: 8px; +} + +/* Case Status Enhancements */ +.case-date { + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; +} + +/* Known Device Type Badge */ +.known-device-type { + font-size: 9px; + padding: 2px 6px; + background: rgba(74, 158, 255, 0.2); + color: #4a9eff; + border-radius: 3px; + margin-left: 8px; +} diff --git a/templates/index.html b/templates/index.html index 9909f9b..947946f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1100,6 +1100,50 @@ No content is intercepted or decoded. Professional verification required. + + + + + + + + +
@@ -8955,6 +8999,585 @@ div.textContent = str; return div.innerHTML; } + + // ========== TSCM Advanced Features ========== + + // Meeting Window Management + let tscmActiveMeetingId = null; + let tscmMeetingStartTime = null; + + async function tscmStartMeeting() { + const meetingName = document.getElementById('tscmMeetingName').value || + `Meeting ${new Date().toLocaleString()}`; + + try { + const response = await fetch('/tscm/meeting/start-tracked', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: meetingName }) + }); + + const data = await response.json(); + if (data.status === 'success') { + tscmActiveMeetingId = data.meeting_id; + tscmMeetingStartTime = new Date(); + + // Update UI + document.getElementById('tscmStartMeetingBtn').style.display = 'none'; + document.getElementById('tscmEndMeetingBtn').style.display = 'block'; + document.getElementById('tscmMeetingStatus').innerHTML = + `Meeting active: ${escapeHtml(meetingName)}`; + + // Show meeting banner + const banner = document.getElementById('tscmMeetingBanner'); + if (banner) { + banner.style.display = 'flex'; + const nameSpan = document.getElementById('tscmMeetingName_display'); + if (nameSpan) nameSpan.textContent = meetingName; + } + } else { + alert(data.message || 'Failed to start meeting window'); + } + } catch (e) { + console.error('Failed to start meeting:', e); + alert('Failed to start meeting window'); + } + } + + async function tscmEndMeeting() { + if (!tscmActiveMeetingId) return; + + try { + const response = await fetch(`/tscm/meeting/${tscmActiveMeetingId}/end`, { + method: 'POST' + }); + + const data = await response.json(); + + // Update UI + document.getElementById('tscmStartMeetingBtn').style.display = 'block'; + document.getElementById('tscmEndMeetingBtn').style.display = 'none'; + + // Hide meeting banner + const banner = document.getElementById('tscmMeetingBanner'); + if (banner) banner.style.display = 'none'; + + if (data.status === 'success') { + const duration = tscmMeetingStartTime ? + Math.round((new Date() - tscmMeetingStartTime) / 60000) : 0; + document.getElementById('tscmMeetingStatus').innerHTML = + `Meeting ended (${duration} min) - ${data.devices_flagged || 0} devices flagged`; + + // Show export section if devices were flagged + if (data.devices_flagged > 0) { + document.getElementById('tscmExportSection').style.display = 'block'; + } + } else { + document.getElementById('tscmMeetingStatus').textContent = 'Meeting ended'; + } + + tscmActiveMeetingId = null; + tscmMeetingStartTime = null; + } catch (e) { + console.error('Failed to end meeting:', e); + } + } + + // Capabilities Display + async function tscmShowCapabilities() { + const modal = document.getElementById('tscmDeviceModal'); + const content = document.getElementById('tscmDeviceModalContent'); + + content.innerHTML = '
Loading capabilities...
'; + modal.style.display = 'flex'; + + try { + const response = await fetch('/tscm/capabilities'); + const data = await response.json(); + + if (data.status === 'success') { + const caps = data.capabilities; + content.innerHTML = ` +
+

Sweep Capabilities

+
+
+

Available Detection Methods

+
+
+ 📶 + WiFi Scanning + ${caps.wifi_available ? 'Available' : 'Not Available'} + ${caps.wifi_interface ? `${caps.wifi_interface}` : ''} +
+
+ 🔵 + Bluetooth Scanning + ${caps.bluetooth_available ? 'Available' : 'Not Available'} + ${caps.bt_adapter ? `${caps.bt_adapter}` : ''} +
+
+ 📡 + RF/SDR Scanning + ${caps.sdr_available ? 'Available' : 'Not Available'} + ${caps.sdr_device ? `${caps.sdr_device}` : ''} +
+
+
+
+

What This Sweep CAN Detect

+
    + ${(caps.can_detect || []).map(item => `
  • ✅ ${escapeHtml(item)}
  • `).join('')} +
+
+
+

What This Sweep CANNOT Detect

+
    + ${(caps.cannot_detect || []).map(item => `
  • ❌ ${escapeHtml(item)}
  • `).join('')} +
+
+
+ Important: This tool detects wireless RF emissions only. + Professional TSCM requires physical inspection, NLJD, thermal imaging, and spectrum analysis equipment. +
+ `; + } else { + content.innerHTML = `
Failed to load capabilities: ${data.message || 'Unknown error'}
`; + } + } catch (e) { + console.error('Failed to load capabilities:', e); + content.innerHTML = '
Failed to load capabilities
'; + } + } + + // Known Devices Management + async function tscmShowKnownDevices() { + const modal = document.getElementById('tscmDeviceModal'); + const content = document.getElementById('tscmDeviceModalContent'); + + content.innerHTML = '
Loading known devices...
'; + modal.style.display = 'flex'; + + try { + const response = await fetch('/tscm/known-devices'); + const data = await response.json(); + + const devices = data.devices || []; + content.innerHTML = ` +
+

✅ Known/Approved Devices (${devices.length})

+
+
+
+ +
+ ${devices.length === 0 ? + '

No known devices registered. Devices you mark as "known" will be excluded from threat scoring.

' : + `
+ ${devices.map(d => ` +
+
+ ${escapeHtml(d.name || d.identifier)} + ${escapeHtml(d.identifier)} + ${d.device_type} +
+
+ +
+
+ `).join('')} +
` + } +
+ `; + } catch (e) { + console.error('Failed to load known devices:', e); + content.innerHTML = '
Failed to load known devices
'; + } + } + + async function tscmAddKnownDevice() { + const identifier = prompt('Enter device identifier (MAC address, BSSID, or frequency):'); + if (!identifier) return; + + const name = prompt('Enter friendly name for this device:'); + const deviceType = prompt('Enter device type (wifi/bluetooth/rf):') || 'wifi'; + + try { + const response = await fetch('/tscm/known-devices', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + identifier: identifier, + name: name || identifier, + device_type: deviceType + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + tscmShowKnownDevices(); // Refresh list + } else { + alert(data.message || 'Failed to add device'); + } + } catch (e) { + console.error('Failed to add known device:', e); + alert('Failed to add device'); + } + } + + async function tscmRemoveKnownDevice(deviceId) { + if (!confirm('Remove this device from known devices list?')) return; + + try { + const response = await fetch(`/tscm/known-devices/${deviceId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + if (data.status === 'success') { + tscmShowKnownDevices(); // Refresh list + } else { + alert(data.message || 'Failed to remove device'); + } + } catch (e) { + console.error('Failed to remove known device:', e); + } + } + + // Cases Management + async function tscmShowCases() { + const modal = document.getElementById('tscmDeviceModal'); + const content = document.getElementById('tscmDeviceModalContent'); + + content.innerHTML = '
Loading cases...
'; + modal.style.display = 'flex'; + + try { + const response = await fetch('/tscm/cases'); + const data = await response.json(); + + const cases = data.cases || []; + content.innerHTML = ` +
+

📁 TSCM Cases (${cases.length})

+
+
+
+ +
+ ${cases.length === 0 ? + '

No cases created. Cases help you organize sweeps and findings for specific locations or clients.

' : + `
+ ${cases.map(c => ` +
+
+ ${escapeHtml(c.name)} + ${c.status} +
+
+ ${c.client_name ? `Client: ${escapeHtml(c.client_name)} | ` : ''} + ${c.location ? `Location: ${escapeHtml(c.location)} | ` : ''} + Sweeps: ${c.sweep_count || 0} | Threats: ${c.threat_count || 0} +
+
+ Created: ${new Date(c.created_at).toLocaleDateString()} +
+
+ `).join('')} +
` + } +
+ `; + } catch (e) { + console.error('Failed to load cases:', e); + content.innerHTML = '
Failed to load cases
'; + } + } + + async function tscmCreateCase() { + const name = prompt('Enter case name:'); + if (!name) return; + + const clientName = prompt('Enter client name (optional):'); + const location = prompt('Enter location (optional):'); + + try { + const response = await fetch('/tscm/cases', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + client_name: clientName || null, + location: location || null + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + tscmShowCases(); // Refresh list + } else { + alert(data.message || 'Failed to create case'); + } + } catch (e) { + console.error('Failed to create case:', e); + alert('Failed to create case'); + } + } + + async function tscmViewCase(caseId) { + try { + const response = await fetch(`/tscm/cases/${caseId}`); + const data = await response.json(); + + if (data.status === 'success') { + const c = data.case; + const content = document.getElementById('tscmDeviceModalContent'); + content.innerHTML = ` +
+

📁 ${escapeHtml(c.name)}

+ ${c.status} +
+
+

Case Details

+ + + + + +
Client${escapeHtml(c.client_name || 'N/A')}
Location${escapeHtml(c.location || 'N/A')}
Created${new Date(c.created_at).toLocaleString()}
Status${c.status}
+
+
+

Linked Sweeps (${(c.sweeps || []).length})

+ ${(c.sweeps || []).length === 0 ? + '

No sweeps linked to this case yet.

' : + `
    ${(c.sweeps || []).map(s => `
  • Sweep ${s.id} - ${new Date(s.timestamp).toLocaleString()}
  • `).join('')}
` + } +
+
+

Flagged Threats (${(c.threats || []).length})

+ ${(c.threats || []).length === 0 ? + '

No threats flagged in this case.

' : + `
    ${(c.threats || []).map(t => `
  • ${escapeHtml(t.identifier)} - ${t.threat_type}
  • `).join('')}
` + } +
+
+ +
+ `; + } + } catch (e) { + console.error('Failed to view case:', e); + } + } + + // Playbooks Display + async function tscmShowPlaybooks() { + const modal = document.getElementById('tscmDeviceModal'); + const content = document.getElementById('tscmDeviceModalContent'); + + content.innerHTML = '
Loading playbooks...
'; + modal.style.display = 'flex'; + + try { + const response = await fetch('/tscm/playbooks'); + const data = await response.json(); + + const playbooks = data.playbooks || []; + content.innerHTML = ` +
+

📋 Operator Playbooks

+
+
+

+ Playbooks provide step-by-step guidance for investigating specific types of findings. +

+
+ ${playbooks.map(p => ` +
+
+ ${escapeHtml(p.name)} + ${escapeHtml(p.category || 'General')} +
+
+ ${escapeHtml(p.description || 'No description')} +
+
+ ${p.steps?.length || 0} steps | Applies to: ${(p.applies_to || ['Any']).join(', ')} +
+
+ `).join('')} +
+
+ `; + } catch (e) { + console.error('Failed to load playbooks:', e); + content.innerHTML = '
Failed to load playbooks
'; + } + } + + async function tscmViewPlaybook(playbookId) { + try { + const response = await fetch(`/tscm/playbooks/${playbookId}`); + const data = await response.json(); + + if (data.status === 'success') { + const p = data.playbook; + const content = document.getElementById('tscmDeviceModalContent'); + content.innerHTML = ` +
+

📋 ${escapeHtml(p.name)}

+
+
+

${escapeHtml(p.description || '')}

+
+
+

Steps

+
    + ${(p.steps || []).map((step, i) => ` +
  1. + ${escapeHtml(step.title || `Step ${i+1}`)} +

    ${escapeHtml(step.description || '')}

    + ${step.warning ? `
    ⚠️ ${escapeHtml(step.warning)}
    ` : ''} +
  2. + `).join('')} +
+
+ ${p.equipment_needed ? ` +
+

Equipment Needed

+
    + ${(p.equipment_needed || []).map(e => `
  • ${escapeHtml(e)}
  • `).join('')} +
+
+ ` : ''} +
+ +
+ `; + } + } catch (e) { + console.error('Failed to view playbook:', e); + } + } + + // Report Downloads + async function tscmDownloadPdf() { + try { + const response = await fetch('/tscm/report/pdf'); + if (response.ok) { + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `TSCM_Report_${new Date().toISOString().split('T')[0]}.pdf`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } else { + const data = await response.json(); + alert(data.message || 'Failed to generate PDF'); + } + } catch (e) { + console.error('Failed to download PDF:', e); + alert('Failed to download PDF report'); + } + } + + async function tscmDownloadAnnex(format) { + try { + const response = await fetch(`/tscm/report/annex?format=${format}`); + if (response.ok) { + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `TSCM_Annex_${new Date().toISOString().split('T')[0]}.${format}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } else { + const data = await response.json(); + alert(data.message || 'Failed to generate annex'); + } + } catch (e) { + console.error('Failed to download annex:', e); + alert('Failed to download technical annex'); + } + } + + // Update capabilities bar on sweep start + async function updateTscmCapabilitiesBar() { + try { + const response = await fetch('/tscm/capabilities'); + const data = await response.json(); + + if (data.status === 'success') { + const caps = data.capabilities; + const bar = document.getElementById('tscmCapabilitiesBar'); + + if (bar) { + document.getElementById('capWifiStatus').textContent = caps.wifi_available ? 'ON' : 'OFF'; + document.getElementById('capWifi').classList.toggle('active', caps.wifi_available); + + document.getElementById('capBtStatus').textContent = caps.bluetooth_available ? 'ON' : 'OFF'; + document.getElementById('capBt').classList.toggle('active', caps.bluetooth_available); + + document.getElementById('capSdrStatus').textContent = caps.sdr_available ? 'ON' : 'OFF'; + document.getElementById('capSdr').classList.toggle('active', caps.sdr_available); + + bar.style.display = 'flex'; + } + } + } catch (e) { + console.error('Failed to update capabilities bar:', e); + } + } + + // Update baseline health indicator + async function updateTscmBaselineHealth(baselineId) { + if (!baselineId) { + const healthDiv = document.getElementById('tscmBaselineHealth'); + if (healthDiv) healthDiv.style.display = 'none'; + return; + } + + try { + const response = await fetch(`/tscm/baseline/${baselineId}/health`); + const data = await response.json(); + + if (data.status === 'success') { + const healthDiv = document.getElementById('tscmBaselineHealth'); + const badge = document.getElementById('baselineHealthBadge'); + + if (healthDiv && badge) { + badge.textContent = data.health_status || 'Unknown'; + badge.className = `health-badge health-${(data.health_status || 'unknown').toLowerCase()}`; + healthDiv.style.display = 'block'; + } + } + } catch (e) { + console.error('Failed to update baseline health:', e); + } + } + + // Listen for baseline selection changes + document.addEventListener('DOMContentLoaded', function() { + const baselineSelect = document.getElementById('tscmBaselineSelect'); + if (baselineSelect) { + baselineSelect.addEventListener('change', function() { + updateTscmBaselineHealth(this.value); + }); + } + }); diff --git a/templates/partials/modes/tscm.html b/templates/partials/modes/tscm.html index 80c51a7..8ee6071 100644 --- a/templates/partials/modes/tscm.html +++ b/templates/partials/modes/tscm.html @@ -79,6 +79,61 @@ 📄 Generate Report + +
+

Meeting Window

+
+ No active meeting +
+
+ +
+ + +
+ Devices detected during meetings get flagged +
+
+ + +
+

Quick Actions

+
+ + + + +
+
+ + + +