mirror of
https://github.com/smittix/intercept.git
synced 2026-07-27 10:08:10 -07:00
Add terrestrial HF SSTV mode with predefined frequencies and modulation support
Adds a general-purpose SSTV decoder alongside the existing ISS SSTV mode, supporting USB/LSB/FM modulation on common amateur radio HF/VHF/UHF frequencies (14.230 MHz USB, 3.845 MHz LSB, etc.) with auto-detection of modulation from preset frequency table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+563
-51
@@ -43,6 +43,8 @@
|
||||
{% else %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
{% endif %}
|
||||
<!-- Chart.js date adapter for time-scale axes -->
|
||||
<script src="{{ url_for('static', filename='vendor/chartjs/chartjs-adapter-date-fns.bundle.min.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||||
@@ -57,6 +59,7 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/spy-stations.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/meshtastic.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/sstv.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/sstv-general.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}">
|
||||
</head>
|
||||
@@ -183,6 +186,14 @@
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span>
|
||||
<span class="mode-name">Meshtastic</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('dmr')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg></span>
|
||||
<span class="mode-name">Digital Voice</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('websdr')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
||||
<span class="mode-name">WebSDR</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -224,6 +235,10 @@
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span>
|
||||
<span class="mode-name">ISS SSTV</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('sstv_general')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span>
|
||||
<span class="mode-name">HF SSTV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -506,6 +521,8 @@
|
||||
|
||||
{% include 'partials/modes/sstv.html' %}
|
||||
|
||||
{% include 'partials/modes/sstv-general.html' %}
|
||||
|
||||
{% include 'partials/modes/listening-post.html' %}
|
||||
|
||||
{% include 'partials/modes/tscm.html' %}
|
||||
@@ -516,6 +533,10 @@
|
||||
|
||||
{% include 'partials/modes/meshtastic.html' %}
|
||||
|
||||
{% include 'partials/modes/dmr.html' %}
|
||||
|
||||
{% include 'partials/modes/websdr.html' %}
|
||||
|
||||
<button class="preset-btn" onclick="killAll()"
|
||||
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
||||
Kill All Processes
|
||||
@@ -1372,6 +1393,16 @@
|
||||
<div class="radio-module-box" style="grid-column: span 4; padding: 10px;">
|
||||
<div id="listeningPostTimelineContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- WATERFALL / SPECTROGRAM PANEL -->
|
||||
<div id="waterfallPanel" class="radio-module-box" style="grid-column: span 4; padding: 10px; display: none;">
|
||||
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
|
||||
<span>WATERFALL / SPECTROGRAM</span>
|
||||
<span id="waterfallFreqRange" style="font-size: 9px; color: var(--accent-cyan);"></span>
|
||||
</div>
|
||||
<canvas id="spectrumCanvas" width="800" height="120" style="width: 100%; height: 120px; border-radius: 4px; background: rgba(0,0,0,0.8);"></canvas>
|
||||
<canvas id="waterfallCanvas" width="800" height="400" style="width: 100%; height: 400px; border-radius: 4px; margin-top: 4px; background: rgba(0,0,0,0.9);"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satellite Dashboard (Embedded) -->
|
||||
@@ -1570,6 +1601,130 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Timelines Overview -->
|
||||
<div class="tscm-panel" id="tscmDeviceTimelinesPanel" style="margin-top: 12px;">
|
||||
<div class="tscm-panel-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
Device Timelines
|
||||
<button class="preset-btn" onclick="loadDeviceTimelines()" style="font-size: 9px; padding: 3px 8px;">Refresh</button>
|
||||
</div>
|
||||
<div class="tscm-panel-content" id="tscmDeviceTimelinesList">
|
||||
<div class="tscm-empty">Run a sweep to see device timelines</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DMR / Digital Voice Dashboard -->
|
||||
<div id="dmrVisuals" style="display: none; padding: 12px; flex-direction: column; gap: 12px; flex: 1; min-height: 0;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<!-- Call History Panel -->
|
||||
<div class="radio-module-box" style="padding: 10px;">
|
||||
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
|
||||
<span>CALL HISTORY</span>
|
||||
<span id="dmrHistoryCount" style="color: var(--accent-cyan);">0 calls</span>
|
||||
</div>
|
||||
<div style="max-height: 400px; overflow-y: auto;">
|
||||
<table style="width: 100%; font-size: 10px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="color: var(--text-muted); border-bottom: 1px solid var(--border-color);">
|
||||
<th style="text-align: left; padding: 4px;">Time</th>
|
||||
<th style="text-align: left; padding: 4px;">Talkgroup</th>
|
||||
<th style="text-align: left; padding: 4px;">Source</th>
|
||||
<th style="text-align: left; padding: 4px;">Protocol</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dmrHistoryBody">
|
||||
<tr><td colspan="4" style="padding: 15px; text-align: center; color: var(--text-muted);">No calls recorded</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Protocol Info Panel -->
|
||||
<div class="radio-module-box" style="padding: 10px;">
|
||||
<div class="module-header" style="margin-bottom: 8px; font-size: 10px;">
|
||||
<span>DECODER STATUS</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; padding: 10px;">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 2px; margin-bottom: 4px;">PROTOCOL</div>
|
||||
<div id="dmrMainProtocol" style="font-size: 28px; font-weight: bold; color: var(--accent-cyan); font-family: var(--font-mono);">--</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
||||
<div style="text-align: center; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px;">
|
||||
<div style="font-size: 9px; color: var(--text-muted);">CALLS</div>
|
||||
<div id="dmrMainCallCount" style="font-size: 22px; font-weight: bold; color: var(--accent-green); font-family: var(--font-mono);">0</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px;">
|
||||
<div style="font-size: 9px; color: var(--text-muted);">SYNCS</div>
|
||||
<div id="dmrSyncCount" style="font-size: 22px; font-weight: bold; color: var(--accent-orange); font-family: var(--font-mono);">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSDR Dashboard -->
|
||||
<div id="websdrVisuals" style="display: none; padding: 12px; flex-direction: column; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
|
||||
<!-- Audio Control Bar (hidden until connected) -->
|
||||
<div id="kiwiAudioControls" class="radio-module-box" style="display: none; padding: 8px 12px; flex-shrink: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<!-- Live indicator -->
|
||||
<div style="display: flex; align-items: center; gap: 5px;">
|
||||
<div id="kiwiLiveIndicator" style="width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green); animation: pulse 1.5s infinite;"></div>
|
||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">LIVE</span>
|
||||
</div>
|
||||
<!-- Receiver name -->
|
||||
<span id="kiwiBarReceiverName" style="font-size: 11px; color: var(--accent-cyan); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></span>
|
||||
<!-- Frequency input -->
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<input type="number" id="kiwiBarFrequency" step="1" style="width: 80px; font-size: 12px; font-family: var(--font-mono); padding: 2px 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||||
<span style="font-size: 10px; color: var(--text-muted);">kHz</span>
|
||||
</div>
|
||||
<!-- Mode selector -->
|
||||
<select id="kiwiBarMode" style="font-size: 11px; padding: 2px 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||||
<option value="am">AM</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
<option value="cw">CW</option>
|
||||
</select>
|
||||
<!-- Tune button -->
|
||||
<button class="preset-btn" onclick="tuneFromBar()" style="font-size: 10px; padding: 3px 10px;">Tune</button>
|
||||
<!-- Volume -->
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<span style="font-size: 10px; color: var(--text-muted);">VOL</span>
|
||||
<input type="range" id="kiwiBarVolume" min="0" max="100" value="80" style="width: 60px;" oninput="setKiwiVolume(this.value)">
|
||||
</div>
|
||||
<!-- S-meter mini -->
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<div style="width: 50px; height: 6px; background: rgba(0,0,0,0.5); border-radius: 3px; overflow: hidden;">
|
||||
<div id="kiwiBarSmeter" style="height: 100%; width: 0%; background: linear-gradient(to right, var(--accent-green), var(--accent-orange)); transition: width 0.2s; border-radius: 3px;"></div>
|
||||
</div>
|
||||
<span id="kiwiBarSmeterValue" style="font-size: 9px; color: var(--text-muted); font-family: var(--font-mono); min-width: 20px;">S0</span>
|
||||
</div>
|
||||
<!-- Disconnect -->
|
||||
<button class="stop-btn" onclick="disconnectFromReceiver()" style="font-size: 10px; padding: 3px 10px; margin-left: auto;">Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Map and receiver list side by side -->
|
||||
<div style="display: grid; grid-template-columns: 3fr 1fr; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
|
||||
<!-- Map -->
|
||||
<div class="radio-module-box" style="padding: 0; overflow: hidden; position: relative; min-height: 0;">
|
||||
<div id="websdrMap" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
|
||||
</div>
|
||||
<!-- Receiver List -->
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; overflow: hidden;">
|
||||
<div class="radio-module-box" style="padding: 10px; flex: 1; overflow-y: auto; min-height: 0;">
|
||||
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
|
||||
<span>RECEIVERS</span>
|
||||
<span id="websdrReceiverCount" style="color: var(--accent-cyan);">0 found</span>
|
||||
</div>
|
||||
<div id="websdrReceiverList" style="font-size: 11px;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 20px;">Click "Find Receivers" to search</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spy Stations Dashboard -->
|
||||
@@ -1880,6 +2035,88 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSTV General Decoder Dashboard -->
|
||||
<div id="sstvGeneralVisuals" class="sstv-general-visuals-container" style="display: none;">
|
||||
<!-- Status Strip -->
|
||||
<div class="sstv-general-stats-strip">
|
||||
<div class="sstv-general-strip-group">
|
||||
<div class="sstv-general-strip-status">
|
||||
<span class="sstv-general-strip-dot idle" id="sstvGeneralStripDot"></span>
|
||||
<span class="sstv-general-strip-status-text" id="sstvGeneralStripStatus">Idle</span>
|
||||
</div>
|
||||
<button class="sstv-general-strip-btn start" id="sstvGeneralStartBtn" onclick="SSTVGeneral.start()">Start</button>
|
||||
<button class="sstv-general-strip-btn stop" id="sstvGeneralStopBtn" onclick="SSTVGeneral.stop()" style="display: none;">Stop</button>
|
||||
</div>
|
||||
<div class="sstv-general-strip-divider"></div>
|
||||
<div class="sstv-general-strip-group">
|
||||
<div class="sstv-general-strip-stat">
|
||||
<span class="sstv-general-strip-value accent-cyan" id="sstvGeneralStripFreq">14.230</span>
|
||||
<span class="sstv-general-strip-label">MHZ</span>
|
||||
</div>
|
||||
<div class="sstv-general-strip-stat">
|
||||
<span class="sstv-general-strip-value" id="sstvGeneralStripMod">USB</span>
|
||||
<span class="sstv-general-strip-label">MOD</span>
|
||||
</div>
|
||||
<div class="sstv-general-strip-stat">
|
||||
<span class="sstv-general-strip-value" id="sstvGeneralStripImageCount">0</span>
|
||||
<span class="sstv-general-strip-label">IMAGES</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Row (Live + Gallery) -->
|
||||
<div class="sstv-general-main-row">
|
||||
<!-- Live Decode Section -->
|
||||
<div class="sstv-general-live-section">
|
||||
<div class="sstv-general-live-header">
|
||||
<div class="sstv-general-live-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
Live Decode
|
||||
</div>
|
||||
</div>
|
||||
<div class="sstv-general-live-content" id="sstvGeneralLiveContent">
|
||||
<div class="sstv-general-idle-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M3 9h2M19 9h2M3 15h2M19 15h2"/>
|
||||
</svg>
|
||||
<h4>SSTV Decoder</h4>
|
||||
<p>Select a frequency and click Start to listen for SSTV transmissions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Section -->
|
||||
<div class="sstv-general-gallery-section">
|
||||
<div class="sstv-general-gallery-header">
|
||||
<div class="sstv-general-gallery-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
Decoded Images
|
||||
</div>
|
||||
<span class="sstv-general-gallery-count" id="sstvGeneralImageCount">0</span>
|
||||
</div>
|
||||
<div class="sstv-general-gallery-grid" id="sstvGeneralGallery">
|
||||
<div class="sstv-general-gallery-empty">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
<p>No images decoded yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
|
||||
<div class="recon-panel collapsed" id="reconPanel">
|
||||
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
|
||||
@@ -1967,6 +2204,9 @@
|
||||
<script src="{{ url_for('static', filename='js/modes/spy-stations.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/meshtastic.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/sstv.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/sstv-general.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/dmr.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/websdr.js') }}"></script>
|
||||
|
||||
<script>
|
||||
// ============================================
|
||||
@@ -2102,7 +2342,7 @@
|
||||
const validModes = new Set([
|
||||
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
||||
'spystations', 'meshtastic', 'wifi', 'bluetooth',
|
||||
'tscm', 'satellite', 'sstv'
|
||||
'tscm', 'satellite', 'sstv', 'sstv_general', 'dmr', 'websdr'
|
||||
]);
|
||||
|
||||
function getModeFromQuery() {
|
||||
@@ -2524,7 +2764,7 @@
|
||||
'tscm': 'security',
|
||||
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
|
||||
'meshtastic': 'sdr',
|
||||
'satellite': 'space', 'sstv': 'space'
|
||||
'satellite': 'space', 'sstv': 'space', 'sstv_general': 'space'
|
||||
};
|
||||
|
||||
// Remove has-active from all dropdowns
|
||||
@@ -2593,7 +2833,8 @@
|
||||
const modeMap = {
|
||||
'pager': 'pager', 'sensor': '433',
|
||||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||||
'listening': 'listening', 'aprs': 'aprs', 'tscm': 'tscm', 'meshtastic': 'meshtastic'
|
||||
'listening': 'listening', 'aprs': 'aprs', 'tscm': 'tscm', 'meshtastic': 'meshtastic',
|
||||
'dmr': 'dmr', 'websdr': 'websdr', 'sstv_general': 'hf sstv'
|
||||
};
|
||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||
const label = btn.querySelector('.nav-label');
|
||||
@@ -2606,6 +2847,7 @@
|
||||
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
|
||||
document.getElementById('satelliteMode')?.classList.toggle('active', mode === 'satellite');
|
||||
document.getElementById('sstvMode')?.classList.toggle('active', mode === 'sstv');
|
||||
document.getElementById('sstvGeneralMode')?.classList.toggle('active', mode === 'sstv_general');
|
||||
document.getElementById('wifiMode')?.classList.toggle('active', mode === 'wifi');
|
||||
document.getElementById('bluetoothMode')?.classList.toggle('active', mode === 'bluetooth');
|
||||
document.getElementById('listeningPostMode')?.classList.toggle('active', mode === 'listening');
|
||||
@@ -2614,6 +2856,8 @@
|
||||
document.getElementById('aisMode')?.classList.toggle('active', mode === 'ais');
|
||||
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
||||
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
|
||||
document.getElementById('dmrMode')?.classList.toggle('active', mode === 'dmr');
|
||||
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
|
||||
const pagerStats = document.getElementById('pagerStats');
|
||||
const sensorStats = document.getElementById('sensorStats');
|
||||
const satelliteStats = document.getElementById('satelliteStats');
|
||||
@@ -2640,6 +2884,7 @@
|
||||
'rtlamr': 'METERS',
|
||||
'satellite': 'SATELLITE',
|
||||
'sstv': 'ISS SSTV',
|
||||
'sstv_general': 'HF SSTV',
|
||||
'wifi': 'WIFI',
|
||||
'bluetooth': 'BLUETOOTH',
|
||||
'listening': 'LISTENING POST',
|
||||
@@ -2647,7 +2892,9 @@
|
||||
'tscm': 'TSCM',
|
||||
'ais': 'AIS VESSELS',
|
||||
'spystations': 'SPY STATIONS',
|
||||
'meshtastic': 'MESHTASTIC'
|
||||
'meshtastic': 'MESHTASTIC',
|
||||
'dmr': 'DIGITAL VOICE',
|
||||
'websdr': 'WEBSDR'
|
||||
};
|
||||
const activeModeIndicator = document.getElementById('activeModeIndicator');
|
||||
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
|
||||
@@ -2660,6 +2907,9 @@
|
||||
const spyStationsVisuals = document.getElementById('spyStationsVisuals');
|
||||
const meshtasticVisuals = document.getElementById('meshtasticVisuals');
|
||||
const sstvVisuals = document.getElementById('sstvVisuals');
|
||||
const sstvGeneralVisuals = document.getElementById('sstvGeneralVisuals');
|
||||
const dmrVisuals = document.getElementById('dmrVisuals');
|
||||
const websdrVisuals = document.getElementById('websdrVisuals');
|
||||
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||||
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
|
||||
@@ -2669,6 +2919,9 @@
|
||||
if (spyStationsVisuals) spyStationsVisuals.style.display = mode === 'spystations' ? 'flex' : 'none';
|
||||
if (meshtasticVisuals) meshtasticVisuals.style.display = mode === 'meshtastic' ? 'flex' : 'none';
|
||||
if (sstvVisuals) sstvVisuals.style.display = mode === 'sstv' ? 'flex' : 'none';
|
||||
if (sstvGeneralVisuals) sstvGeneralVisuals.style.display = mode === 'sstv_general' ? 'flex' : 'none';
|
||||
if (dmrVisuals) dmrVisuals.style.display = mode === 'dmr' ? 'flex' : 'none';
|
||||
if (websdrVisuals) websdrVisuals.style.display = mode === 'websdr' ? 'flex' : 'none';
|
||||
|
||||
// Hide sidebar by default for Meshtastic mode, show for others
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
@@ -2693,6 +2946,7 @@
|
||||
'rtlamr': 'Utility Meter Monitor',
|
||||
'satellite': 'Satellite Monitor',
|
||||
'sstv': 'ISS SSTV Decoder',
|
||||
'sstv_general': 'HF SSTV Decoder',
|
||||
'wifi': 'WiFi Scanner',
|
||||
'bluetooth': 'Bluetooth Scanner',
|
||||
'listening': 'Listening Post',
|
||||
@@ -2700,7 +2954,9 @@
|
||||
'tscm': 'TSCM Counter-Surveillance',
|
||||
'ais': 'AIS Vessel Tracker',
|
||||
'spystations': 'Spy Stations',
|
||||
'meshtastic': 'Meshtastic Mesh Monitor'
|
||||
'meshtastic': 'Meshtastic Mesh Monitor',
|
||||
'dmr': 'Digital Voice Decoder',
|
||||
'websdr': 'HF/Shortwave WebSDR'
|
||||
};
|
||||
const outputTitle = document.getElementById('outputTitle');
|
||||
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
|
||||
@@ -2718,7 +2974,7 @@
|
||||
const reconBtn = document.getElementById('reconBtn');
|
||||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||||
const reconPanel = document.getElementById('reconPanel');
|
||||
if (mode === 'satellite' || mode === 'sstv' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic') {
|
||||
if (mode === 'satellite' || mode === 'sstv' || mode === 'sstv_general' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr') {
|
||||
if (reconPanel) reconPanel.style.display = 'none';
|
||||
if (reconBtn) reconBtn.style.display = 'none';
|
||||
if (intelBtn) intelBtn.style.display = 'none';
|
||||
@@ -2738,7 +2994,7 @@
|
||||
|
||||
// Show RTL-SDR device section for modes that use it
|
||||
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
|
||||
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv') ? 'block' : 'none';
|
||||
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'sstv_general' || mode === 'dmr') ? 'block' : 'none';
|
||||
|
||||
// Toggle mode-specific tool status displays
|
||||
const toolStatusPager = document.getElementById('toolStatusPager');
|
||||
@@ -2749,8 +3005,8 @@
|
||||
// Hide output console for modes with their own visualizations
|
||||
const outputEl = document.getElementById('output');
|
||||
const statusBar = document.querySelector('.status-bar');
|
||||
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'listening' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic') ? 'none' : 'block';
|
||||
if (statusBar) statusBar.style.display = (mode === 'satellite') ? 'none' : 'flex';
|
||||
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'sstv_general' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'listening' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr') ? 'none' : 'block';
|
||||
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'dmr') ? 'none' : 'flex';
|
||||
|
||||
// Restore sidebar when leaving Meshtastic mode (user may have collapsed it)
|
||||
if (mode !== 'meshtastic') {
|
||||
@@ -2797,6 +3053,12 @@
|
||||
}, 100);
|
||||
} else if (mode === 'sstv') {
|
||||
SSTV.init();
|
||||
} else if (mode === 'sstv_general') {
|
||||
SSTVGeneral.init();
|
||||
} else if (mode === 'dmr') {
|
||||
if (typeof checkDmrTools === 'function') checkDmrTools();
|
||||
} else if (mode === 'websdr') {
|
||||
if (typeof initWebSDR === 'function') initWebSDR();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11067,6 +11329,23 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// Signal Timeline Chart
|
||||
html += `
|
||||
<div class="device-detail-section">
|
||||
<h4>Signal Timeline</h4>
|
||||
<canvas id="deviceTimelineChart" width="600" height="180" style="width: 100%; max-height: 180px;"></canvas>
|
||||
<div id="deviceTimelineMetrics" style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Playbook section
|
||||
html += `
|
||||
<div class="device-detail-section" id="devicePlaybookSection" style="display: none;">
|
||||
<h4>Recommended Playbook</h4>
|
||||
<div id="devicePlaybookContent"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add disclaimer
|
||||
html += `
|
||||
<div class="device-detail-disclaimer">
|
||||
@@ -11081,6 +11360,21 @@
|
||||
const timelineIdentifier = tscmNormalizeIdentifier(id, protocol, device);
|
||||
loadTscmTimeline(timelineIdentifier, protocol);
|
||||
loadTscmAdvancedAnalysis(device, protocol);
|
||||
|
||||
// Load timeline chart
|
||||
fetchDeviceTimelineChart(timelineIdentifier, protocol);
|
||||
|
||||
// Load playbook for this device
|
||||
fetchDevicePlaybook(timelineIdentifier).then(playbook => {
|
||||
if (playbook) {
|
||||
const section = document.getElementById('devicePlaybookSection');
|
||||
const pbContent = document.getElementById('devicePlaybookContent');
|
||||
if (section && pbContent) {
|
||||
section.style.display = 'block';
|
||||
pbContent.innerHTML = renderPlaybook(playbook);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tscmNormalizeIdentifier(identifier, protocol, device) {
|
||||
@@ -11437,6 +11731,152 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDeviceTimelines() {
|
||||
const container = document.getElementById('tscmDeviceTimelinesList');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="tscm-empty">Loading timelines...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/tscm/timelines');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status !== 'success' || !data.timelines || data.timelines.length === 0) {
|
||||
container.innerHTML = '<div class="tscm-empty">No device timelines available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const timelines = data.timelines;
|
||||
let html = '';
|
||||
timelines.forEach(tl => {
|
||||
const identifier = tl.identifier || 'Unknown';
|
||||
const protocol = tl.protocol || 'unknown';
|
||||
const presencePct = tl.presence_ratio !== undefined ? Math.round(tl.presence_ratio * 100) : 0;
|
||||
const pattern = tl.movement_pattern || 'UNKNOWN';
|
||||
const patternColors = { 'STATIONARY': '#00e676', 'MOBILE': '#ff3366', 'INTERMITTENT': '#ff9800' };
|
||||
const pColor = patternColors[pattern] || '#9e9e9e';
|
||||
|
||||
// Create a compact swim-lane row
|
||||
html += `
|
||||
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; font-size: 10px;"
|
||||
onclick="tscmShowInvestigateById('${escapeHtml(identifier)}', '${escapeHtml(protocol)}')">
|
||||
<span style="width: 12px; text-transform: uppercase; color: var(--text-muted); font-size: 8px;">${protocol.charAt(0).toUpperCase()}</span>
|
||||
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); font-family: var(--font-mono);">${escapeHtml(identifier)}</span>
|
||||
<div style="width: 100px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden;">
|
||||
<div style="width: ${presencePct}%; height: 100%; background: var(--accent-cyan); border-radius: 3px;"></div>
|
||||
</div>
|
||||
<span style="width: 35px; text-align: right; color: var(--accent-cyan);">${presencePct}%</span>
|
||||
<span style="padding: 1px 6px; background: ${pColor}22; color: ${pColor}; border-radius: 3px; font-size: 8px; font-weight: bold;">${pattern}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
} catch (e) {
|
||||
console.error('Failed to load device timelines:', e);
|
||||
container.innerHTML = '<div class="tscm-empty">Failed to load timelines</div>';
|
||||
}
|
||||
}
|
||||
|
||||
let deviceTimelineChartInstance = null;
|
||||
|
||||
async function fetchDeviceTimelineChart(identifier, protocol) {
|
||||
try {
|
||||
const response = await fetch(`/tscm/device/${encodeURIComponent(identifier)}/timeline?protocol=${encodeURIComponent(protocol)}&since_hours=24`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status !== 'success' || !data.timeline) return;
|
||||
|
||||
const timeline = data.timeline;
|
||||
const observations = timeline.observations || [];
|
||||
const metrics = timeline.metrics || {};
|
||||
const signal = timeline.signal || {};
|
||||
const movement = timeline.movement || {};
|
||||
|
||||
// Render Chart.js RSSI timeline
|
||||
const canvas = document.getElementById('deviceTimelineChart');
|
||||
if (canvas && typeof Chart !== 'undefined' && observations.length > 0) {
|
||||
if (deviceTimelineChartInstance) {
|
||||
deviceTimelineChartInstance.destroy();
|
||||
}
|
||||
|
||||
const chartData = observations.map(o => ({
|
||||
x: new Date(o.timestamp),
|
||||
y: o.rssi !== null && o.rssi !== undefined ? o.rssi : null,
|
||||
})).filter(d => d.y !== null);
|
||||
|
||||
const pointColors = chartData.map(d => d.y !== null ? 'rgba(0, 230, 118, 0.8)' : 'rgba(158, 158, 158, 0.5)');
|
||||
|
||||
deviceTimelineChartInstance = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'RSSI (dBm)',
|
||||
data: chartData,
|
||||
borderColor: 'rgba(0, 212, 255, 0.8)',
|
||||
backgroundColor: 'rgba(0, 212, 255, 0.1)',
|
||||
fill: true,
|
||||
pointBackgroundColor: pointColors,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
tension: 0.3,
|
||||
borderWidth: 1.5,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: { unit: 'hour', displayFormats: { hour: 'ha', minute: 'h:mm a' } },
|
||||
ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 9 } },
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
},
|
||||
y: {
|
||||
title: { display: true, text: 'dBm', color: 'rgba(255,255,255,0.5)', font: { size: 9 } },
|
||||
ticks: { color: 'rgba(255,255,255,0.5)', font: { size: 9 } },
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render metrics badges
|
||||
renderTimelineMetrics(metrics, signal, movement);
|
||||
} catch (e) {
|
||||
console.error('Failed to load device timeline chart:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimelineMetrics(metrics, signal, movement) {
|
||||
const container = document.getElementById('deviceTimelineMetrics');
|
||||
if (!container) return;
|
||||
|
||||
const badges = [];
|
||||
if (metrics.total_observations !== undefined) {
|
||||
badges.push(`<span style="padding: 4px 8px; background: rgba(0,212,255,0.15); color: var(--accent-cyan); border-radius: 4px; font-size: 10px;">${metrics.total_observations} observations</span>`);
|
||||
}
|
||||
if (metrics.presence_ratio !== undefined) {
|
||||
const pct = Math.round(metrics.presence_ratio * 100);
|
||||
badges.push(`<span style="padding: 4px 8px; background: rgba(0,230,118,0.15); color: #00e676; border-radius: 4px; font-size: 10px;">${pct}% presence</span>`);
|
||||
}
|
||||
if (signal.rssi_min !== undefined && signal.rssi_max !== undefined && signal.rssi_min !== null) {
|
||||
badges.push(`<span style="padding: 4px 8px; background: rgba(255,152,0,0.15); color: #ff9800; border-radius: 4px; font-size: 10px;">${signal.rssi_min} to ${signal.rssi_max} dBm</span>`);
|
||||
}
|
||||
if (signal.stability !== undefined && signal.stability !== null) {
|
||||
badges.push(`<span style="padding: 4px 8px; background: rgba(156,39,176,0.15); color: #ce93d8; border-radius: 4px; font-size: 10px;">${Math.round(signal.stability * 100)}% stability</span>`);
|
||||
}
|
||||
if (movement.pattern) {
|
||||
const patternColors = { 'STATIONARY': '#00e676', 'MOBILE': '#ff3366', 'INTERMITTENT': '#ff9800' };
|
||||
const color = patternColors[movement.pattern] || '#9e9e9e';
|
||||
badges.push(`<span style="padding: 4px 8px; background: ${color}22; color: ${color}; border-radius: 4px; font-size: 10px; font-weight: bold;">${movement.pattern}</span>`);
|
||||
}
|
||||
container.innerHTML = badges.join('');
|
||||
}
|
||||
|
||||
async function loadTscmAdvancedAnalysis(device, protocol) {
|
||||
if (protocol === 'wifi') {
|
||||
const section = document.getElementById('tscmWifiAdvancedSection');
|
||||
@@ -13247,54 +13687,102 @@
|
||||
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.title || p.name || 'Playbook')}</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.action || step.title || `Step ${step.step || i + 1}`)}</strong>
|
||||
<p>${escapeHtml(step.details || step.description || '')}</p>
|
||||
${step.safety_note ? `<div class="playbook-warning">${escapeHtml(step.safety_note)}</div>` : ''}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ol>
|
||||
</div>
|
||||
${p.when_to_escalate ? `
|
||||
<div class="device-detail-section">
|
||||
<h4>When to Escalate</h4>
|
||||
<p>${escapeHtml(p.when_to_escalate)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
${p.documentation_required && p.documentation_required.length > 0 ? `
|
||||
<div class="device-detail-section">
|
||||
<h4>Documentation Required</h4>
|
||||
<ul>
|
||||
${p.documentation_required.map(d => `<li>${escapeHtml(d)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${p.disclaimer ? `
|
||||
<div class="device-detail-disclaimer">
|
||||
<strong>Disclaimer:</strong> ${escapeHtml(p.disclaimer)}
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="margin-top: 16px;">
|
||||
<button class="preset-btn" onclick="tscmShowPlaybooks()">← Back to Playbooks</button>
|
||||
</div>
|
||||
`;
|
||||
content.innerHTML = renderPlaybook(p);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to view playbook:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlaybook(p) {
|
||||
const riskColors = { 'critical': '#ff3366', 'high': '#ff6633', 'medium': '#ff9800', 'low': '#4caf50' };
|
||||
const riskColor = riskColors[(p.risk_level || '').toLowerCase()] || '#ff9800';
|
||||
return `
|
||||
<div class="device-detail-header" style="border-left: 4px solid ${riskColor};">
|
||||
<h3>${escapeHtml(p.title || p.name || 'Playbook')}</h3>
|
||||
<span style="font-size: 10px; background: ${riskColor}; color: #000; padding: 2px 8px; border-radius: 3px; font-weight: bold; text-transform: uppercase;">${escapeHtml(p.risk_level || 'MEDIUM')}</span>
|
||||
</div>
|
||||
<div class="device-detail-section">
|
||||
<p style="color: var(--text-muted);">${escapeHtml(p.description || '')}</p>
|
||||
</div>
|
||||
${p.when_to_escalate ? `
|
||||
<div style="margin: 12px 0; padding: 10px 14px; background: rgba(255,51,102,0.1); border: 1px solid rgba(255,51,102,0.4); border-radius: 6px;">
|
||||
<strong style="color: #ff3366; font-size: 11px; text-transform: uppercase;">Escalation Trigger</strong>
|
||||
<p style="margin: 4px 0 0; font-size: 12px; color: #ff6666;">${escapeHtml(p.when_to_escalate)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="device-detail-section">
|
||||
<h4 style="margin-bottom: 10px;">Investigation Steps</h4>
|
||||
<div class="playbook-checklist">
|
||||
${(p.steps || []).map((step, i) => {
|
||||
const stepNum = step.step_number || step.step || (i + 1);
|
||||
return `
|
||||
<div class="playbook-check-step" id="pbStep${i}" style="display: flex; gap: 10px; padding: 10px; margin-bottom: 8px; background: rgba(0,0,0,0.2); border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; transition: border-color 0.3s;" onclick="togglePlaybookStep(${i})">
|
||||
<div style="flex-shrink: 0; display: flex; align-items: flex-start; padding-top: 2px;">
|
||||
<input type="checkbox" id="pbCheck${i}" style="width: 16px; height: 16px; accent-color: #00e676; cursor: pointer;" onclick="event.stopPropagation(); togglePlaybookStep(${i})">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;">
|
||||
<span style="font-size: 10px; color: var(--accent-cyan); font-weight: bold;">STEP ${stepNum}</span>
|
||||
<strong style="font-size: 12px;">${escapeHtml(step.action || step.title || '')}</strong>
|
||||
</div>
|
||||
<p style="font-size: 11px; color: var(--text-muted); margin: 0;">${escapeHtml(step.details || step.description || '')}</p>
|
||||
${step.safety_note ? `<div style="margin-top: 6px; padding: 6px 8px; background: rgba(255,152,0,0.1); border-left: 3px solid #ff9800; border-radius: 3px; font-size: 10px; color: #ffb74d;"><strong>Safety:</strong> ${escapeHtml(step.safety_note)}</div>` : ''}
|
||||
${step.evidence_needed && step.evidence_needed.length > 0 ? `<div style="margin-top: 6px; font-size: 10px; color: var(--text-muted);"><strong>Evidence needed:</strong> ${step.evidence_needed.map(e => escapeHtml(e)).join(', ')}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
${p.documentation_required && p.documentation_required.length > 0 ? `
|
||||
<div class="device-detail-section">
|
||||
<h4>Documentation Required</h4>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
${p.documentation_required.map(d => `<li style="padding: 4px 0; font-size: 11px; color: var(--text-secondary);">☐ ${escapeHtml(d)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${p.disclaimer ? `
|
||||
<div class="device-detail-disclaimer">
|
||||
<strong>Disclaimer:</strong> ${escapeHtml(p.disclaimer)}
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="margin-top: 16px;">
|
||||
<button class="preset-btn" onclick="tscmShowPlaybooks()">← Back to Playbooks</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function togglePlaybookStep(index) {
|
||||
const checkbox = document.getElementById('pbCheck' + index);
|
||||
const stepEl = document.getElementById('pbStep' + index);
|
||||
if (!checkbox || !stepEl) return;
|
||||
// Toggle if triggered from the row (not the checkbox itself)
|
||||
if (document.activeElement !== checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
if (checkbox.checked) {
|
||||
stepEl.style.borderColor = '#00e676';
|
||||
stepEl.style.background = 'rgba(0, 230, 118, 0.05)';
|
||||
} else {
|
||||
stepEl.style.borderColor = 'var(--border-color)';
|
||||
stepEl.style.background = 'rgba(0,0,0,0.2)';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDevicePlaybook(identifier) {
|
||||
try {
|
||||
const response = await fetch(`/tscm/findings/${encodeURIComponent(identifier)}/playbook`);
|
||||
const data = await response.json();
|
||||
if (data.status === 'success' && data.playbook) {
|
||||
return data.playbook;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch device playbook:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Report Downloads
|
||||
async function tscmDownloadPdf() {
|
||||
try {
|
||||
@@ -13530,6 +14018,10 @@
|
||||
scanner</span></div>
|
||||
<div class="icon-item"><span class="icon">🔍</span><span class="desc">TSCM -
|
||||
Counter-surveillance</span></div>
|
||||
<div class="icon-item"><span class="icon">📺</span><span class="desc">ISS SSTV - Space station
|
||||
images</span></div>
|
||||
<div class="icon-item"><span class="icon">📺</span><span class="desc">HF SSTV - Terrestrial
|
||||
SSTV images</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13649,6 +14141,26 @@
|
||||
<li>View connected nodes and message history</li>
|
||||
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>ISS SSTV Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decode Slow-Scan Television images from the International Space Station</li>
|
||||
<li>ISS transmits on 145.800 MHz FM during special ARISS events</li>
|
||||
<li>Real-time ISS tracking map with ground track overlay</li>
|
||||
<li>Next-pass countdown with elevation and duration predictions</li>
|
||||
<li>Optional Doppler shift compensation for improved reception</li>
|
||||
<li>Requires: <code>slowrx</code> decoder + RTL-SDR</li>
|
||||
</ul>
|
||||
|
||||
<h3>HF SSTV Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decode terrestrial SSTV images on HF/VHF/UHF amateur radio frequencies</li>
|
||||
<li>Predefined frequencies: 14.230 MHz USB (20m, most popular), 3.845/7.171 MHz LSB, and more</li>
|
||||
<li>Supports USB, LSB, and FM demodulation modes</li>
|
||||
<li>Auto-detects correct modulation when selecting a preset frequency</li>
|
||||
<li>HF frequencies (below 30 MHz) require an upconverter with RTL-SDR</li>
|
||||
<li>Requires: <code>slowrx</code> decoder + RTL-SDR (+ upconverter for HF)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Section -->
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- SSTV GENERAL MODE -->
|
||||
<div id="sstvGeneralMode" class="mode-content">
|
||||
<div class="section">
|
||||
<h3>SSTV Decoder</h3>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 12px;">
|
||||
Decode Slow-Scan Television images on common amateur radio HF/VHF/UHF frequencies.
|
||||
Select a predefined frequency or enter a custom one.
|
||||
</p>
|
||||
<p class="info-text" style="font-size: 10px; color: var(--accent-yellow); margin-bottom: 8px;">
|
||||
Note: HF frequencies (below 30 MHz) require an upconverter with RTL-SDR.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Frequency</h3>
|
||||
<div class="form-group">
|
||||
<label>Preset Frequency</label>
|
||||
<select id="sstvGeneralPresetFreq" onchange="SSTVGeneral.selectPreset(this.value)" style="width: 100%; padding: 6px 8px; font-family: var(--font-mono); font-size: 11px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||||
<option value="">-- Select frequency --</option>
|
||||
<optgroup label="80 m (HF)">
|
||||
<option value="3.845|lsb">3.845 MHz LSB - US calling</option>
|
||||
<option value="3.730|lsb">3.730 MHz LSB - Europe primary</option>
|
||||
</optgroup>
|
||||
<optgroup label="40 m (HF)">
|
||||
<option value="7.171|lsb">7.171 MHz LSB - International</option>
|
||||
<option value="7.040|lsb">7.040 MHz LSB - Alt US/EU</option>
|
||||
</optgroup>
|
||||
<optgroup label="30 m (HF)">
|
||||
<option value="10.132|usb">10.132 MHz USB - Narrowband</option>
|
||||
</optgroup>
|
||||
<optgroup label="20 m (HF)">
|
||||
<option value="14.230|usb">14.230 MHz USB - Most popular</option>
|
||||
<option value="14.233|usb">14.233 MHz USB - Digital SSTV</option>
|
||||
<option value="14.240|usb">14.240 MHz USB - Europe alt</option>
|
||||
</optgroup>
|
||||
<optgroup label="15 m (HF)">
|
||||
<option value="21.340|usb">21.340 MHz USB - International</option>
|
||||
</optgroup>
|
||||
<optgroup label="10 m (HF)">
|
||||
<option value="28.680|usb">28.680 MHz USB - International</option>
|
||||
</optgroup>
|
||||
<optgroup label="6 m (VHF)">
|
||||
<option value="50.950|usb">50.950 MHz USB - SSTV calling</option>
|
||||
</optgroup>
|
||||
<optgroup label="2 m (VHF)">
|
||||
<option value="145.625|fm">145.625 MHz FM - Simplex</option>
|
||||
</optgroup>
|
||||
<optgroup label="70 cm (UHF)">
|
||||
<option value="433.775|fm">433.775 MHz FM - Simplex</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frequency (MHz)</label>
|
||||
<input type="number" id="sstvGeneralFrequency" value="14.230" step="0.001" min="1" max="500">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Modulation</label>
|
||||
<select id="sstvGeneralModulation" style="width: 100%; padding: 6px 8px; font-family: var(--font-mono); font-size: 11px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||||
<option value="usb">USB (Upper Sideband)</option>
|
||||
<option value="lsb">LSB (Lower Sideband)</option>
|
||||
<option value="fm">FM (Frequency Modulation)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Resources</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||||
<a href="https://www.sigidwiki.com/wiki/Slow-Scan_Television_(SSTV)" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
|
||||
SigID Wiki - SSTV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>About Terrestrial SSTV</h3>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim);">
|
||||
Amateur radio operators transmit SSTV images on HF bands worldwide.
|
||||
The most popular frequency is 14.230 MHz USB on the 20m band.
|
||||
</p>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-top: 8px;">
|
||||
Common modes: PD120, PD180, Martin1, Scottie1, Robot36
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,6 +71,8 @@
|
||||
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||
{{ mode_item('meshtastic', 'Meshtastic', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
{{ mode_item('dmr', 'Digital Voice', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>') }}
|
||||
{{ mode_item('websdr', 'WebSDR', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +118,7 @@
|
||||
{{ mode_item('satellite', 'Satellite', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>', '/satellite/dashboard') }}
|
||||
{% endif %}
|
||||
{{ mode_item('sstv', 'ISS SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg>') }}
|
||||
{{ mode_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,9 +185,12 @@
|
||||
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>', '/satellite/dashboard') }}
|
||||
{% endif %}
|
||||
{{ mobile_item('sstv', 'SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
|
||||
{{ mobile_item('sstv_general', 'HF SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
|
||||
{{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||
{{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
{{ mobile_item('dmr', 'DMR', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>') }}
|
||||
{{ mobile_item('websdr', 'WebSDR', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>') }}
|
||||
</nav>
|
||||
|
||||
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||
|
||||
Reference in New Issue
Block a user