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.
+
+
+
+
+ MEETING WINDOW ACTIVE
+
+
+
+
+
+
+
+
+
+
+ 📶
+ --
+
+
+ 🔵
+ --
+
+
+ 📡
+ --
+
+
+ 🔒
+ --
+
+
+ ⚠️
+ 0 limitations
+
+
+
+
+
+ Baseline:
+ --
+ --
+
+
+
@@ -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 = `
+
+
+
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 = `
+
+
+
+
+
+ ${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 = `
+
+
+
+
+
+ ${cases.length === 0 ?
+ '
No cases created. Cases help you organize sweeps and findings for specific locations or clients.
' :
+ `
+ ${cases.map(c => `
+
+
+
+ ${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 = `
+
+
+
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 = `
+
+
+
+ Playbooks provide step-by-step guidance for investigating specific types of findings.
+
+
+ ${playbooks.map(p => `
+
+
+
+ ${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.description || '')}
+
+
+
Steps
+
+ ${(p.steps || []).map((step, i) => `
+ -
+ ${escapeHtml(step.title || `Step ${i+1}`)}
+
${escapeHtml(step.description || '')}
+ ${step.warning ? `⚠️ ${escapeHtml(step.warning)}
` : ''}
+
+ `).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
+
+
+
+
+
+
+
+
+
+
+
Export Report
+
+
+
+
+
+
+