Add welcome page with changelog and mode selection

- Replace simple splash screen with comprehensive welcome page
- Show version number and latest changelog entries
- Add 9-button mode selection grid for direct navigation
- User can now choose which mode to start with
- Responsive layout adapts to mobile screens
- Flow: Welcome → Disclaimer (if needed) → Selected mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-14 17:26:23 +00:00
parent 30126b1709
commit 7b49c95967
4 changed files with 387 additions and 149 deletions

4
app.py
View File

@@ -25,7 +25,7 @@ from typing import Any
from flask import Flask, render_template, jsonify, send_file, Response, request
from config import VERSION
from config import VERSION, CHANGELOG
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from utils.process import cleanup_stale_processes
from utils.sdr import SDRFactory
@@ -164,7 +164,7 @@ def index() -> str:
'rtl_433': check_tool('rtl_433')
}
devices = [d.to_dict() for d in SDRFactory.detect_devices()]
return render_template('index.html', tools=tools, devices=devices, version=VERSION)
return render_template('index.html', tools=tools, devices=devices, version=VERSION, changelog=CHANGELOG)
@app.route('/favicon.svg')

View File

@@ -9,6 +9,40 @@ import sys
# Application version
VERSION = "2.9.0"
# Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [
{
"version": "2.9.0",
"date": "January 2026",
"highlights": [
"New dropdown navigation menus for cleaner UI",
"TSCM baseline recording now captures device data",
"Device identity engine integration for threat detection",
"Welcome screen with mode selection",
]
},
{
"version": "2.8.0",
"date": "December 2025",
"highlights": [
"Added TSCM counter-surveillance mode",
"WiFi/Bluetooth device correlation engine",
"Tracker detection (AirTag, Tile, SmartTag)",
"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",
]
},
]
def _get_env(key: str, default: str) -> str:
"""Get environment variable with default."""

View File

@@ -83,10 +83,10 @@ body {
}
/* ============================================
LANDING PAGE / SPLASH SCREEN
WELCOME PAGE
============================================ */
.landing-overlay {
.welcome-overlay {
position: fixed;
top: 0;
left: 0;
@@ -100,7 +100,7 @@ body {
overflow: hidden;
}
.landing-overlay::before {
.welcome-overlay::before {
content: '';
position: absolute;
top: 0;
@@ -113,13 +113,14 @@ body {
pointer-events: none;
}
.landing-content {
text-align: center;
.welcome-container {
width: 90%;
max-width: 900px;
z-index: 1;
animation: landingFadeIn 1s ease-out;
animation: welcomeFadeIn 0.8s ease-out;
}
@keyframes landingFadeIn {
@keyframes welcomeFadeIn {
from {
opacity: 0;
transform: translateY(20px);
@@ -130,46 +131,44 @@ body {
}
}
.landing-logo {
/* Welcome Header */
.welcome-header {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.welcome-logo {
animation: logoPulse 3s ease-in-out infinite;
}
@keyframes logoPulse {
0%, 100% {
filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.3));
filter: drop-shadow(0 0 15px rgba(0, 212, 255, 0.3));
}
50% {
filter: drop-shadow(0 0 40px rgba(0, 212, 255, 0.6));
filter: drop-shadow(0 0 30px rgba(0, 212, 255, 0.6));
}
}
.landing-logo .signal-wave {
.welcome-logo .signal-wave {
animation: signalPulse 2s ease-in-out infinite;
}
.landing-logo .signal-wave-1 {
animation-delay: 0s;
}
.landing-logo .signal-wave-2 {
animation-delay: 0.2s;
}
.landing-logo .signal-wave-3 {
animation-delay: 0.4s;
}
.welcome-logo .signal-wave-1 { animation-delay: 0s; }
.welcome-logo .signal-wave-2 { animation-delay: 0.2s; }
.welcome-logo .signal-wave-3 { animation-delay: 0.4s; }
@keyframes signalPulse {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.landing-logo .logo-dot {
.welcome-logo .logo-dot {
animation: dotPulse 1.5s ease-in-out infinite;
}
@@ -184,119 +183,239 @@ body {
}
}
.landing-title {
.welcome-title-block {
text-align: left;
}
.welcome-title {
font-family: 'JetBrains Mono', monospace;
font-size: 4rem;
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: 0.3em;
margin: 0 0 10px 0;
text-shadow: 0 0 30px rgba(0, 212, 255, 0.3);
}
.landing-tagline {
font-family: 'JetBrains Mono', monospace;
font-size: 1.2rem;
color: #00d4ff;
letter-spacing: 0.2em;
margin: 0 0 8px 0;
opacity: 0.9;
margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.landing-subtitle {
font-family: 'Inter', sans-serif;
.welcome-tagline {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--text-secondary);
color: var(--accent-cyan);
letter-spacing: 0.15em;
margin: 4px 0 0 0;
}
.welcome-version {
display: inline-block;
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
color: var(--bg-primary);
background: var(--accent-cyan);
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.05em;
margin-top: 8px;
}
/* Welcome Content Grid */
.welcome-content {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 30px;
margin-bottom: 20px;
}
.welcome-content h2 {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
margin: 0 0 40px 0;
letter-spacing: 0.15em;
margin: 0 0 15px 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.landing-enter-btn {
background: transparent;
border: 2px solid #00d4ff;
color: #00d4ff;
padding: 15px 50px;
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
letter-spacing: 0.2em;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
/* Changelog Section */
.welcome-changelog {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
max-height: 320px;
overflow-y: auto;
}
.changelog-release {
margin-bottom: 20px;
}
.changelog-release:last-child {
margin-bottom: 0;
}
.changelog-version-header {
display: flex;
align-items: center;
gap: 15px;
position: relative;
overflow: hidden;
gap: 10px;
margin-bottom: 10px;
}
.landing-enter-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.landing-enter-btn:hover::before {
left: 100%;
}
.landing-enter-btn:hover {
background: rgba(0, 212, 255, 0.1);
box-shadow: 0 0 30px rgba(0, 212, 255, 0.3), inset 0 0 20px rgba(0, 212, 255, 0.1);
transform: scale(1.02);
}
.landing-enter-btn .btn-icon {
transition: transform 0.3s ease;
}
.landing-enter-btn:hover .btn-icon {
transform: translateX(5px);
}
.landing-version {
.changelog-version {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--accent-cyan);
font-weight: 600;
}
.changelog-date {
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
color: var(--text-dim);
margin-top: 30px;
letter-spacing: 0.1em;
}
.landing-scanline {
.changelog-list {
margin: 0;
padding-left: 18px;
list-style: none;
}
.changelog-list li {
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 6px;
position: relative;
}
.changelog-list li::before {
content: '>';
position: absolute;
left: -15px;
color: var(--accent-green);
font-family: 'JetBrains Mono', monospace;
}
/* Mode Selection Grid */
.welcome-modes {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.mode-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.mode-card:hover {
background: var(--bg-elevated);
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.15);
}
.mode-card:active {
transform: translateY(0);
}
.mode-card .mode-icon {
font-size: 1.5rem;
margin-bottom: 6px;
}
.mode-card .mode-name {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mode-card .mode-desc {
font-family: 'Inter', sans-serif;
font-size: 0.65rem;
color: var(--text-dim);
margin-top: 4px;
}
/* Welcome Footer */
.welcome-footer {
text-align: center;
padding-top: 15px;
border-top: 1px solid var(--border-color);
}
.welcome-footer p {
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
color: var(--text-dim);
letter-spacing: 0.1em;
text-transform: uppercase;
margin: 0;
}
/* Welcome Scanline */
.welcome-scanline {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
animation: scanlineMove 4s linear infinite;
opacity: 0.5;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
animation: scanlineMove 5s linear infinite;
opacity: 0.4;
}
@keyframes scanlineMove {
0% {
top: 0;
}
100% {
top: 100%;
}
0% { top: 0; }
100% { top: 100%; }
}
.landing-overlay.fade-out {
animation: landingFadeOut 0.5s ease-in forwards;
/* Welcome Fade Out */
.welcome-overlay.fade-out {
animation: welcomeFadeOut 0.4s ease-in forwards;
}
@keyframes landingFadeOut {
from {
opacity: 1;
@keyframes welcomeFadeOut {
from { opacity: 1; }
to { opacity: 0; visibility: hidden; }
}
/* Responsive */
@media (max-width: 768px) {
.welcome-content {
grid-template-columns: 1fr;
}
to {
opacity: 0;
visibility: hidden;
.welcome-header {
flex-direction: column;
text-align: center;
}
.welcome-title-block {
text-align: center;
}
.mode-grid {
grid-template-columns: repeat(2, 1fr);
}
}

View File

@@ -17,36 +17,111 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
</head>
<body>
<!-- Landing Page / Splash Screen -->
<div class="landing-overlay" id="landingPage">
<div class="landing-content">
<div class="landing-logo">
<svg width="200" height="200" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Signal brackets - left side -->
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<!-- Signal brackets - right side -->
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<!-- The 'i' letter -->
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot"/>
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff"/>
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff"/>
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff"/>
</svg>
<!-- Welcome Page -->
<div class="welcome-overlay" id="welcomePage">
<div class="welcome-container">
<!-- Header Section -->
<div class="welcome-header">
<div class="welcome-logo">
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1"/>
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2"/>
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" class="signal-wave signal-wave-3"/>
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot"/>
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff"/>
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff"/>
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff"/>
</svg>
</div>
<div class="welcome-title-block">
<h1 class="welcome-title">iNTERCEPT</h1>
<p class="welcome-tagline">// See the Invisible</p>
<span class="welcome-version">v{{ version }}</span>
</div>
</div>
<!-- Main Content Grid -->
<div class="welcome-content">
<!-- Left: Changelog -->
<div class="welcome-changelog">
<h2>What's New</h2>
{% for release in changelog[:2] %}
<div class="changelog-release">
<div class="changelog-version-header">
<span class="changelog-version">v{{ release.version }}</span>
<span class="changelog-date">{{ release.date }}</span>
</div>
<ul class="changelog-list">
{% for item in release.highlights %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<!-- Right: Mode Selection -->
<div class="welcome-modes">
<h2>Select Mode</h2>
<div class="mode-grid">
<button class="mode-card" onclick="selectMode('pager')">
<span class="mode-icon">📟</span>
<span class="mode-name">Pager</span>
<span class="mode-desc">POCSAG/FLEX decoding</span>
</button>
<button class="mode-card" onclick="selectMode('sensor')">
<span class="mode-icon">📡</span>
<span class="mode-name">433MHz</span>
<span class="mode-desc">IoT sensor monitoring</span>
</button>
<button class="mode-card" onclick="selectMode('aircraft')">
<span class="mode-icon">✈️</span>
<span class="mode-name">Aircraft</span>
<span class="mode-desc">ADS-B tracking</span>
</button>
<button class="mode-card" onclick="selectMode('wifi')">
<span class="mode-icon">📶</span>
<span class="mode-name">WiFi</span>
<span class="mode-desc">Network reconnaissance</span>
</button>
<button class="mode-card" onclick="selectMode('bluetooth')">
<span class="mode-icon">🔵</span>
<span class="mode-name">Bluetooth</span>
<span class="mode-desc">Device discovery</span>
</button>
<button class="mode-card" onclick="selectMode('tscm')">
<span class="mode-icon">🔍</span>
<span class="mode-name">TSCM</span>
<span class="mode-desc">Counter-surveillance</span>
</button>
<button class="mode-card" onclick="selectMode('satellite')">
<span class="mode-icon">🛰️</span>
<span class="mode-name">Satellite</span>
<span class="mode-desc">Pass prediction</span>
</button>
<button class="mode-card" onclick="selectMode('listening')">
<span class="mode-icon">📻</span>
<span class="mode-name">Scanner</span>
<span class="mode-desc">Frequency scanning</span>
</button>
<button class="mode-card" onclick="selectMode('aprs')">
<span class="mode-icon">📍</span>
<span class="mode-name">APRS</span>
<span class="mode-desc">Amateur radio</span>
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="welcome-footer">
<p>Signal Intelligence & Counter Surveillance Platform</p>
</div>
<h1 class="landing-title">iNTERCEPT</h1>
<p class="landing-tagline">// See the Invisible</p>
<p class="landing-subtitle">Signal Intelligence & Counter Surveillance Platform</p>
<button class="landing-enter-btn" onclick="enterApp()">
<span class="btn-text">ENTER</span>
<span class="btn-icon"></span>
</button>
<p class="landing-version">v{{ version }}</p>
</div>
<div class="landing-scanline"></div>
<div class="welcome-scanline"></div>
</div>
<!-- Disclaimer Modal -->
@@ -2831,16 +2906,20 @@
<script src="{{ url_for('static', filename='js/modes/listening-post.js') }}"></script>
<script>
// Landing page enter function
function enterApp() {
const landing = document.getElementById('landingPage');
landing.classList.add('fade-out');
// Selected mode from welcome screen
let selectedStartMode = 'pager';
// After fade out, hide landing and show disclaimer if needed
// Mode selection from welcome page
function selectMode(mode) {
selectedStartMode = mode;
const welcome = document.getElementById('welcomePage');
welcome.classList.add('fade-out');
// After fade out, hide welcome and show disclaimer if needed
setTimeout(() => {
landing.style.display = 'none';
welcome.style.display = 'none';
checkDisclaimer();
}, 500);
}, 400);
}
// Disclaimer handling
@@ -2848,6 +2927,8 @@
const accepted = localStorage.getItem('disclaimerAccepted');
if (accepted === 'true') {
document.getElementById('disclaimerModal').style.display = 'none';
// Switch to the selected mode
switchMode(selectedStartMode);
} else {
document.getElementById('disclaimerModal').style.display = 'flex';
}
@@ -2856,6 +2937,10 @@
function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
// Switch to the selected mode after accepting
setTimeout(() => {
switchMode(selectedStartMode);
}, 300);
}
function declineDisclaimer() {
@@ -2863,7 +2948,7 @@
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
}
// Don't auto-check disclaimer - wait for landing page enter
// Don't auto-check disclaimer - wait for welcome page mode selection
let eventSource = null;
let isRunning = false;