mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user