Add TSCM advanced features UI

- Add meeting window controls (start/end tracked meetings)
- Add quick actions: capabilities, known devices, cases, playbooks
- Add export options for PDF and JSON/CSV reports
- Add JavaScript functions to connect UI to backend API endpoints
- Add CSS for capabilities grid, modal headers, playbook styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-16 16:20:47 +00:00
parent 234f254f4f
commit 6dbf2fda01
3 changed files with 1205 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -1100,6 +1100,50 @@
No content is intercepted or decoded. Professional verification required.
</div>
<!-- Active Meeting Banner (hidden by default) -->
<div id="tscmMeetingBanner" class="tscm-meeting-banner" style="display: none;">
<div class="meeting-indicator">
<span class="meeting-pulse"></span>
<span class="meeting-text">MEETING WINDOW ACTIVE</span>
</div>
<div class="meeting-info">
<span id="tscmMeetingBannerName"></span>
<span id="tscmMeetingBannerTime"></span>
</div>
</div>
<!-- Capabilities Summary Bar -->
<div id="tscmCapabilitiesBar" class="tscm-capabilities-bar" style="display: none;">
<div class="cap-item" id="capWifi" title="WiFi Capability">
<span class="cap-icon">📶</span>
<span class="cap-status" id="capWifiStatus">--</span>
</div>
<div class="cap-item" id="capBt" title="Bluetooth Capability">
<span class="cap-icon">🔵</span>
<span class="cap-status" id="capBtStatus">--</span>
</div>
<div class="cap-item" id="capRf" title="RF/SDR Capability">
<span class="cap-icon">📡</span>
<span class="cap-status" id="capRfStatus">--</span>
</div>
<div class="cap-item" id="capRoot" title="Privilege Level">
<span class="cap-icon">🔒</span>
<span class="cap-status" id="capRootStatus">--</span>
</div>
<div class="cap-limitations" id="capLimitations" onclick="tscmShowCapabilities()" style="cursor: pointer;">
<span class="cap-warn">⚠️</span>
<span id="capLimitationCount">0</span> limitations
</div>
</div>
<!-- Baseline Health Indicator -->
<div id="tscmBaselineHealth" class="tscm-baseline-health" style="display: none;">
<span class="health-label">Baseline:</span>
<span class="health-name" id="baselineHealthName">--</span>
<span class="health-badge" id="baselineHealthBadge">--</span>
<span class="health-age" id="baselineHealthAge"></span>
</div>
<!-- Risk Summary Banner (new scoring model) - clickable cards -->
<div class="tscm-threat-banner">
<div class="threat-card critical clickable" id="tscmHighInterestCard" onclick="showDevicesByCategory('high_interest')" title="Click to view high interest devices">
@@ -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 =
`<span style="color: #ff9933;">Meeting active: ${escapeHtml(meetingName)}</span>`;
// 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 =
`<span style="color: #00ff88;">Meeting ended (${duration} min) - ${data.devices_flagged || 0} devices flagged</span>`;
// 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 = '<div style="text-align: center; padding: 40px;">Loading capabilities...</div>';
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 = `
<div class="device-detail-header classification-cyan">
<h3>Sweep Capabilities</h3>
</div>
<div class="device-detail-section">
<h4>Available Detection Methods</h4>
<div class="capabilities-grid">
<div class="cap-detail-item ${caps.wifi_available ? 'available' : 'unavailable'}">
<span class="cap-icon">📶</span>
<span class="cap-name">WiFi Scanning</span>
<span class="cap-status">${caps.wifi_available ? 'Available' : 'Not Available'}</span>
${caps.wifi_interface ? `<span class="cap-detail">${caps.wifi_interface}</span>` : ''}
</div>
<div class="cap-detail-item ${caps.bluetooth_available ? 'available' : 'unavailable'}">
<span class="cap-icon">🔵</span>
<span class="cap-name">Bluetooth Scanning</span>
<span class="cap-status">${caps.bluetooth_available ? 'Available' : 'Not Available'}</span>
${caps.bt_adapter ? `<span class="cap-detail">${caps.bt_adapter}</span>` : ''}
</div>
<div class="cap-detail-item ${caps.sdr_available ? 'available' : 'unavailable'}">
<span class="cap-icon">📡</span>
<span class="cap-name">RF/SDR Scanning</span>
<span class="cap-status">${caps.sdr_available ? 'Available' : 'Not Available'}</span>
${caps.sdr_device ? `<span class="cap-detail">${caps.sdr_device}</span>` : ''}
</div>
</div>
</div>
<div class="device-detail-section">
<h4>What This Sweep CAN Detect</h4>
<ul class="cap-can-list">
${(caps.can_detect || []).map(item => `<li>✅ ${escapeHtml(item)}</li>`).join('')}
</ul>
</div>
<div class="device-detail-section">
<h4>What This Sweep CANNOT Detect</h4>
<ul class="cap-cannot-list">
${(caps.cannot_detect || []).map(item => `<li>❌ ${escapeHtml(item)}</li>`).join('')}
</ul>
</div>
<div class="device-detail-disclaimer">
<strong>Important:</strong> This tool detects wireless RF emissions only.
Professional TSCM requires physical inspection, NLJD, thermal imaging, and spectrum analysis equipment.
</div>
`;
} else {
content.innerHTML = `<div style="padding: 20px; color: #ff6666;">Failed to load capabilities: ${data.message || 'Unknown error'}</div>`;
}
} catch (e) {
console.error('Failed to load capabilities:', e);
content.innerHTML = '<div style="padding: 20px; color: #ff6666;">Failed to load capabilities</div>';
}
}
// Known Devices Management
async function tscmShowKnownDevices() {
const modal = document.getElementById('tscmDeviceModal');
const content = document.getElementById('tscmDeviceModalContent');
content.innerHTML = '<div style="text-align: center; padding: 40px;">Loading known devices...</div>';
modal.style.display = 'flex';
try {
const response = await fetch('/tscm/known-devices');
const data = await response.json();
const devices = data.devices || [];
content.innerHTML = `
<div class="device-detail-header classification-green">
<h3>✅ Known/Approved Devices (${devices.length})</h3>
</div>
<div class="device-detail-section">
<div style="margin-bottom: 12px;">
<button class="preset-btn" onclick="tscmAddKnownDevice()" style="font-size: 11px;">
+ Add Device
</button>
</div>
${devices.length === 0 ?
'<p style="color: var(--text-muted);">No known devices registered. Devices you mark as "known" will be excluded from threat scoring.</p>' :
`<div class="known-devices-list">
${devices.map(d => `
<div class="known-device-item">
<div class="known-device-info">
<strong>${escapeHtml(d.name || d.identifier)}</strong>
<span class="known-device-id">${escapeHtml(d.identifier)}</span>
<span class="known-device-type">${d.device_type}</span>
</div>
<div class="known-device-actions">
<button class="preset-btn" onclick="tscmRemoveKnownDevice(${d.id})" style="font-size: 10px; background: #ff4444;">
Remove
</button>
</div>
</div>
`).join('')}
</div>`
}
</div>
`;
} catch (e) {
console.error('Failed to load known devices:', e);
content.innerHTML = '<div style="padding: 20px; color: #ff6666;">Failed to load known devices</div>';
}
}
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 = '<div style="text-align: center; padding: 40px;">Loading cases...</div>';
modal.style.display = 'flex';
try {
const response = await fetch('/tscm/cases');
const data = await response.json();
const cases = data.cases || [];
content.innerHTML = `
<div class="device-detail-header classification-cyan">
<h3>📁 TSCM Cases (${cases.length})</h3>
</div>
<div class="device-detail-section">
<div style="margin-bottom: 12px;">
<button class="preset-btn" onclick="tscmCreateCase()" style="font-size: 11px;">
+ New Case
</button>
</div>
${cases.length === 0 ?
'<p style="color: var(--text-muted);">No cases created. Cases help you organize sweeps and findings for specific locations or clients.</p>' :
`<div class="cases-list">
${cases.map(c => `
<div class="case-item" onclick="tscmViewCase(${c.id})">
<div class="case-header">
<strong>${escapeHtml(c.name)}</strong>
<span class="case-status ${c.status}">${c.status}</span>
</div>
<div class="case-meta">
${c.client_name ? `Client: ${escapeHtml(c.client_name)} | ` : ''}
${c.location ? `Location: ${escapeHtml(c.location)} | ` : ''}
Sweeps: ${c.sweep_count || 0} | Threats: ${c.threat_count || 0}
</div>
<div class="case-date">
Created: ${new Date(c.created_at).toLocaleDateString()}
</div>
</div>
`).join('')}
</div>`
}
</div>
`;
} catch (e) {
console.error('Failed to load cases:', e);
content.innerHTML = '<div style="padding: 20px; color: #ff6666;">Failed to load cases</div>';
}
}
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 = `
<div class="device-detail-header classification-cyan">
<h3>📁 ${escapeHtml(c.name)}</h3>
<span class="case-status ${c.status}">${c.status}</span>
</div>
<div class="device-detail-section">
<h4>Case Details</h4>
<table class="device-detail-table">
<tr><td>Client</td><td>${escapeHtml(c.client_name || 'N/A')}</td></tr>
<tr><td>Location</td><td>${escapeHtml(c.location || 'N/A')}</td></tr>
<tr><td>Created</td><td>${new Date(c.created_at).toLocaleString()}</td></tr>
<tr><td>Status</td><td>${c.status}</td></tr>
</table>
</div>
<div class="device-detail-section">
<h4>Linked Sweeps (${(c.sweeps || []).length})</h4>
${(c.sweeps || []).length === 0 ?
'<p style="color: var(--text-muted);">No sweeps linked to this case yet.</p>' :
`<ul>${(c.sweeps || []).map(s => `<li>Sweep ${s.id} - ${new Date(s.timestamp).toLocaleString()}</li>`).join('')}</ul>`
}
</div>
<div class="device-detail-section">
<h4>Flagged Threats (${(c.threats || []).length})</h4>
${(c.threats || []).length === 0 ?
'<p style="color: var(--text-muted);">No threats flagged in this case.</p>' :
`<ul>${(c.threats || []).map(t => `<li>${escapeHtml(t.identifier)} - ${t.threat_type}</li>`).join('')}</ul>`
}
</div>
<div style="margin-top: 16px;">
<button class="preset-btn" onclick="tscmShowCases()">← Back to Cases</button>
</div>
`;
}
} 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 = '<div style="text-align: center; padding: 40px;">Loading playbooks...</div>';
modal.style.display = 'flex';
try {
const response = await fetch('/tscm/playbooks');
const data = await response.json();
const playbooks = data.playbooks || [];
content.innerHTML = `
<div class="device-detail-header classification-orange">
<h3>📋 Operator Playbooks</h3>
</div>
<div class="device-detail-section">
<p style="color: var(--text-muted); margin-bottom: 16px;">
Playbooks provide step-by-step guidance for investigating specific types of findings.
</p>
<div class="playbooks-list">
${playbooks.map(p => `
<div class="playbook-item" onclick="tscmViewPlaybook('${p.id}')">
<div class="playbook-header">
<strong>${escapeHtml(p.name)}</strong>
<span class="playbook-category">${escapeHtml(p.category || 'General')}</span>
</div>
<div class="playbook-desc">
${escapeHtml(p.description || 'No description')}
</div>
<div class="playbook-meta">
${p.steps?.length || 0} steps | Applies to: ${(p.applies_to || ['Any']).join(', ')}
</div>
</div>
`).join('')}
</div>
</div>
`;
} catch (e) {
console.error('Failed to load playbooks:', e);
content.innerHTML = '<div style="padding: 20px; color: #ff6666;">Failed to load playbooks</div>';
}
}
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 = `
<div class="device-detail-header classification-orange">
<h3>📋 ${escapeHtml(p.name)}</h3>
</div>
<div class="device-detail-section">
<p>${escapeHtml(p.description || '')}</p>
</div>
<div class="device-detail-section">
<h4>Steps</h4>
<ol class="playbook-steps">
${(p.steps || []).map((step, i) => `
<li class="playbook-step">
<strong>${escapeHtml(step.title || `Step ${i+1}`)}</strong>
<p>${escapeHtml(step.description || '')}</p>
${step.warning ? `<div class="playbook-warning">⚠️ ${escapeHtml(step.warning)}</div>` : ''}
</li>
`).join('')}
</ol>
</div>
${p.equipment_needed ? `
<div class="device-detail-section">
<h4>Equipment Needed</h4>
<ul>
${(p.equipment_needed || []).map(e => `<li>${escapeHtml(e)}</li>`).join('')}
</ul>
</div>
` : ''}
<div style="margin-top: 16px;">
<button class="preset-btn" onclick="tscmShowPlaybooks()">← Back to Playbooks</button>
</div>
`;
}
} 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);
});
}
});
</script>
<!-- Scanner/Audio code moved to static/js/modes/listening-post.js -->

View File

@@ -79,6 +79,61 @@
📄 Generate Report
</button>
<!-- Meeting Window Control -->
<div class="section" id="tscmMeetingSection" style="margin-top: 12px;">
<h3>Meeting Window</h3>
<div id="tscmMeetingStatus" style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
No active meeting
</div>
<div class="form-group">
<input type="text" id="tscmMeetingName" placeholder="Meeting name (optional)">
</div>
<button class="run-btn" id="tscmStartMeetingBtn" onclick="tscmStartMeeting()" style="width: 100%; padding: 8px;">
🎯 Start Meeting Window
</button>
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
⏹ End Meeting Window
</button>
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
Devices detected during meetings get flagged
</div>
</div>
<!-- Quick Actions -->
<div class="section" style="margin-top: 12px;">
<h3>Quick Actions</h3>
<div style="display: flex; flex-direction: column; gap: 6px;">
<button class="preset-btn" onclick="tscmShowCapabilities()" style="width: 100%; font-size: 10px;">
⚙️ View Capabilities
</button>
<button class="preset-btn" onclick="tscmShowKnownDevices()" style="width: 100%; font-size: 10px;">
✅ Known Devices
</button>
<button class="preset-btn" onclick="tscmShowCases()" style="width: 100%; font-size: 10px;">
📁 Cases
</button>
<button class="preset-btn" onclick="tscmShowPlaybooks()" style="width: 100%; font-size: 10px;">
📋 Playbooks
</button>
</div>
</div>
<!-- Export Options -->
<div class="section" id="tscmExportSection" style="margin-top: 12px; display: none;">
<h3>Export Report</h3>
<div style="display: flex; gap: 6px;">
<button class="preset-btn" onclick="tscmDownloadPdf()" style="flex: 1; font-size: 10px;">
📄 PDF
</button>
<button class="preset-btn" onclick="tscmDownloadAnnex('json')" style="flex: 1; font-size: 10px;">
📊 JSON
</button>
<button class="preset-btn" onclick="tscmDownloadAnnex('csv')" style="flex: 1; font-size: 10px;">
📈 CSV
</button>
</div>
</div>
<!-- Futuristic Scanner Progress -->
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none;">
<div class="scanner-ring">