diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2c177..c532525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ All notable changes to iNTERCEPT will be documented in this file. +## [2.12.0] - 2026-01-29 + +### Added +- **ISS SSTV Decoder Mode** - Receive Slow Scan Television transmissions from the ISS + - Real-time ISS tracking globe with accurate position via N2YO API + - Leaflet world map showing ISS ground track and current position + - Location settings for ISS pass predictions + - Integration with satellite tracking TLE data +- **GitHub Update Notifications** - Automatic new version alerts + - Checks for updates on app startup + - Unobtrusive notification when new releases are available + - Configurable check interval via settings +- **Meshtastic Enhancements** + - QR code support for easy device sharing + - Telemetry display with battery, voltage, and environmental data + - Traceroute visualization for mesh network topology + - Improved node synchronization between map and top bar +- **UI Improvements** + - New Space category for satellite and ISS-related modes + - Pulsating ring effect for tracked aircraft/vessels + - Map marker highlighting for selected aircraft in ADS-B + - Consolidated settings and dependencies into single modal +- **Auto-Update TLE Data** - Satellite tracking data updates automatically on app startup +- **GPS Auto-Connect** - AIS dashboard now connects to gpsd automatically + +### Changed +- **Utility Meters** - Added device grouping by ID with consumption trends +- **Utility Meters** - Device intelligence and manufacturer information display + +### Fixed +- **SoapySDR** - Module detection on macOS with Homebrew +- **dump1090** - Build failures in Docker containers +- **dump1090** - Build failures on Kali Linux and newer GCC versions +- **Flask** - Ensure Flask 3.0+ compatibility in setup script +- **psycopg2** - Now optional for Flask/Werkzeug compatibility +- **Bias-T** - Setting now properly passed to ADS-B and AIS dashboards +- **Dark Mode Maps** - Removed CSS filter that was inverting dark tiles +- **Map Tiles** - Fixed CARTO tile URLs and added cache-busting +- **Meshtastic** - Traceroute button and dark mode map fixes +- **ADS-B Dashboard** - Height adjustment to prevent bottom controls cutoff +- **Audio Visualizer** - Now works without spectrum canvas + +--- + ## [2.11.0] - 2026-01-28 ### Added diff --git a/config.py b/config.py index 6406039..21e54fe 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,20 @@ import os import sys # Application version -VERSION = "2.11.0" +VERSION = "2.12.0" # Changelog - latest release notes (shown on welcome screen) CHANGELOG = [ + { + "version": "2.12.0", + "date": "January 2026", + "highlights": [ + "ISS SSTV decoder with real-time ISS tracking globe", + "GitHub update notifications for new releases", + "Meshtastic QR code support and telemetry display", + "New Space category with reorganized UI", + ] + }, { "version": "2.11.0", "date": "January 2026", @@ -61,16 +71,6 @@ CHANGELOG = [ "Risk scoring and threat classification", ] }, - { - "version": "2.7.0", - "date": "November 2025", - "highlights": [ - "Multi-SDR hardware support via SoapySDR", - "LimeSDR, HackRF, Airspy, SDRplay support", - "Improved aircraft database with photo lookup", - "GPS auto-detection and integration", - ] - }, ] diff --git a/docs/index.html b/docs/index.html index 6733fbc..ccdadc3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -35,7 +35,7 @@
- 12+ + 15+ Modes
@@ -142,6 +142,12 @@

Meshtastic

LoRa mesh network integration. Connect to Meshtastic devices for decentralized, long-range communication monitoring.

+ +
+
🖼️
+

ISS SSTV

+

Receive Slow Scan Television from the ISS. Real-time tracking globe, pass predictions, and image decoding.

+
diff --git a/pyproject.toml b/pyproject.toml index 88a331d..7dc672d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "intercept" -version = "2.10.0" +version = "2.12.0" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" readme = "README.md" requires-python = ">=3.9" diff --git a/setup.sh b/setup.sh index 954c65b..6c9acf2 100755 --- a/setup.sh +++ b/setup.sh @@ -1016,3 +1016,4 @@ main() { } main "$@" +exit 0 diff --git a/static/css/modes/sstv.css b/static/css/modes/sstv.css index c2a336f..c6f3b10 100644 --- a/static/css/modes/sstv.css +++ b/static/css/modes/sstv.css @@ -16,8 +16,8 @@ .sstv-visuals-container { display: flex; flex-direction: column; - gap: 16px; - padding: 16px; + gap: 12px; + padding: 12px; min-height: 0; flex: 1; height: 100%; @@ -30,10 +30,9 @@ .sstv-main-row { display: flex; flex-direction: row; - gap: 16px; - flex: 1 1 auto; - min-height: 400px; - height: 100%; + gap: 12px; + flex: 1; + min-height: 0; overflow: hidden; } @@ -44,11 +43,12 @@ display: flex; align-items: center; gap: 12px; - padding: 10px 16px; + padding: 8px 14px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; flex-wrap: wrap; + flex-shrink: 0; } .sstv-strip-group { @@ -253,7 +253,7 @@ align-items: center; justify-content: center; padding: 16px; - min-height: 300px; + min-height: 0; } .sstv-canvas-container { @@ -448,11 +448,23 @@ margin-bottom: 12px; } +/* ============================================ + TOP ROW (Map + Countdown) + ============================================ */ +.sstv-top-row { + display: flex; + gap: 12px; + height: 220px; + flex-shrink: 0; +} + /* ============================================ ISS MAP ROW ============================================ */ .sstv-map-row { - margin-bottom: 16px; + flex: 1.5; + min-width: 0; + height: 100%; } .sstv-map-container { @@ -461,11 +473,12 @@ border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; + height: 100%; } .sstv-iss-map { width: 100%; - height: 200px; + height: 100%; background: #0a1628; } @@ -571,6 +584,158 @@ border: none; } +/* ============================================ + COUNTDOWN PANEL + ============================================ */ +.sstv-countdown-panel { + flex: 1; + min-width: 280px; + max-width: 380px; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; +} + +.sstv-countdown-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid var(--border-color); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} + +.sstv-countdown-header svg { + color: var(--accent-cyan); +} + +.sstv-countdown-body { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 12px; + gap: 10px; +} + +.sstv-countdown-timer { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.sstv-countdown-value { + font-family: 'JetBrains Mono', monospace; + font-size: 28px; + font-weight: 700; + color: var(--accent-cyan); + letter-spacing: 2px; + text-shadow: 0 0 20px rgba(0, 212, 255, 0.3); +} + +.sstv-countdown-value.imminent { + color: var(--accent-green); + text-shadow: 0 0 20px rgba(0, 255, 136, 0.4); + animation: countdown-pulse 1s ease-in-out infinite; +} + +.sstv-countdown-value.active { + color: var(--accent-yellow); + text-shadow: 0 0 20px rgba(255, 204, 0, 0.4); + animation: countdown-pulse 0.5s ease-in-out infinite; +} + +@keyframes countdown-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.sstv-countdown-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +.sstv-countdown-details { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px 12px; + width: 100%; + padding: 10px; + background: var(--bg-secondary); + border-radius: 6px; +} + +.sstv-countdown-detail { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.sstv-detail-label { + font-family: 'JetBrains Mono', monospace; + font-size: 8px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.sstv-detail-value { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} + +.sstv-countdown-status { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 14px; + background: rgba(0, 0, 0, 0.15); + border-top: 1px solid var(--border-color); + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; +} + +.sstv-countdown-status .sstv-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--text-dim); +} + +.sstv-countdown-status.has-pass .sstv-status-dot { + background: var(--accent-cyan); +} + +.sstv-countdown-status.imminent .sstv-status-dot { + background: var(--accent-green); + animation: pulse 1s infinite; +} + +.sstv-countdown-status.active .sstv-status-dot { + background: var(--accent-yellow); + box-shadow: 0 0 8px var(--accent-yellow); + animation: pulse 0.5s infinite; +} + /* ============================================ IMAGE MODAL ============================================ */ @@ -636,6 +801,26 @@ } @media (max-width: 1024px) { + .sstv-top-row { + flex-direction: column; + height: auto; + } + + .sstv-map-row { + flex: none; + height: 180px; + } + + .sstv-countdown-panel { + min-width: auto; + max-width: none; + height: auto; + } + + .sstv-countdown-value { + font-size: 24px; + } + .sstv-iss-map { height: 180px; } diff --git a/static/js/modes/sstv.js b/static/js/modes/sstv.js index 9515ef1..cdcb3e8 100644 --- a/static/js/modes/sstv.js +++ b/static/js/modes/sstv.js @@ -15,6 +15,8 @@ const SSTV = (function() { let issTrackLine = null; let issPosition = null; let issUpdateInterval = null; + let countdownInterval = null; + let nextPassData = null; // ISS frequency const ISS_FREQ = 145.800; @@ -29,6 +31,7 @@ const SSTV = (function() { loadIssSchedule(); initMap(); startIssTracking(); + startCountdown(); } /** @@ -208,6 +211,150 @@ const SSTV = (function() { } } + /** + * Start countdown timer + */ + function startCountdown() { + if (countdownInterval) clearInterval(countdownInterval); + countdownInterval = setInterval(updateCountdown, 1000); + updateCountdown(); + } + + /** + * Stop countdown timer + */ + function stopCountdown() { + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + } + + /** + * Update countdown display + */ + function updateCountdown() { + const valueEl = document.getElementById('sstvCountdownValue'); + const labelEl = document.getElementById('sstvCountdownLabel'); + const statusEl = document.getElementById('sstvCountdownStatus'); + + if (!nextPassData || !nextPassData.startTimestamp) { + if (valueEl) { + valueEl.textContent = '--:--:--'; + valueEl.className = 'sstv-countdown-value'; + } + if (labelEl) { + const hasLocation = localStorage.getItem('observerLat') !== null; + labelEl.textContent = hasLocation ? 'No passes in 48h' : 'Set location'; + } + if (statusEl) { + statusEl.className = 'sstv-countdown-status'; + statusEl.innerHTML = 'Waiting for pass data...'; + } + return; + } + + const now = Date.now(); + const startTime = nextPassData.startTimestamp; + const endTime = nextPassData.endTimestamp || (startTime + (nextPassData.durationMinutes || 10) * 60 * 1000); + const diff = startTime - now; + + if (now >= startTime && now < endTime) { + // Pass is currently active + const remaining = endTime - now; + const mins = Math.floor(remaining / 60000); + const secs = Math.floor((remaining % 60000) / 1000); + + if (valueEl) { + valueEl.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + valueEl.className = 'sstv-countdown-value active'; + } + if (labelEl) labelEl.textContent = 'Pass in progress!'; + if (statusEl) { + statusEl.className = 'sstv-countdown-status active'; + statusEl.innerHTML = 'ISS overhead now!'; + } + } else if (diff > 0) { + // Countdown to next pass + const hours = Math.floor(diff / 3600000); + const mins = Math.floor((diff % 3600000) / 60000); + const secs = Math.floor((diff % 60000) / 1000); + + if (valueEl) { + if (hours > 0) { + valueEl.textContent = `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } else { + valueEl.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + // Highlight when pass is imminent (< 5 minutes) + if (diff < 300000) { + valueEl.className = 'sstv-countdown-value imminent'; + } else { + valueEl.className = 'sstv-countdown-value'; + } + } + + if (labelEl) { + if (diff < 60000) { + labelEl.textContent = 'Starting soon!'; + } else if (diff < 300000) { + labelEl.textContent = 'Get ready!'; + } else if (diff < 3600000) { + labelEl.textContent = 'Until next pass'; + } else { + labelEl.textContent = 'Until next pass'; + } + } + + if (statusEl) { + if (diff < 300000) { + statusEl.className = 'sstv-countdown-status imminent'; + statusEl.innerHTML = 'Pass imminent!'; + } else { + statusEl.className = 'sstv-countdown-status has-pass'; + statusEl.innerHTML = 'Next pass scheduled'; + } + } + } else { + // Pass has ended, need to refresh schedule + loadIssSchedule(); + } + } + + /** + * Update countdown panel details + */ + function updateCountdownDetails(pass) { + const startEl = document.getElementById('sstvPassStart'); + const maxElEl = document.getElementById('sstvPassMaxEl'); + const durationEl = document.getElementById('sstvPassDuration'); + const directionEl = document.getElementById('sstvPassDirection'); + + if (!pass) { + if (startEl) startEl.textContent = '--:--'; + if (maxElEl) maxElEl.textContent = '--°'; + if (durationEl) durationEl.textContent = '-- min'; + if (directionEl) directionEl.textContent = '--'; + return; + } + + if (startEl) startEl.textContent = pass.startTime || '--:--'; + if (maxElEl) maxElEl.textContent = (pass.maxEl || '--') + '°'; + if (durationEl) durationEl.textContent = (pass.duration || '--') + ' min'; + if (directionEl) directionEl.textContent = pass.direction || (pass.azStart ? getDirection(pass.azStart) : '--'); + } + + /** + * Get compass direction from azimuth + */ + function getDirection(azimuth) { + if (azimuth === undefined || azimuth === null) return '--'; + const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; + const index = Math.round(azimuth / 22.5) % 16; + return directions[index]; + } + /** * Fetch current ISS position */ @@ -620,31 +767,110 @@ const SSTV = (function() { const data = await response.json(); if (data.status === 'ok' && data.passes && data.passes.length > 0) { - renderIssInfo(data.passes[0], hasLocation); + const pass = data.passes[0]; + // Parse the pass data to get timestamps + nextPassData = parsePassData(pass); + updateCountdownDetails(pass); + updateCountdown(); } else { - renderIssInfo(null, hasLocation); + nextPassData = null; + updateCountdownDetails(null); + updateCountdown(); } } catch (err) { console.error('Failed to load ISS schedule:', err); - renderIssInfo(null, hasLocation); + nextPassData = null; + updateCountdownDetails(null); + updateCountdown(); } } /** - * Render ISS pass info + * Parse pass data to extract timestamps */ - function renderIssInfo(nextPass, hasLocation = true) { - const passEl = document.getElementById('sstvNextPass'); - if (!passEl) return; + function parsePassData(pass) { + if (!pass) return null; - if (!nextPass) { - passEl.textContent = hasLocation - ? 'No passes in 48h' - : 'Set location above'; - return; + let startTimestamp = null; + let endTimestamp = null; + const durationMinutes = parseInt(pass.duration) || 10; + + // Try to parse the startTime + if (pass.startTimestamp) { + // If timestamp is provided directly + startTimestamp = pass.startTimestamp; + } else if (pass.startTime) { + // Parse time string (format: "HH:MM" or "HH:MM:SS" or with date) + startTimestamp = parseTimeString(pass.startTime, pass.date); } - passEl.textContent = `${nextPass.startTime} (${nextPass.maxEl}° el, ${nextPass.duration}min)`; + if (startTimestamp) { + endTimestamp = startTimestamp + durationMinutes * 60 * 1000; + } + + return { + startTimestamp, + endTimestamp, + durationMinutes, + maxEl: pass.maxEl, + azStart: pass.azStart + }; + } + + /** + * Parse time string to timestamp + */ + function parseTimeString(timeStr, dateStr) { + if (!timeStr) return null; + + // Try to parse as a full datetime string first (e.g., "2026-01-30 03:01 UTC") + // Remove UTC suffix for parsing + const cleanedStr = timeStr.replace(' UTC', '').replace('UTC', ''); + + // Try full datetime parse + let parsed = new Date(cleanedStr); + if (!isNaN(parsed.getTime())) { + return parsed.getTime(); + } + + // Try with T separator (ISO format) + parsed = new Date(cleanedStr.replace(' ', 'T')); + if (!isNaN(parsed.getTime())) { + return parsed.getTime(); + } + + // Fallback: parse as time only (HH:MM or HH:MM:SS) + const now = new Date(); + let targetDate = new Date(); + + // If a date string is provided + if (dateStr) { + const parsedDate = new Date(dateStr); + if (!isNaN(parsedDate)) { + targetDate = parsedDate; + } + } + + // Parse time (HH:MM or HH:MM:SS format) + const timeParts = cleanedStr.split(':'); + if (timeParts.length >= 2) { + const hours = parseInt(timeParts[0]); + const minutes = parseInt(timeParts[1]); + const seconds = timeParts.length > 2 ? parseInt(timeParts[2]) : 0; + + if (!isNaN(hours) && !isNaN(minutes)) { + targetDate.setHours(hours, minutes, seconds, 0); + + // If the time is in the past, assume it's tomorrow + if (targetDate.getTime() < now.getTime() && !dateStr) { + targetDate.setDate(targetDate.getDate() + 1); + } + + return targetDate.getTime(); + } + } + + return null; } /** @@ -723,7 +949,8 @@ const SSTV = (function() { closeImage, useGPS, updateTLE, - stopIssTracking + stopIssTracking, + stopCountdown }; })(); diff --git a/templates/index.html b/templates/index.html index 50a6354..6097b45 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1783,20 +1783,58 @@ - -
-
-
-
-
- ISS - --.-°, --.-° - Alt: --- km + +
+ +
+
+
+
+
+ ISS + --.-°, --.-° + Alt: --- km +
-
- Next Pass: - Loading... +
+
+ + +
+
+ + + + + Next Pass +
+
+
+ --:--:-- + Set location
+
+
+ Start + --:-- +
+
+ Max El + --° +
+
+ Duration + -- min +
+
+ Direction + -- +
+
+
+
+ + Waiting for pass data...