Compare commits

..

54 Commits

Author SHA1 Message Date
Smittix 24332a4e23 Release v2.13.1 - Help modal and navigation improvements
- Add help modal system with keyboard shortcuts reference
- Add Main Dashboard button in navigation bar
- Make settings modal accessible from all dashboards
- Dashboard CSS improvements and consistency fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:05:07 +00:00
Smittix ebc5754684 Update version in pyproject.toml to 2.13.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 17:48:09 +00:00
Smittix 340b300aa4 Release v2.13.0 - WiFi client display in AP detail drawer
Features:
- Display connected clients for access points in detail drawer
- Real-time client updates via SSE streaming
- Client cards show MAC, vendor, RSSI, probed SSIDs, and last seen
- Count badge in Connected Clients header

Other changes:
- Updated aircraft database
- CSS and template refinements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 17:44:23 +00:00
Smittix bf7026cc9f Merge branch 'codex/new-ui'
# Conflicts:
#	static/css/index.css
2026-02-04 15:11:24 +00:00
Smittix 1b04b52509 Sync scanner range from backend updates 2026-02-04 13:25:14 +00:00
Smittix fca334f472 Sync scanner range with backend config 2026-02-04 13:14:42 +00:00
Smittix d81d644319 Prefer progress data for scanner sweep 2026-02-04 13:11:02 +00:00
Smittix 400cf1114f Use frequency-based sweep display 2026-02-04 12:48:40 +00:00
Smittix fec38adc78 Stabilize scanner progress tracking 2026-02-04 12:42:30 +00:00
Smittix 993a7d2626 Stabilize sweep display and lower SNR default 2026-02-04 12:30:13 +00:00
Smittix dbe09411ac Stabilize sweep progress updates 2026-02-04 12:20:38 +00:00
Smittix 0afc47fcdd Ignore out-of-order scan updates 2026-02-04 12:17:36 +00:00
Smittix 4862b285a8 Order sweep updates to avoid progress jitter 2026-02-04 12:14:46 +00:00
Smittix 41dd1555d7 Emit sweep progress and clear scanner queue 2026-02-04 12:11:50 +00:00
Smittix 0cf3a25ac6 Ensure scanner releases SDR before listening 2026-02-04 12:07:30 +00:00
Smittix 3674b6e2d6 Stop rtl_power when starting listen 2026-02-04 12:04:50 +00:00
Smittix 4c9bcb00c3 Improve rtl_power line parsing 2026-02-04 12:03:01 +00:00
Smittix 2067d0bf84 Default squelch to zero and track SDR usage 2026-02-04 11:59:06 +00:00
Smittix c0fa59d10e Add SNR threshold control for power scan 2026-02-04 11:54:56 +00:00
Smittix 37add84d59 Switch scanner to rtl_power sweep 2026-02-04 11:52:39 +00:00
Smittix c23019b8c0 Advance scanner after dwell on signal 2026-02-04 11:44:19 +00:00
Smittix b4edd35f5f Tighten listening signal detection thresholds 2026-02-04 11:41:30 +00:00
Smittix 812f85b9a9 Log only interesting listening signals 2026-02-04 11:37:15 +00:00
Smittix 77888b7d88 Align scanner audio stream start 2026-02-04 11:27:10 +00:00
Smittix 4a38d7512d Align listening action button styles 2026-02-04 11:23:32 +00:00
Smittix 5d0df18dac Silence listen slow-start log 2026-02-04 11:19:44 +00:00
Smittix d18e38800e Retry listen playback without fallback 2026-02-04 11:12:46 +00:00
Smittix 76e595aaec Prompt user to enable audio playback 2026-02-04 11:10:34 +00:00
Smittix dfb9897fa1 Trigger user-initiated audio play on listen 2026-02-04 11:09:04 +00:00
Smittix 82ad784fcb Restart audio pipeline for fresh stream header 2026-02-04 11:04:43 +00:00
Smittix 4bd7077d64 Add listening audio probe diagnostics 2026-02-04 11:02:00 +00:00
Smittix 3f6b9cc5ef Force squelch open for listen audio 2026-02-04 11:00:20 +00:00
Smittix 0742647571 Stream listening audio as WAV 2026-02-04 10:56:57 +00:00
Smittix 33090419df Timeout audio stream if no first chunk 2026-02-04 10:53:03 +00:00
Smittix 4042d0e5f1 Allow listening audio endpoints without login 2026-02-04 10:46:49 +00:00
Smittix d3a0b41fba Flush ffmpeg audio stream packets 2026-02-04 10:06:45 +00:00
Smittix 2fefea5618 Add listening audio debug endpoint 2026-02-04 10:03:47 +00:00
Smittix d75f7c794f Retry listening audio stream fetch 2026-02-04 10:01:58 +00:00
Smittix 503b91ea87 Add fetch stream fallback for listening audio 2026-02-04 09:49:14 +00:00
Smittix 43db7c309d Add WebSocket audio fallback for listening 2026-02-04 09:46:34 +00:00
Smittix 6e57927409 Force audio stream load on listen 2026-02-04 09:39:47 +00:00
Smittix a404f5ded9 Send SDR settings for listening audio 2026-02-04 09:31:07 +00:00
Smittix f6a6aab623 Update URL on mode switch 2026-02-04 09:26:29 +00:00
Smittix 2cfbc0addc Apply JetBrains Mono tokens to standalone pages 2026-02-04 01:15:18 +00:00
Smittix 07d6ef984e Switch app font to JetBrains Mono 2026-02-04 01:10:42 +00:00
Smittix 50227ccae6 Use Terminus font across app 2026-02-04 00:56:22 +00:00
Smittix 8f3c636c61 Fix mode query routing from dashboard nav 2026-02-04 00:49:54 +00:00
Smittix 42761bbdbc Add global nav dropdown behavior 2026-02-04 00:47:05 +00:00
Smittix 0f2eba302c Add global nav styles 2026-02-04 00:45:00 +00:00
Smittix 83dd58721f Wire global navbar across pages 2026-02-04 00:37:41 +00:00
Smittix d658d0b81e Refine UI to clean professional style 2026-02-04 00:21:52 +00:00
Smittix e04113628a Fix dual scrollbar issue on main dashboard
Add overflow: hidden to html and body elements to prevent browser
window scrollbar while keeping internal content areas scrollable.

Fixes #119

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 00:10:47 +00:00
Smittix b1e92326b6 Fix multiple UI bugs and improve error handling
Issues fixed:
- #113: Display RTL-SDR serial numbers in device selector
- #112: Kill all processes now stops Bluetooth scans
- #111: BLE device list no longer overflows container bounds
- #109: WiFi scanner panels maintain minimum width (no more "imploding")
- #108: Radar device hover no longer causes violent shaking
- #106: "Use GPS" button now uses gpsd for USB GPS devices
- #105: Meter trend text no longer overlaps adjacent columns
- #104: dump1090 errors now provide specific troubleshooting guidance

Changes:
- app.py: Add Bluetooth cleanup to /killall endpoint
- routes/adsb.py: Parse dump1090 stderr for specific error messages
- templates/index.html: Show SDR serial numbers in device dropdown
- static/css/index.css: Fix WiFi/BT panel layouts with proper min-width
- static/css/components/signal-cards.css: Fix meter grid overflow
- static/css/components/proximity-viz.css: Fix radar hover transform
- static/css/settings.css: Add GPS detection spinner
- static/js/components/proximity-radar.js: Add invisible hit areas
- static/js/core/settings-manager.js: Use gpsd before browser geolocation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 21:45:40 +00:00
Smittix 9ac63bd75f Add application restart endpoint for post-update restarts
Adds POST /updater/restart endpoint that gracefully restarts the
application using os.execv. Cleans up all decoder processes and
global state before replacing the process with a fresh instance.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 22:28:32 +00:00
68 changed files with 29599 additions and 22974 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"version": "2026-01-11_fae1348c", "version": "2026-02-01_ba81b697",
"downloaded": "2026-01-12T15:55:42.769654Z" "downloaded": "2026-02-04T17:06:54.806043Z"
} }
+32 -6
View File
@@ -278,9 +278,13 @@ def get_sdr_device_status() -> dict[int, str]:
# ============================================ # ============================================
@app.before_request @app.before_request
def require_login(): def require_login():
# Routes that don't require login (to avoid infinite redirect loop) # Routes that don't require login (to avoid infinite redirect loop)
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check'] allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']
# Allow audio streaming endpoints without session auth
if request.path.startswith('/listening/audio/'):
return None
# Controller API endpoints use API key auth, not session auth # Controller API endpoints use API key auth, not session auth
# Allow agent push/pull endpoints without session login # Allow agent push/pull endpoints without session login
@@ -645,19 +649,21 @@ def health_check() -> Response:
@app.route('/killall', methods=['POST']) @app.route('/killall', methods=['POST'])
def kill_all() -> Response: def kill_all() -> Response:
"""Kill all decoder and WiFi processes.""" """Kill all decoder, WiFi, and Bluetooth processes."""
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
# Import adsb and ais modules to reset their state # Import adsb and ais modules to reset their state
from routes import adsb as adsb_module from routes import adsb as adsb_module
from routes import ais as ais_module from routes import ais as ais_module
from utils.bluetooth import reset_bluetooth_scanner
killed = [] killed = []
processes_to_kill = [ processes_to_kill = [
'rtl_fm', 'multimon-ng', 'rtl_433', 'rtl_fm', 'multimon-ng', 'rtl_433',
'airodump-ng', 'aireplay-ng', 'airmon-ng', 'airodump-ng', 'aireplay-ng', 'airmon-ng',
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher' 'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
'hcitool', 'bluetoothctl'
] ]
for proc in processes_to_kill: for proc in processes_to_kill:
@@ -701,6 +707,26 @@ def kill_all() -> Response:
dsc_process = None dsc_process = None
dsc_rtl_process = None dsc_rtl_process = None
# Reset Bluetooth state (legacy)
with bt_lock:
if bt_process:
try:
bt_process.terminate()
bt_process.wait(timeout=2)
except Exception:
try:
bt_process.kill()
except Exception:
pass
bt_process = None
# Reset Bluetooth v2 scanner
try:
reset_bluetooth_scanner()
killed.append('bluetooth_scanner')
except Exception:
pass
# Clear SDR device registry # Clear SDR device registry
with sdr_device_registry_lock: with sdr_device_registry_lock:
sdr_device_registry.clear() sdr_device_registry.clear()
+20 -1
View File
@@ -7,10 +7,29 @@ import os
import sys import sys
# Application version # Application version
VERSION = "2.12.1" VERSION = "2.13.1"
# Changelog - latest release notes (shown on welcome screen) # Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [ CHANGELOG = [
{
"version": "2.13.1",
"date": "February 2026",
"highlights": [
"Help modal system with keyboard shortcuts reference",
"Main Dashboard button in navigation bar",
"Settings modal accessible from all dashboards",
"Dashboard CSS improvements and consistency fixes",
]
},
{
"version": "2.13.0",
"date": "February 2026",
"highlights": [
"WiFi client display in AP detail drawer",
"Real-time client updates via SSE streaming",
"Probed SSID badges for connected clients",
]
},
{ {
"version": "2.12.1", "version": "2.12.1",
"date": "February 2026", "date": "February 2026",
+608
View File
@@ -0,0 +1,608 @@
# iNTERCEPT UI Guide
This guide documents the UI design system, components, and patterns used in iNTERCEPT.
## Table of Contents
1. [Design Tokens](#design-tokens)
2. [Base Templates](#base-templates)
3. [Navigation](#navigation)
4. [Components](#components)
5. [Adding a New Module Page](#adding-a-new-module-page)
6. [Adding a New Dashboard](#adding-a-new-dashboard)
---
## Design Tokens
All design tokens are defined in `static/css/core/variables.css`. Import this file first in any stylesheet.
### Colors
```css
/* Backgrounds (layered depth) */
--bg-primary: #0a0c10; /* Darkest - page background */
--bg-secondary: #0f1218; /* Panels, sidebars */
--bg-tertiary: #151a23; /* Cards, elevated elements */
--bg-card: #121620; /* Card backgrounds */
--bg-elevated: #1a202c; /* Hover states, modals */
/* Accent Colors */
--accent-cyan: #4a9eff; /* Primary action color */
--accent-green: #22c55e; /* Success, online status */
--accent-red: #ef4444; /* Error, danger, stop */
--accent-orange: #f59e0b; /* Warning */
--accent-amber: #d4a853; /* Secondary highlight */
/* Text Hierarchy */
--text-primary: #e8eaed; /* Main content */
--text-secondary: #9ca3af; /* Secondary content */
--text-dim: #4b5563; /* Disabled, placeholder */
--text-muted: #374151; /* Barely visible */
/* Status Colors */
--status-online: #22c55e;
--status-warning: #f59e0b;
--status-error: #ef4444;
--status-offline: #6b7280;
```
### Spacing Scale
```css
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
```
### Typography
```css
/* Font Families */
--font-sans: 'Inter', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Font Sizes */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 20px;
--text-3xl: 24px;
--text-4xl: 30px;
```
### Border Radius
```css
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
```
### Light Theme
The design system supports light/dark themes via `data-theme` attribute:
```html
<html data-theme="dark"> <!-- or "light" -->
```
Toggle with JavaScript:
```javascript
document.documentElement.setAttribute('data-theme', 'light');
```
---
## Base Templates
### `templates/layout/base.html`
The main base template for standard pages. Use for pages with sidebar + content layout.
```html
{% extends 'layout/base.html' %}
{% block title %}My Page Title{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my-page.css') }}">
{% endblock %}
{% block navigation %}
{% set active_mode = 'mymode' %}
{% include 'partials/nav.html' %}
{% endblock %}
{% block sidebar %}
<div class="app-sidebar">
<!-- Sidebar content -->
</div>
{% endblock %}
{% block content %}
<div class="page-container">
<h1>Page Title</h1>
<!-- Page content -->
</div>
{% endblock %}
{% block scripts %}
<script>
// Page-specific JavaScript
</script>
{% endblock %}
```
### `templates/layout/base_dashboard.html`
Extended base for full-screen dashboards (maps, visualizations).
```html
{% extends 'layout/base_dashboard.html' %}
{% set active_mode = 'mydashboard' %}
{% block dashboard_title %}MY DASHBOARD{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_dashboard.css') }}">
{% endblock %}
{% block stats_strip %}
<div class="stats-strip">
<!-- Stats bar content -->
</div>
{% endblock %}
{% block dashboard_content %}
<div class="dashboard-map-container">
<!-- Main visualization -->
</div>
<div class="dashboard-sidebar">
<!-- Sidebar panels -->
</div>
{% endblock %}
```
---
## Navigation
### Including Navigation
```html
{% set active_mode = 'pager' %}
{% include 'partials/nav.html' %}
```
### Valid `active_mode` Values
| Mode | Description |
|------|-------------|
| `pager` | Pager decoding |
| `sensor` | 433MHz sensors |
| `rtlamr` | Utility meters |
| `adsb` | Aircraft tracking |
| `ais` | Vessel tracking |
| `aprs` | Amateur radio |
| `wifi` | WiFi scanning |
| `bluetooth` | Bluetooth scanning |
| `tscm` | Counter-surveillance |
| `satellite` | Satellite tracking |
| `sstv` | ISS SSTV |
| `listening` | Listening post |
| `spystations` | Spy stations |
| `meshtastic` | Mesh networking |
### Navigation Groups
The navigation is organized into groups:
- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic
- **Wireless**: WiFi, Bluetooth
- **Security**: TSCM
- **Space**: Satellite, ISS SSTV
---
## Components
### Card / Panel
```html
{% call card(title='PANEL TITLE', indicator=true, indicator_active=false) %}
<p>Panel content here</p>
{% endcall %}
```
Or manually:
```html
<div class="panel">
<div class="panel-header">
<span>PANEL TITLE</span>
<div class="panel-indicator active"></div>
</div>
<div class="panel-content">
<p>Content here</p>
</div>
</div>
```
### Empty State
```html
{% include 'components/empty_state.html' with context %}
{# Or with variables: #}
{% with title='No data yet', description='Start scanning to see results', action_text='Start Scan', action_onclick='startScan()' %}
{% include 'components/empty_state.html' %}
{% endwith %}
```
### Loading State
```html
{# Inline spinner #}
{% include 'components/loading.html' %}
{# With text #}
{% with text='Loading data...', size='lg' %}
{% include 'components/loading.html' %}
{% endwith %}
{# Full overlay #}
{% with overlay=true, text='Please wait...' %}
{% include 'components/loading.html' %}
{% endwith %}
```
### Status Badge
```html
{% with status='online', text='Connected', id='connectionStatus' %}
{% include 'components/status_badge.html' %}
{% endwith %}
```
Status values: `online`, `offline`, `warning`, `error`, `inactive`
### Buttons
```html
<!-- Primary action -->
<button class="btn btn-primary">Start Tracking</button>
<!-- Secondary action -->
<button class="btn btn-secondary">Cancel</button>
<!-- Danger action -->
<button class="btn btn-danger">Stop</button>
<!-- Ghost/subtle -->
<button class="btn btn-ghost">Settings</button>
<!-- Sizes -->
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-lg">Large</button>
<!-- Icon button -->
<button class="btn btn-icon btn-secondary">
<span class="icon">...</span>
</button>
```
### Badges
```html
<span class="badge">Default</span>
<span class="badge badge-primary">Primary</span>
<span class="badge badge-success">Online</span>
<span class="badge badge-warning">Warning</span>
<span class="badge badge-danger">Error</span>
```
### Form Groups
```html
<div class="form-group">
<label for="frequency">Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350">
<span class="form-help">Enter frequency in MHz</span>
</div>
<div class="form-group">
<label for="gain">Gain</label>
<select id="gain">
<option value="auto">Auto</option>
<option value="30">30 dB</option>
</select>
</div>
<label class="form-check">
<input type="checkbox" id="alerts">
<span>Enable alerts</span>
</label>
```
### Stats Strip
Used in dashboards for horizontal statistics display:
```html
<div class="stats-strip">
<div class="stats-strip-inner">
<div class="strip-stat">
<span class="strip-value" id="count">0</span>
<span class="strip-label">COUNT</span>
</div>
<div class="strip-divider"></div>
<div class="strip-status">
<div class="status-dot active" id="statusDot"></div>
<span id="statusText">TRACKING</span>
</div>
<div class="strip-time" id="utcTime">--:--:-- UTC</div>
</div>
</div>
```
---
## Adding a New Module Page
### 1. Create the Route
In `routes/mymodule.py`:
```python
from flask import Blueprint, render_template
mymodule_bp = Blueprint('mymodule', __name__, url_prefix='/mymodule')
@mymodule_bp.route('/dashboard')
def dashboard():
return render_template('mymodule_dashboard.html',
offline_settings=get_offline_settings())
```
### 2. Register the Blueprint
In `routes/__init__.py`:
```python
from routes.mymodule import mymodule_bp
app.register_blueprint(mymodule_bp)
```
### 3. Create the Template
Option A: Simple page extending base.html
```html
{% extends 'layout/base.html' %}
{% set active_mode = 'mymodule' %}
{% block title %}My Module{% endblock %}
{% block navigation %}
{% include 'partials/nav.html' %}
{% endblock %}
{% block content %}
<!-- Your content -->
{% endblock %}
```
Option B: Full-screen dashboard
```html
{% extends 'layout/base_dashboard.html' %}
{% set active_mode = 'mymodule' %}
{% block dashboard_title %}MY MODULE{% endblock %}
{% block dashboard_content %}
<!-- Your dashboard content -->
{% endblock %}
```
### 4. Add to Navigation
In `templates/partials/nav.html`, add your module to the appropriate group:
```html
<button class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
onclick="switchMode('mymodule')">
<span class="nav-icon icon"><!-- SVG icon --></span>
<span class="nav-label">My Module</span>
</button>
```
Or if it's a dashboard link:
```html
<a href="/mymodule/dashboard"
class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
style="text-decoration: none;">
<span class="nav-icon icon"><!-- SVG icon --></span>
<span class="nav-label">My Module</span>
</a>
```
### 5. Create Stylesheet
In `static/css/mymodule.css`:
```css
/**
* My Module Styles
*/
@import url('./core/variables.css');
/* Your styles using design tokens */
.mymodule-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-4);
}
```
---
## Adding a New Dashboard
For full-screen dashboards like ADSB, AIS, or Satellite:
### 1. Create the Template
```html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MY DASHBOARD // iNTERCEPT</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<!-- Design tokens (required) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<!-- Fonts -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
{% endif %}
<!-- External libraries if needed -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Dashboard styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/mydashboard.css') }}">
</head>
<body>
<!-- Background effects -->
<div class="radar-bg"></div>
<div class="scanline"></div>
<!-- Header -->
<header class="header">
<div class="logo">
<a href="/" style="color: inherit; text-decoration: none;">
MY DASHBOARD
<span>// iNTERCEPT</span>
</a>
</div>
<div class="status-bar">
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
<a href="/" class="back-link">Main Dashboard</a>
</div>
</header>
<!-- Unified Navigation -->
{% set active_mode = 'mydashboard' %}
{% include 'partials/nav.html' %}
<!-- Stats Strip -->
<div class="stats-strip">
<!-- Stats content -->
</div>
<!-- Main Dashboard Content -->
<main class="dashboard">
<!-- Your dashboard layout -->
</main>
<script>
// Dashboard JavaScript
</script>
</body>
</html>
```
### 2. Create the Stylesheet
```css
/**
* My Dashboard Styles
*/
@import url('./core/variables.css');
:root {
/* Dashboard-specific aliases */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--bg-card: var(--bg-tertiary);
--grid-line: rgba(74, 158, 255, 0.08);
}
/* Your dashboard styles */
```
---
## Best Practices
### DO
- Use design tokens for all colors, spacing, and typography
- Include the nav partial on all pages for consistent navigation
- Set `active_mode` before including the nav partial
- Use semantic component classes (`btn`, `panel`, `badge`, etc.)
- Support both light and dark themes
- Test on mobile viewports
### DON'T
- Hardcode color values - use CSS variables
- Create new color variations without adding to tokens
- Duplicate navigation markup - use the partial
- Skip the favicon and design tokens imports
- Use inline styles for layout (use utility classes)
---
## File Structure
```
templates/
├── layout/
│ ├── base.html # Standard page base
│ └── base_dashboard.html # Dashboard page base
├── partials/
│ ├── nav.html # Unified navigation
│ ├── page_header.html # Page title component
│ └── settings-modal.html # Settings modal
├── components/
│ ├── card.html # Panel/card component
│ ├── empty_state.html # Empty state placeholder
│ ├── loading.html # Loading spinner
│ ├── stats_strip.html # Stats bar component
│ └── status_badge.html # Status indicator
├── index.html # Main dashboard
├── adsb_dashboard.html # Aircraft tracking
├── ais_dashboard.html # Vessel tracking
└── satellite_dashboard.html # Satellite tracking
static/css/
├── core/
│ ├── variables.css # Design tokens
│ ├── base.css # Reset & typography
│ ├── components.css # Component styles
│ └── layout.css # Layout styles
├── index.css # Main dashboard styles
├── adsb_dashboard.css # Aircraft dashboard
├── ais_dashboard.css # Vessel dashboard
├── satellite_dashboard.css # Satellite dashboard
└── responsive.css # Responsive breakpoints
```
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "intercept" name = "intercept"
version = "2.12.1" version = "2.13.1"
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
+36 -9
View File
@@ -732,16 +732,43 @@ def start_adsb():
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip() stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
except Exception: except Exception:
pass pass
if sdr_type == SDRType.RTL_SDR:
error_msg = 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.' # Parse stderr to provide specific guidance
if stderr_output: error_type = 'START_FAILED'
error_msg += f' Error: {stderr_output[:200]}' stderr_lower = stderr_output.lower()
return jsonify({'status': 'error', 'message': error_msg})
if 'usb_claim_interface' in stderr_lower or 'libusb_error_busy' in stderr_lower or 'device or resource busy' in stderr_lower:
error_msg = 'SDR device is busy. Another process may be using it.'
suggestion = 'Try: 1) Stop other SDR applications, 2) Run "pkill -f rtl_" to kill stale processes, or 3) Remove and reinsert the SDR device.'
error_type = 'DEVICE_BUSY'
elif 'no supported devices' in stderr_lower or 'no rtl-sdr' in stderr_lower or 'failed to open' in stderr_lower:
error_msg = 'RTL-SDR device not found.'
suggestion = 'Ensure the device is connected. Try removing and reinserting the SDR.'
error_type = 'DEVICE_NOT_FOUND'
elif 'kernel driver is active' in stderr_lower or 'dvb' in stderr_lower:
error_msg = 'Kernel DVB-T driver is blocking the device.'
suggestion = 'Blacklist the DVB drivers: Go to Settings > Hardware > "Blacklist DVB Drivers" or run "sudo rmmod dvb_usb_rtl28xxu".'
error_type = 'KERNEL_DRIVER'
elif 'permission' in stderr_lower or 'access' in stderr_lower:
error_msg = 'Permission denied accessing RTL-SDR device.'
suggestion = 'Run Intercept with sudo, or add udev rules for RTL-SDR devices.'
error_type = 'PERMISSION_DENIED'
elif sdr_type == SDRType.RTL_SDR:
error_msg = 'dump1090 failed to start.'
suggestion = 'Try removing and reinserting the SDR device, or check if another application is using it.'
else: else:
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.' error_msg = f'ADS-B decoder failed to start for {sdr_type.value}.'
if stderr_output: suggestion = 'Ensure readsb is installed with SoapySDR support and the device is connected.'
error_msg += f' Error: {stderr_output[:200]}'
return jsonify({'status': 'error', 'message': error_msg}) full_msg = f'{error_msg} {suggestion}'
if stderr_output and len(stderr_output) < 300:
full_msg += f' (Details: {stderr_output})'
return jsonify({
'status': 'error',
'error_type': error_type,
'message': full_msg
})
adsb_using_service = True adsb_using_service = True
adsb_active_device = device # Track which device is being used adsb_active_device = device # Track which device is being used
+7 -3
View File
@@ -228,9 +228,13 @@ def init_audio_websocket(app: Flask):
except TimeoutError: except TimeoutError:
pass pass
except Exception as e: except Exception as e:
if "timed out" not in str(e).lower(): msg = str(e).lower()
logger.error(f"WebSocket receive error: {e}") if "connection closed" in msg:
logger.info("WebSocket closed by client")
break
if "timed out" not in msg:
logger.error(f"WebSocket receive error: {e}")
# Stream audio data if active # Stream audio data if active
if streaming and proc and proc.poll() is None: if streaming and proc and proc.poll() is None:
+720 -257
View File
File diff suppressed because it is too large Load Diff
+42 -2
View File
@@ -2,14 +2,15 @@
from __future__ import annotations from __future__ import annotations
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.logging import get_logger from utils.logging import get_logger
from utils.updater import ( from utils.updater import (
check_for_updates, check_for_updates,
get_update_status,
dismiss_update, dismiss_update,
get_update_status,
perform_update, perform_update,
restart_application,
) )
logger = get_logger('intercept.routes.updater') logger = get_logger('intercept.routes.updater')
@@ -137,3 +138,42 @@ def dismiss_notification() -> Response:
'success': False, 'success': False,
'error': str(e) 'error': str(e)
}), 500 }), 500
@updater_bp.route('/restart', methods=['POST'])
def restart_app() -> Response:
"""
Restart the application.
This endpoint triggers a graceful restart of the application:
1. Stops all running decoder processes
2. Cleans up global state
3. Replaces the current process with a fresh instance
The response may not be received by the client since the process
is replaced immediately. Clients should poll /health until the
server responds again.
Returns:
JSON with restart status (may not be delivered)
"""
import threading
logger.info("Restart requested via API")
# Send response before restarting
# Use a short delay to allow the response to be sent
def delayed_restart():
import time
time.sleep(0.5) # Allow response to be sent
restart_application()
# Start restart in a background thread so we can return a response
restart_thread = threading.Thread(target=delayed_restart, daemon=False)
restart_thread.start()
return jsonify({
'success': True,
'message': 'Application is restarting. Please wait...',
'action': 'restart'
})
+3
View File
@@ -1092,4 +1092,7 @@ main() {
} }
main "$@" main "$@"
# Clear traps before exiting to prevent spurious errors during cleanup
trap - ERR EXIT
exit 0 exit 0
+66 -33
View File
@@ -5,6 +5,8 @@
} }
:root { :root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10; --bg-dark: #0a0c10;
--bg-panel: #0f1218; --bg-panel: #0f1218;
--bg-card: #151a23; --bg-card: #151a23;
@@ -25,7 +27,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--font-sans);
background: var(--bg-dark); background: var(--bg-dark);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
@@ -71,7 +73,7 @@ body {
} }
} }
/* Header - Mobile first */ /* Header */
.header { .header {
position: relative; position: relative;
z-index: 10; z-index: 10;
@@ -81,20 +83,19 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 8px; gap: 12px;
min-height: 52px; min-height: 52px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.header { .header {
padding: 12px 20px; padding: 12px 20px;
flex-wrap: nowrap;
} }
} }
.logo { .logo {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
letter-spacing: 2px; letter-spacing: 2px;
@@ -126,14 +127,52 @@ body {
letter-spacing: 2px; letter-spacing: 2px;
} }
} }
}
.status-bar { .status-bar {
display: flex; display: flex;
gap: 20px; gap: 12px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
flex-wrap: nowrap;
}
.agent-selector-compact {
display: flex;
align-items: center;
gap: 8px;
}
.agent-selector-compact .agent-select-sm {
padding: 4px 8px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 11px;
font-family: var(--font-mono);
}
.agent-selector-compact .agent-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 6px var(--accent-green);
}
.agent-selector-compact .agent-status-dot.offline {
background: var(--accent-red);
box-shadow: 0 0 6px var(--accent-red);
}
.agent-selector-compact .show-all-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-secondary);
cursor: pointer;
} }
.status-dot { .status-dot {
@@ -172,15 +211,15 @@ body {
} }
/* Main dashboard grid - Mobile first */ /* Main dashboard grid - Mobile first */
/* Header ~55px + Stats strip ~55px = ~110px, using 115px for safety */ /* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
.dashboard { .dashboard {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
height: calc(100dvh - 115px); height: calc(100dvh - 160px);
height: calc(100vh - 115px); /* Fallback */ height: calc(100vh - 160px); /* Fallback */
min-height: 400px; min-height: 400px;
} }
@@ -216,7 +255,7 @@ body {
@media (min-width: 1024px) { @media (min-width: 1024px) {
.acars-sidebar { .acars-sidebar {
display: flex; display: flex;
max-height: calc(100dvh - 115px); max-height: calc(100dvh - 160px);
} }
} }
@@ -624,7 +663,7 @@ body {
} }
.telemetry-value { .telemetry-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
@@ -680,7 +719,7 @@ body {
} }
.aircraft-icao { .aircraft-icao {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
color: var(--text-secondary); color: var(--text-secondary);
background: rgba(74, 158, 255, 0.1); background: rgba(74, 158, 255, 0.1);
@@ -700,7 +739,7 @@ body {
} }
.aircraft-detail-value { .aircraft-detail-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
color: var(--accent-cyan); color: var(--accent-cyan);
font-size: 11px; font-size: 11px;
} }
@@ -790,7 +829,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3); border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
} }
@@ -801,7 +840,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3); border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
} }
@@ -879,7 +918,7 @@ body {
border: none; border: none;
background: var(--accent-green); background: var(--accent-green);
color: #fff; color: #fff;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -911,7 +950,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3); border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
cursor: pointer; cursor: pointer;
} }
@@ -1023,7 +1062,7 @@ body {
cursor: pointer; cursor: pointer;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
@@ -1057,7 +1096,7 @@ body {
} }
.airband-status { .airband-status {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
padding: 0 8px; padding: 0 8px;
color: var(--text-muted); color: var(--text-muted);
@@ -1217,7 +1256,7 @@ body {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
height: auto !important; height: auto !important;
min-height: calc(100dvh - 115px); min-height: calc(100dvh - 160px);
overflow-y: auto !important; overflow-y: auto !important;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@@ -1286,12 +1325,6 @@ body {
padding: 6px 8px; padding: 6px 8px;
} }
/* Status bar - compact on mobile */
.status-bar {
flex-wrap: wrap;
gap: 6px;
}
/* Strip time smaller on mobile */ /* Strip time smaller on mobile */
.strip-time { .strip-time {
font-size: 10px; font-size: 10px;
@@ -1407,7 +1440,7 @@ body {
} }
.strip-value { .strip-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -1545,7 +1578,7 @@ body {
.report-grid span:nth-child(even) { .report-grid span:nth-child(even) {
color: var(--text-primary); color: var(--text-primary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.report-highlights { .report-highlights {
@@ -1784,7 +1817,7 @@ body {
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
padding-left: 8px; padding-left: 8px;
border-left: 1px solid rgba(74, 158, 255, 0.2); border-left: 1px solid rgba(74, 158, 255, 0.2);
white-space: nowrap; white-space: nowrap;
@@ -1938,7 +1971,7 @@ body {
} }
.squawk-code { .squawk-code {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-weight: 700; font-weight: 700;
color: var(--accent-cyan); color: var(--accent-cyan);
font-size: 12px; font-size: 12px;
+8 -6
View File
@@ -5,6 +5,8 @@
} }
:root { :root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10; --bg-dark: #0a0c10;
--bg-panel: #0f1218; --bg-panel: #0f1218;
--bg-card: #141a24; --bg-card: #141a24;
@@ -20,14 +22,14 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--font-sans);
background: var(--bg-dark); background: var(--bg-dark);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
} }
.mono { .mono {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.radar-bg { .radar-bg {
@@ -91,7 +93,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -268,7 +270,7 @@ body {
} }
.status-pill { .status-pill {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 999px; border-radius: 999px;
@@ -306,7 +308,7 @@ body {
} }
.panel-meta { .panel-meta {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
@@ -347,7 +349,7 @@ body {
} }
.mono { .mono {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.empty-row td, .empty-row td,
+331 -343
View File
@@ -1,343 +1,331 @@
/* /*
* Agents Management CSS * Agents Management CSS
* Styles for the remote agent management interface * Styles for the remote agent management interface
*/ * Inherits CSS variables from core/variables.css
*/
/* CSS Variables (inherited from main theme) */
:root { /* Agent indicator in navigation */
--bg-primary: #0a0a0f; .agent-indicator {
--bg-secondary: #12121a; display: flex;
--text-primary: #e0e0e0; align-items: center;
--text-secondary: #888; gap: 8px;
--border-color: #1a1a2e; padding: 6px 12px;
--accent-cyan: #00d4ff; background: rgba(0, 212, 255, 0.1);
--accent-green: #00ff88; border: 1px solid rgba(0, 212, 255, 0.3);
--accent-red: #ff3366; border-radius: 20px;
--accent-orange: #ff9f1c; cursor: pointer;
} transition: all 0.2s;
}
/* Agent indicator in navigation */
.agent-indicator { .agent-indicator:hover {
display: flex; background: rgba(0, 212, 255, 0.2);
align-items: center; border-color: var(--accent-cyan);
gap: 8px; }
padding: 6px 12px;
background: rgba(0, 212, 255, 0.1); .agent-indicator-dot {
border: 1px solid rgba(0, 212, 255, 0.3); width: 8px;
border-radius: 20px; height: 8px;
cursor: pointer; border-radius: 50%;
transition: all 0.2s; background: var(--accent-green);
} box-shadow: 0 0 6px var(--accent-green);
}
.agent-indicator:hover {
background: rgba(0, 212, 255, 0.2); .agent-indicator-dot.remote {
border-color: var(--accent-cyan); background: var(--accent-cyan);
} box-shadow: 0 0 6px var(--accent-cyan);
}
.agent-indicator-dot {
width: 8px; .agent-indicator-dot.multiple {
height: 8px; background: var(--accent-orange);
border-radius: 50%; box-shadow: 0 0 6px var(--accent-orange);
background: var(--accent-green); }
box-shadow: 0 0 6px var(--accent-green);
} .agent-indicator-label {
font-size: 11px;
.agent-indicator-dot.remote { color: var(--text-primary);
background: var(--accent-cyan); font-family: var(--font-mono);
box-shadow: 0 0 6px var(--accent-cyan); }
}
.agent-indicator-count {
.agent-indicator-dot.multiple { font-size: 10px;
background: var(--accent-orange); padding: 2px 6px;
box-shadow: 0 0 6px var(--accent-orange); background: rgba(0, 212, 255, 0.2);
} border-radius: 10px;
color: var(--accent-cyan);
.agent-indicator-label { }
font-size: 11px;
color: var(--text-primary); /* Agent selector dropdown */
font-family: 'JetBrains Mono', monospace; .agent-selector {
} position: relative;
}
.agent-indicator-count {
font-size: 10px; .agent-selector-dropdown {
padding: 2px 6px; position: absolute;
background: rgba(0, 212, 255, 0.2); top: 100%;
border-radius: 10px; right: 0;
color: var(--accent-cyan); margin-top: 8px;
} min-width: 280px;
background: var(--bg-secondary);
/* Agent selector dropdown */ border: 1px solid var(--border-color);
.agent-selector { border-radius: 8px;
position: relative; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
} z-index: 1000;
display: none;
.agent-selector-dropdown { }
position: absolute;
top: 100%; .agent-selector-dropdown.show {
right: 0; display: block;
margin-top: 8px; }
min-width: 280px;
background: var(--bg-secondary); .agent-selector-header {
border: 1px solid var(--border-color); display: flex;
border-radius: 8px; justify-content: space-between;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); align-items: center;
z-index: 1000; padding: 12px 15px;
display: none; border-bottom: 1px solid var(--border-color);
} }
.agent-selector-dropdown.show { .agent-selector-header h4 {
display: block; margin: 0;
} font-size: 12px;
color: var(--accent-cyan);
.agent-selector-header { text-transform: uppercase;
display: flex; letter-spacing: 1px;
justify-content: space-between; }
align-items: center;
padding: 12px 15px; .agent-selector-manage {
border-bottom: 1px solid var(--border-color); font-size: 11px;
} color: var(--accent-cyan);
text-decoration: none;
.agent-selector-header h4 { }
margin: 0;
font-size: 12px; .agent-selector-manage:hover {
color: var(--accent-cyan); text-decoration: underline;
text-transform: uppercase; }
letter-spacing: 1px;
} .agent-selector-list {
max-height: 300px;
.agent-selector-manage { overflow-y: auto;
font-size: 11px; }
color: var(--accent-cyan);
text-decoration: none; .agent-selector-item {
} display: flex;
align-items: center;
.agent-selector-manage:hover { gap: 10px;
text-decoration: underline; padding: 10px 15px;
} cursor: pointer;
transition: background 0.2s;
.agent-selector-list { border-bottom: 1px solid var(--border-color);
max-height: 300px; }
overflow-y: auto;
} .agent-selector-item:last-child {
border-bottom: none;
.agent-selector-item { }
display: flex;
align-items: center; .agent-selector-item:hover {
gap: 10px; background: rgba(0, 212, 255, 0.1);
padding: 10px 15px; }
cursor: pointer;
transition: background 0.2s; .agent-selector-item.selected {
border-bottom: 1px solid var(--border-color); background: rgba(0, 212, 255, 0.15);
} border-left: 3px solid var(--accent-cyan);
}
.agent-selector-item:last-child {
border-bottom: none; .agent-selector-item.local {
} border-left: 3px solid var(--accent-green);
}
.agent-selector-item:hover {
background: rgba(0, 212, 255, 0.1); .agent-selector-item-status {
} width: 8px;
height: 8px;
.agent-selector-item.selected { border-radius: 50%;
background: rgba(0, 212, 255, 0.15); flex-shrink: 0;
border-left: 3px solid var(--accent-cyan); }
}
.agent-selector-item-status.online {
.agent-selector-item.local { background: var(--accent-green);
border-left: 3px solid var(--accent-green); }
}
.agent-selector-item-status.offline {
.agent-selector-item-status { background: var(--accent-red);
width: 8px; }
height: 8px;
border-radius: 50%; .agent-selector-item-info {
flex-shrink: 0; flex: 1;
} min-width: 0;
}
.agent-selector-item-status.online {
background: var(--accent-green); .agent-selector-item-name {
} font-size: 13px;
color: var(--text-primary);
.agent-selector-item-status.offline { white-space: nowrap;
background: var(--accent-red); overflow: hidden;
} text-overflow: ellipsis;
}
.agent-selector-item-info {
flex: 1; .agent-selector-item-url {
min-width: 0; font-size: 10px;
} color: var(--text-secondary);
font-family: var(--font-mono);
.agent-selector-item-name { white-space: nowrap;
font-size: 13px; overflow: hidden;
color: var(--text-primary); text-overflow: ellipsis;
white-space: nowrap; }
overflow: hidden;
text-overflow: ellipsis; .agent-selector-item-check {
} color: var(--accent-green);
opacity: 0;
.agent-selector-item-url { }
font-size: 10px;
color: var(--text-secondary); .agent-selector-item.selected .agent-selector-item-check {
font-family: 'JetBrains Mono', monospace; opacity: 1;
white-space: nowrap; }
overflow: hidden;
text-overflow: ellipsis; /* Agent badge in data displays */
} .agent-badge {
display: inline-flex;
.agent-selector-item-check { align-items: center;
color: var(--accent-green); gap: 4px;
opacity: 0; padding: 2px 8px;
} font-size: 10px;
background: rgba(0, 212, 255, 0.1);
.agent-selector-item.selected .agent-selector-item-check { color: var(--accent-cyan);
opacity: 1; border-radius: 10px;
} font-family: var(--font-mono);
}
/* Agent badge in data displays */
.agent-badge { .agent-badge.local,
display: inline-flex; .agent-badge.agent-local {
align-items: center; background: rgba(0, 255, 136, 0.1);
gap: 4px; color: var(--accent-green);
padding: 2px 8px; }
font-size: 10px;
background: rgba(0, 212, 255, 0.1); .agent-badge.agent-remote {
color: var(--accent-cyan); background: rgba(0, 212, 255, 0.1);
border-radius: 10px; color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; }
}
/* WiFi table agent column */
.agent-badge.local, .wifi-networks-table .col-agent {
.agent-badge.agent-local { width: 100px;
background: rgba(0, 255, 136, 0.1); text-align: center;
color: var(--accent-green); }
}
.wifi-networks-table th.col-agent {
.agent-badge.agent-remote { font-size: 10px;
background: rgba(0, 212, 255, 0.1); }
color: var(--accent-cyan);
} /* Bluetooth table agent column */
.bt-devices-table .col-agent {
/* WiFi table agent column */ width: 100px;
.wifi-networks-table .col-agent { text-align: center;
width: 100px; }
text-align: center;
} .agent-badge-dot {
width: 6px;
.wifi-networks-table th.col-agent { height: 6px;
font-size: 10px; border-radius: 50%;
} background: currentColor;
}
/* Bluetooth table agent column */
.bt-devices-table .col-agent { /* Agent column in data tables */
width: 100px; .data-table .agent-col {
text-align: center; width: 120px;
} max-width: 120px;
}
.agent-badge-dot {
width: 6px; /* Multi-agent stream indicator */
height: 6px; .multi-agent-indicator {
border-radius: 50%; position: fixed;
background: currentColor; bottom: 20px;
} left: 20px;
display: flex;
/* Agent column in data tables */ align-items: center;
.data-table .agent-col { gap: 8px;
width: 120px; padding: 8px 12px;
max-width: 120px; background: var(--bg-secondary);
} border: 1px solid var(--border-color);
border-radius: 20px;
/* Multi-agent stream indicator */ font-size: 11px;
.multi-agent-indicator { color: var(--text-secondary);
position: fixed; z-index: 100;
bottom: 20px; }
left: 20px;
display: flex; .multi-agent-indicator.active {
align-items: center; border-color: var(--accent-cyan);
gap: 8px; color: var(--accent-cyan);
padding: 8px 12px; }
background: var(--bg-secondary);
border: 1px solid var(--border-color); .multi-agent-indicator-pulse {
border-radius: 20px; width: 8px;
font-size: 11px; height: 8px;
color: var(--text-secondary); border-radius: 50%;
z-index: 100; background: var(--accent-cyan);
} animation: pulse 2s infinite;
}
.multi-agent-indicator.active {
border-color: var(--accent-cyan); @keyframes pulse {
color: var(--accent-cyan); 0%, 100% { opacity: 1; transform: scale(1); }
} 50% { opacity: 0.5; transform: scale(0.8); }
}
.multi-agent-indicator-pulse {
width: 8px; /* Agent connection status toast */
height: 8px; .agent-toast {
border-radius: 50%; position: fixed;
background: var(--accent-cyan); top: 80px;
animation: pulse 2s infinite; right: 20px;
} padding: 10px 15px;
background: var(--bg-secondary);
@keyframes pulse { border: 1px solid var(--border-color);
0%, 100% { opacity: 1; transform: scale(1); } border-radius: 6px;
50% { opacity: 0.5; transform: scale(0.8); } font-size: 12px;
} z-index: 1001;
animation: slideInRight 0.3s ease;
/* Agent connection status toast */ }
.agent-toast {
position: fixed; .agent-toast.connected {
top: 80px; border-color: var(--accent-green);
right: 20px; color: var(--accent-green);
padding: 10px 15px; }
background: var(--bg-secondary);
border: 1px solid var(--border-color); .agent-toast.disconnected {
border-radius: 6px; border-color: var(--accent-red);
font-size: 12px; color: var(--accent-red);
z-index: 1001; }
animation: slideInRight 0.3s ease;
} @keyframes slideInRight {
from {
.agent-toast.connected { transform: translateX(100%);
border-color: var(--accent-green); opacity: 0;
color: var(--accent-green); }
} to {
transform: translateX(0);
.agent-toast.disconnected { opacity: 1;
border-color: var(--accent-red); }
color: var(--accent-red); }
}
/* Responsive adjustments */
@keyframes slideInRight { @media (max-width: 768px) {
from { .agent-indicator {
transform: translateX(100%); padding: 4px 8px;
opacity: 0; }
}
to { .agent-indicator-label {
transform: translateX(0); display: none;
opacity: 1; }
}
} .agent-selector-dropdown {
position: fixed;
/* Responsive adjustments */ top: auto;
@media (max-width: 768px) { bottom: 0;
.agent-indicator { left: 0;
padding: 4px 8px; right: 0;
} margin: 0;
border-radius: 16px 16px 0 0;
.agent-indicator-label { max-height: 60vh;
display: none; }
}
.agents-grid {
.agent-selector-dropdown { grid-template-columns: 1fr;
position: fixed; }
top: auto; }
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-radius: 16px 16px 0 0;
max-height: 60vh;
}
.agents-grid {
grid-template-columns: 1fr;
}
}
+71 -29
View File
@@ -8,6 +8,8 @@
} }
:root { :root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10; --bg-dark: #0a0c10;
--bg-panel: #0f1218; --bg-panel: #0f1218;
--bg-card: #151a23; --bg-card: #151a23;
@@ -28,7 +30,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--font-sans);
background: var(--bg-dark); background: var(--bg-dark);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
@@ -97,7 +99,7 @@ body {
} }
.logo { .logo {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
letter-spacing: 2px; letter-spacing: 2px;
@@ -132,10 +134,49 @@ body {
.status-bar { .status-bar {
display: flex; display: flex;
gap: 20px; gap: 12px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
flex-wrap: nowrap;
}
.agent-selector-compact {
display: flex;
align-items: center;
gap: 8px;
}
.agent-selector-compact .agent-select-sm {
padding: 4px 8px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 11px;
font-family: var(--font-mono);
}
.agent-selector-compact .agent-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 6px var(--accent-green);
}
.agent-selector-compact .agent-status-dot.offline {
background: var(--accent-red);
box-shadow: 0 0 6px var(--accent-red);
}
.agent-selector-compact .show-all-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-secondary);
cursor: pointer;
} }
.back-link { .back-link {
@@ -183,7 +224,7 @@ body {
} }
.strip-value { .strip-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -287,7 +328,7 @@ body {
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
padding-left: 8px; padding-left: 8px;
border-left: 1px solid rgba(74, 158, 255, 0.2); border-left: 1px solid rgba(74, 158, 255, 0.2);
white-space: nowrap; white-space: nowrap;
@@ -320,14 +361,15 @@ body {
} }
/* Main dashboard grid - Mobile first */ /* Main dashboard grid - Mobile first */
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
.dashboard { .dashboard {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
height: calc(100dvh - 95px); height: calc(100dvh - 160px);
height: calc(100vh - 95px); height: calc(100vh - 160px);
min-height: 400px; min-height: 400px;
} }
@@ -367,7 +409,7 @@ body {
/* Leaflet overrides - Dark map styling */ /* Leaflet overrides - Dark map styling */
.leaflet-container { .leaflet-container {
background: var(--bg-dark) !important; background: var(--bg-dark) !important;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Using actual dark tiles now - no filter needed */ /* Using actual dark tiles now - no filter needed */
@@ -438,7 +480,7 @@ body {
padding: 10px 15px; padding: 10px 15px;
background: rgba(74, 158, 255, 0.05); background: rgba(74, 158, 255, 0.05);
border-bottom: 1px solid rgba(74, 158, 255, 0.1); border-bottom: 1px solid rgba(74, 158, 255, 0.1);
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
letter-spacing: 2px; letter-spacing: 2px;
@@ -510,7 +552,7 @@ body {
} }
.vessel-name { .vessel-name {
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -518,7 +560,7 @@ body {
} }
.vessel-mmsi { .vessel-mmsi {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--text-secondary); color: var(--text-secondary);
background: rgba(74, 158, 255, 0.1); background: rgba(74, 158, 255, 0.1);
@@ -548,7 +590,7 @@ body {
} }
.detail-value { .detail-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
@@ -604,20 +646,20 @@ body {
} }
.vessel-item-name { .vessel-item-name {
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
.vessel-item-type { .vessel-item-type {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
color: var(--text-secondary); color: var(--text-secondary);
} }
.vessel-item-speed { .vessel-item-speed {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-cyan); color: var(--accent-cyan);
text-align: right; text-align: right;
@@ -687,7 +729,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3); border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
} }
@@ -698,7 +740,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3); border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
} }
@@ -717,7 +759,7 @@ body {
border: none; border: none;
background: var(--accent-green); background: var(--accent-green);
color: #fff; color: #fff;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -834,7 +876,7 @@ body {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
height: auto !important; height: auto !important;
min-height: calc(100dvh - 95px); min-height: calc(100dvh - 160px);
overflow-y: auto !important; overflow-y: auto !important;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@@ -1004,7 +1046,7 @@ body {
padding: 6px 12px; padding: 6px 12px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(245, 158, 11, 0.1); border-bottom: 1px solid rgba(245, 158, 11, 0.1);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
} }
@@ -1079,7 +1121,7 @@ body {
} }
.dsc-message-category { .dsc-message-category {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
@@ -1096,13 +1138,13 @@ body {
} }
.dsc-message-time { .dsc-message-time {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
color: var(--text-dim); color: var(--text-dim);
} }
.dsc-message-mmsi { .dsc-message-mmsi {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-orange); color: var(--accent-orange);
} }
@@ -1120,7 +1162,7 @@ body {
} }
.dsc-message-pos { .dsc-message-pos {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
color: var(--text-secondary); color: var(--text-secondary);
} }
@@ -1148,7 +1190,7 @@ body {
} }
.dsc-distress-alert .dsc-alert-header { .dsc-distress-alert .dsc-alert-header {
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
color: var(--accent-red); color: var(--accent-red);
@@ -1157,7 +1199,7 @@ body {
} }
.dsc-distress-alert .dsc-alert-mmsi { .dsc-distress-alert .dsc-alert-mmsi {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 16px; font-size: 16px;
color: var(--accent-cyan); color: var(--accent-cyan);
margin-bottom: 8px; margin-bottom: 8px;
@@ -1177,7 +1219,7 @@ body {
} }
.dsc-distress-alert .dsc-alert-position { .dsc-distress-alert .dsc-alert-position {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
color: var(--accent-cyan); color: var(--accent-cyan);
margin-bottom: 16px; margin-bottom: 16px;
@@ -1188,7 +1230,7 @@ body {
border: none; border: none;
color: white; color: white;
padding: 10px 24px; padding: 10px 24px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+371
View File
@@ -0,0 +1,371 @@
/* Function Strip (Action Bar) - Shared across modes
* Based on APRS strip pattern, reusable for Pager, Sensor, Bluetooth, WiFi, TSCM, etc.
*/
.function-strip {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 10px;
overflow: visible;
min-height: 44px;
}
.function-strip-inner {
display: flex;
align-items: center;
gap: 8px;
min-width: max-content;
}
/* Stats */
.function-strip .strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 10px;
background: rgba(74, 158, 255, 0.05);
border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px;
min-width: 55px;
}
.function-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3);
}
.function-strip .strip-value {
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan);
line-height: 1.2;
}
.function-strip .strip-label {
font-size: 8px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 1px;
}
.function-strip .strip-divider {
width: 1px;
height: 28px;
background: var(--border-color);
margin: 0 4px;
}
/* Signal stat coloring */
.function-strip .signal-stat.good .strip-value { color: var(--accent-green); }
.function-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); }
.function-strip .signal-stat.poor .strip-value { color: var(--accent-red); }
/* Controls */
.function-strip .strip-control {
display: flex;
align-items: center;
gap: 4px;
}
.function-strip .strip-select {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
cursor: pointer;
}
.function-strip .strip-select:hover {
border-color: var(--accent-cyan);
}
.function-strip .strip-select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.function-strip .strip-input-label {
font-size: 9px;
color: var(--text-muted);
font-weight: 600;
}
.function-strip .strip-input {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 4px 6px;
border-radius: 4px;
font-size: 10px;
width: 50px;
text-align: center;
}
.function-strip .strip-input:hover,
.function-strip .strip-input:focus {
border-color: var(--accent-cyan);
outline: none;
}
.function-strip .strip-input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Wider input for frequency values */
.function-strip .strip-input.wide {
width: 70px;
}
/* Tool Status Indicators */
.function-strip .strip-tools {
display: flex;
gap: 4px;
}
.function-strip .strip-tool {
font-size: 9px;
font-weight: 600;
padding: 3px 6px;
border-radius: 3px;
background: rgba(255, 59, 48, 0.2);
color: var(--accent-red);
border: 1px solid rgba(255, 59, 48, 0.3);
}
.function-strip .strip-tool.ok {
background: rgba(0, 255, 136, 0.1);
color: var(--accent-green);
border-color: rgba(0, 255, 136, 0.3);
}
.function-strip .strip-tool.warn {
background: rgba(255, 193, 7, 0.2);
color: var(--accent-yellow);
border-color: rgba(255, 193, 7, 0.3);
}
/* Buttons */
.function-strip .strip-btn {
background: rgba(74, 158, 255, 0.1);
border: 1px solid rgba(74, 158, 255, 0.2);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.function-strip .strip-btn:hover:not(:disabled) {
background: rgba(74, 158, 255, 0.2);
border-color: rgba(74, 158, 255, 0.4);
}
.function-strip .strip-btn.primary {
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
border: none;
color: #000;
}
.function-strip .strip-btn.primary:hover:not(:disabled) {
filter: brightness(1.1);
}
.function-strip .strip-btn.stop {
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
border: none;
color: #fff;
}
.function-strip .strip-btn.stop:hover:not(:disabled) {
filter: brightness(1.1);
}
.function-strip .strip-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Status indicator */
.function-strip .strip-status {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: rgba(0,0,0,0.2);
border-radius: 4px;
font-size: 10px;
font-weight: 600;
color: var(--text-secondary);
}
.function-strip .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.function-strip .status-dot.inactive {
background: var(--text-muted);
}
.function-strip .status-dot.active,
.function-strip .status-dot.scanning,
.function-strip .status-dot.decoding {
background: var(--accent-cyan);
animation: strip-pulse 1.5s ease-in-out infinite;
}
.function-strip .status-dot.listening,
.function-strip .status-dot.tracking,
.function-strip .status-dot.receiving {
background: var(--accent-green);
animation: strip-pulse 1.5s ease-in-out infinite;
}
.function-strip .status-dot.sweeping {
background: var(--accent-orange);
animation: strip-pulse 1s ease-in-out infinite;
}
.function-strip .status-dot.error {
background: var(--accent-red);
}
@keyframes strip-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
50% { opacity: 0.6; box-shadow: none; }
}
/* Time display */
.function-strip .strip-time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
padding: 4px 8px;
background: rgba(0,0,0,0.2);
border-radius: 4px;
white-space: nowrap;
}
/* Mode-specific accent colors */
.function-strip.pager-strip .strip-stat {
background: rgba(255, 193, 7, 0.05);
border-color: rgba(255, 193, 7, 0.15);
}
.function-strip.pager-strip .strip-stat:hover {
background: rgba(255, 193, 7, 0.1);
border-color: rgba(255, 193, 7, 0.3);
}
.function-strip.pager-strip .strip-value {
color: var(--accent-yellow);
}
.function-strip.sensor-strip .strip-stat {
background: rgba(0, 255, 136, 0.05);
border-color: rgba(0, 255, 136, 0.15);
}
.function-strip.sensor-strip .strip-stat:hover {
background: rgba(0, 255, 136, 0.1);
border-color: rgba(0, 255, 136, 0.3);
}
.function-strip.sensor-strip .strip-value {
color: var(--accent-green);
}
.function-strip.bt-strip .strip-stat {
background: rgba(0, 122, 255, 0.05);
border-color: rgba(0, 122, 255, 0.15);
}
.function-strip.bt-strip .strip-stat:hover {
background: rgba(0, 122, 255, 0.1);
border-color: rgba(0, 122, 255, 0.3);
}
.function-strip.bt-strip .strip-value {
color: #0a84ff;
}
.function-strip.wifi-strip .strip-stat {
background: rgba(255, 149, 0, 0.05);
border-color: rgba(255, 149, 0, 0.15);
}
.function-strip.wifi-strip .strip-stat:hover {
background: rgba(255, 149, 0, 0.1);
border-color: rgba(255, 149, 0, 0.3);
}
.function-strip.wifi-strip .strip-value {
color: var(--accent-orange);
}
.function-strip.tscm-strip {
margin-top: 4px; /* Extra clearance to prevent top clipping */
}
.function-strip.tscm-strip .strip-stat {
background: rgba(255, 59, 48, 0.15);
border: 1px solid rgba(255, 59, 48, 0.4);
}
.function-strip.tscm-strip .strip-stat:hover {
background: rgba(255, 59, 48, 0.25);
border-color: rgba(255, 59, 48, 0.6);
}
.function-strip.tscm-strip .strip-value {
color: #ef4444; /* Explicit red color */
}
.function-strip.tscm-strip .strip-label {
color: #9ca3af; /* Explicit light gray */
}
.function-strip.tscm-strip .strip-select {
color: #e8eaed; /* Explicit white for selects */
background: rgba(0, 0, 0, 0.4);
}
.function-strip.tscm-strip .strip-btn {
color: #e8eaed; /* Explicit white for buttons */
}
.function-strip.tscm-strip .strip-tool {
color: #e8eaed; /* Explicit white for tool indicators */
}
.function-strip.tscm-strip .strip-time,
.function-strip.tscm-strip .strip-status span {
color: #9ca3af; /* Explicit gray for status/time */
}
.function-strip.rtlamr-strip .strip-stat {
background: rgba(175, 82, 222, 0.05);
border-color: rgba(175, 82, 222, 0.15);
}
.function-strip.rtlamr-strip .strip-stat:hover {
background: rgba(175, 82, 222, 0.1);
border-color: rgba(175, 82, 222, 0.3);
}
.function-strip.rtlamr-strip .strip-value {
color: #af52de;
}
.function-strip.listening-strip .strip-stat {
background: rgba(74, 158, 255, 0.05);
border-color: rgba(74, 158, 255, 0.15);
}
.function-strip.listening-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3);
}
.function-strip.listening-strip .strip-value {
color: var(--accent-cyan);
}
/* Threat-colored stats for TSCM */
.function-strip .strip-stat.threat-high .strip-value { color: var(--accent-red); }
.function-strip .strip-stat.threat-review .strip-value { color: var(--accent-orange); }
.function-strip .strip-stat.threat-info .strip-value { color: var(--accent-cyan); }
+9 -1
View File
@@ -14,10 +14,18 @@
.radar-device { .radar-device {
transition: transform 0.2s ease; transition: transform 0.2s ease;
transform-origin: center center;
cursor: pointer;
} }
.radar-device:hover { .radar-device:hover {
transform: scale(1.3); transform: scale(1.2);
}
/* Invisible larger hit area to prevent hover flicker */
.radar-device-hitarea {
fill: transparent;
pointer-events: all;
} }
.radar-dot-pulse circle:first-child { .radar-dot-pulse circle:first-child {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+420
View File
@@ -0,0 +1,420 @@
/**
* INTERCEPT Base Styles
* Reset, typography, and foundational element styles
* Requires: variables.css to be imported first
*/
/* ============================================
CSS RESET
============================================ */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-normal);
color: var(--text-primary);
background: var(--bg-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4, h5, h6 {
font-weight: var(--font-semibold);
line-height: var(--leading-tight);
color: var(--text-primary);
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
h5 { font-size: var(--text-lg); }
h6 { font-size: var(--text-base); }
p {
margin-bottom: var(--space-4);
}
a {
color: var(--accent-cyan);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--accent-cyan-hover);
}
a:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
strong, b {
font-weight: var(--font-semibold);
}
small {
font-size: var(--text-sm);
}
code, kbd, pre, samp {
font-family: var(--font-mono);
font-size: 0.9em;
}
code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
pre {
background: var(--bg-tertiary);
padding: var(--space-4);
border-radius: var(--radius-md);
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
}
/* ============================================
FORM ELEMENTS
============================================ */
button,
input,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
}
button {
cursor: pointer;
border: none;
background: none;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
input,
select,
textarea {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
color: var(--text-primary);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px var(--accent-cyan-dim);
}
input::placeholder,
textarea::placeholder {
color: var(--text-dim);
}
select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
}
input[type="checkbox"],
input[type="radio"] {
width: 16px;
height: 16px;
padding: 0;
cursor: pointer;
accent-color: var(--accent-cyan);
}
label {
display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-1);
}
/* ============================================
TABLES
============================================ */
table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
th,
td {
padding: var(--space-2) var(--space-3);
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
font-weight: var(--font-semibold);
color: var(--text-secondary);
background: var(--bg-secondary);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.05em;
}
tr:hover td {
background: var(--bg-tertiary);
}
/* ============================================
LISTS
============================================ */
ul, ol {
padding-left: var(--space-6);
margin-bottom: var(--space-4);
}
li {
margin-bottom: var(--space-1);
}
/* ============================================
UTILITY CLASSES
============================================ */
/* Text colors */
.text-primary { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-muted { color: var(--text-muted); }
.text-cyan { color: var(--accent-cyan); }
.text-green { color: var(--accent-green); }
.text-red { color: var(--accent-red); }
.text-orange { color: var(--accent-orange); }
.text-amber { color: var(--accent-amber); }
/* Font utilities */
.font-mono { font-family: var(--font-mono); }
.font-medium { font-weight: var(--font-medium); }
.font-semibold { font-weight: var(--font-semibold); }
.font-bold { font-weight: var(--font-bold); }
/* Text sizes */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
/* Display */
.hidden { display: none !important; }
.block { display: block; }
.inline-block { display: inline-block; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
/* Flexbox */
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.flex-1 { flex: 1; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
/* Spacing */
.m-0 { margin: 0; }
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-4 { margin-bottom: var(--space-4); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
/* Borders */
.rounded { border-radius: var(--radius-md); }
.rounded-lg { border-radius: var(--radius-lg); }
.border { border: 1px solid var(--border-color); }
/* Truncate text */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ============================================
SCROLLBAR STYLING
============================================ */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-light);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-dim);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--border-light) var(--bg-secondary);
}
/* ============================================
SELECTION
============================================ */
::selection {
background: var(--accent-cyan-dim);
color: var(--text-primary);
}
/* ============================================
UX POLISH - TRANSITIONS & INTERACTIONS
============================================ */
/* Smooth page transitions */
html {
scroll-behavior: smooth;
}
/* Better focus ring for all interactive elements */
:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
}
/* Remove focus ring for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* Active state feedback */
button:active:not(:disabled),
a:active,
[role="button"]:active {
transform: scale(0.98);
}
/* Smooth transitions for all interactive elements */
button,
a,
input,
select,
textarea,
[role="button"] {
transition:
color var(--transition-fast),
background-color var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast),
opacity var(--transition-fast);
}
/* Subtle hover lift effect for cards and panels */
.card:hover,
.panel:hover {
box-shadow: var(--shadow-md);
}
/* Link underline on hover */
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Skip link for accessibility */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent-cyan);
color: var(--bg-primary);
padding: var(--space-2) var(--space-4);
z-index: 9999;
transition: top var(--transition-fast);
}
.skip-link:focus {
top: 0;
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--border-color: #4b5563;
--text-secondary: #d1d5db;
}
}
+723
View File
@@ -0,0 +1,723 @@
/**
* INTERCEPT UI Components
* Reusable component styles for buttons, cards, badges, etc.
* Requires: variables.css and base.css
*/
/* ============================================
BUTTONS
============================================ */
/* Base button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
font-weight: var(--font-medium);
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
text-decoration: none;
}
.btn:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Button variants */
.btn-primary {
background: var(--accent-cyan);
color: var(--text-inverse);
border-color: var(--accent-cyan);
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-cyan-hover);
border-color: var(--accent-cyan-hover);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-elevated);
border-color: var(--border-light);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-danger {
background: var(--accent-red);
color: white;
border-color: var(--accent-red);
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
border-color: #dc2626;
}
.btn-success {
background: var(--accent-green);
color: white;
border-color: var(--accent-green);
}
.btn-success:hover:not(:disabled) {
background: #16a34a;
border-color: #16a34a;
}
/* Button sizes */
.btn-sm {
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
}
.btn-lg {
padding: var(--space-3) var(--space-6);
font-size: var(--text-base);
}
/* Icon button */
.btn-icon {
padding: var(--space-2);
width: 36px;
height: 36px;
}
.btn-icon.btn-sm {
width: 28px;
height: 28px;
padding: var(--space-1);
}
/* ============================================
CARDS / PANELS
============================================ */
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.card-header-title {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.card-body {
padding: var(--space-4);
}
.card-footer {
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
}
/* Panel variant (used in dashboards) */
.panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-color);
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
}
.panel-indicator {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--status-offline);
}
.panel-indicator.active {
background: var(--status-online);
box-shadow: 0 0 8px var(--status-online);
}
.panel-content {
padding: var(--space-3);
}
/* ============================================
BADGES
============================================ */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px var(--space-2);
font-size: var(--text-xs);
font-weight: var(--font-medium);
border-radius: var(--radius-full);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.badge-primary {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.badge-success {
background: var(--accent-green-dim);
color: var(--accent-green);
}
.badge-warning {
background: var(--accent-orange-dim);
color: var(--accent-orange);
}
.badge-danger {
background: var(--accent-red-dim);
color: var(--accent-red);
}
/* ============================================
STATUS INDICATORS
============================================ */
.status-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--status-offline);
flex-shrink: 0;
}
.status-dot.online,
.status-dot.active {
background: var(--status-online);
box-shadow: 0 0 6px var(--status-online);
}
.status-dot.warning {
background: var(--status-warning);
box-shadow: 0 0 6px var(--status-warning);
}
.status-dot.error,
.status-dot.offline {
background: var(--status-error);
}
.status-dot.inactive {
background: var(--status-offline);
}
/* Pulse animation for active status */
.status-dot.pulse {
animation: statusPulse 2s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ============================================
EMPTY STATE
============================================ */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-8) var(--space-4);
text-align: center;
color: var(--text-muted);
}
.empty-state-icon {
width: 48px;
height: 48px;
margin-bottom: var(--space-4);
opacity: 0.5;
}
.empty-state-title {
font-size: var(--text-base);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.empty-state-description {
font-size: var(--text-sm);
color: var(--text-dim);
max-width: 300px;
}
.empty-state-action {
margin-top: var(--space-4);
}
/* ============================================
LOADING STATES
============================================ */
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-cyan);
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
}
.spinner-sm {
width: 14px;
height: 14px;
border-width: 2px;
}
.spinner-lg {
width: 32px;
height: 32px;
border-width: 3px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Loading overlay */
.loading-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-overlay);
z-index: var(--z-modal);
}
/* Skeleton loader */
.skeleton {
background: linear-gradient(
90deg,
var(--bg-tertiary) 25%,
var(--bg-elevated) 50%,
var(--bg-tertiary) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-sm);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ============================================
STATS STRIP
============================================ */
.stats-strip {
display: flex;
align-items: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 0 var(--space-4);
height: var(--stats-strip-height);
overflow-x: auto;
gap: var(--space-1);
}
.strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 var(--space-3);
min-width: fit-content;
}
.strip-value {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--accent-cyan);
line-height: 1;
}
.strip-label {
font-size: 9px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
margin-top: 2px;
}
.strip-divider {
width: 1px;
height: 20px;
background: var(--border-color);
margin: 0 var(--space-2);
}
/* ============================================
FORM GROUPS
============================================ */
.form-group {
margin-bottom: var(--space-4);
}
.form-group label {
display: block;
margin-bottom: var(--space-1);
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
}
.form-help {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--text-dim);
}
.form-error {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--accent-red);
}
/* Inline checkbox/radio */
.form-check {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
.form-check input {
width: auto;
}
.form-check label {
margin-bottom: 0;
cursor: pointer;
}
/* ============================================
ALERTS / TOASTS
============================================ */
.alert {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
border: 1px solid;
font-size: var(--text-sm);
}
.alert-info {
background: var(--accent-cyan-dim);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.alert-success {
background: var(--accent-green-dim);
border-color: var(--accent-green);
color: var(--accent-green);
}
.alert-warning {
background: var(--accent-orange-dim);
border-color: var(--accent-orange);
color: var(--accent-orange);
}
.alert-danger {
background: var(--accent-red-dim);
border-color: var(--accent-red);
color: var(--accent-red);
}
/* ============================================
TOOLTIPS
============================================ */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: var(--space-1) var(--space-2);
background: var(--bg-elevated);
color: var(--text-primary);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-fast), visibility var(--transition-fast);
z-index: var(--z-tooltip);
pointer-events: none;
margin-bottom: var(--space-1);
box-shadow: var(--shadow-md);
}
[data-tooltip]:hover::after {
opacity: 1;
visibility: visible;
}
/* ============================================
ICONS
============================================ */
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.icon svg {
width: 100%;
height: 100%;
}
.icon--sm {
width: 16px;
height: 16px;
}
.icon--lg {
width: 24px;
height: 24px;
}
/* ============================================
SECTION HEADERS
============================================ */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--border-color);
}
.section-title {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
/* ============================================
DIVIDERS
============================================ */
.divider {
height: 1px;
background: var(--border-color);
margin: var(--space-4) 0;
}
.divider-vertical {
width: 1px;
height: 100%;
background: var(--border-color);
margin: 0 var(--space-3);
}
/* ============================================
UX POLISH - ENHANCED INTERACTIONS
============================================ */
/* Button hover lift */
.btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
/* Card/Panel hover effects */
.card,
.panel {
transition:
box-shadow var(--transition-base),
border-color var(--transition-base),
transform var(--transition-base);
}
.card:hover,
.panel:hover {
border-color: var(--border-light);
}
/* Stats strip value highlight on hover */
.strip-stat {
transition: background-color var(--transition-fast);
border-radius: var(--radius-sm);
cursor: default;
}
.strip-stat:hover {
background: var(--bg-tertiary);
}
/* Status dot pulse animation */
.status-dot.online,
.status-dot.active {
animation: statusGlow 2s ease-in-out infinite;
}
@keyframes statusGlow {
0%, 100% {
box-shadow: 0 0 6px var(--status-online);
}
50% {
box-shadow: 0 0 12px var(--status-online), 0 0 20px var(--status-online);
}
}
/* Badge hover effect */
.badge {
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.badge:hover {
transform: scale(1.05);
}
/* Alert entrance animation */
.alert {
animation: alertSlideIn 0.3s ease-out;
}
@keyframes alertSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Loading spinner smooth appearance */
.spinner {
animation: spin 0.8s linear infinite, fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Input focus glow */
input:focus,
select:focus,
textarea:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px var(--accent-cyan-dim), 0 0 20px rgba(74, 158, 255, 0.1);
}
/* Nav item active indicator */
.mode-nav-btn.active::after,
.mobile-nav-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 2px;
background: currentColor;
border-radius: var(--radius-full);
}
/* Smooth tooltip appearance */
[data-tooltip]::after {
transition:
opacity var(--transition-fast),
visibility var(--transition-fast),
transform var(--transition-fast);
transform: translateX(-50%) translateY(-4px);
}
[data-tooltip]:hover::after {
transform: translateX(-50%) translateY(0);
}
/* Disabled state with better visual feedback */
:disabled,
.disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(30%);
}
+950
View File
@@ -0,0 +1,950 @@
/**
* INTERCEPT Layout Styles
* Global layout structure: header, navigation, sidebar, main content
* Requires: variables.css, base.css, components.css
*/
/* ============================================
APP SHELL
============================================ */
.app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-main {
flex: 1;
display: flex;
flex-direction: column;
}
/* ============================================
GLOBAL HEADER
============================================ */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-height);
padding: 0 var(--space-4);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.app-header-left {
display: flex;
align-items: center;
gap: var(--space-4);
}
.app-header-right {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* Logo */
.app-logo {
display: flex;
align-items: center;
gap: var(--space-3);
text-decoration: none;
color: var(--text-primary);
}
.app-logo-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
}
.app-logo-text {
display: flex;
flex-direction: column;
}
.app-logo-title {
font-size: var(--text-lg);
font-weight: var(--font-bold);
line-height: 1.2;
color: var(--text-primary);
}
.app-logo-tagline {
font-size: var(--text-xs);
color: var(--text-dim);
}
/* Page title in header */
.app-header-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
}
.app-header-subtitle {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-left: var(--space-2);
}
/* Header utilities */
.header-utilities {
display: flex;
align-items: center;
gap: var(--space-2);
}
.header-clock {
display: flex;
align-items: center;
gap: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.header-clock-label {
font-size: var(--text-xs);
color: var(--text-dim);
}
/* ============================================
GLOBAL NAVIGATION
============================================ */
.app-nav {
display: flex;
align-items: center;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: 0 var(--space-4);
height: var(--nav-height);
gap: var(--space-1);
overflow-x: auto;
}
.app-nav::-webkit-scrollbar {
height: 0;
}
/* Nav groups */
.nav-group {
display: flex;
align-items: center;
position: relative;
}
/* Dropdown trigger */
.nav-dropdown-trigger {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
background: transparent;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.nav-dropdown-trigger:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
.nav-dropdown-trigger.active {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.nav-dropdown-arrow {
width: 12px;
height: 12px;
transition: transform var(--transition-fast);
}
.nav-group.open .nav-dropdown-arrow {
transform: rotate(180deg);
}
/* Dropdown menu */
.nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
min-width: 180px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: var(--space-1);
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: all var(--transition-fast);
z-index: var(--z-dropdown);
}
.nav-group.open .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(4px);
}
/* Nav items */
.nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
border: none;
background: none;
width: 100%;
text-align: left;
}
.nav-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item.active {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.nav-item-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Nav divider */
.nav-divider {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 var(--space-2);
}
/* Nav utilities (right side) */
.nav-utilities {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
padding-left: var(--space-4);
}
.nav-tool-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
}
.nav-tool-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
/* ============================================
MOBILE NAVIGATION
============================================ */
.mobile-nav {
display: none;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: var(--space-2) var(--space-3);
overflow-x: auto;
gap: var(--space-2);
}
.mobile-nav::-webkit-scrollbar {
height: 0;
}
.mobile-nav-btn {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--text-secondary);
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
white-space: nowrap;
text-decoration: none;
transition: all var(--transition-fast);
}
.mobile-nav-btn:hover,
.mobile-nav-btn.active {
background: var(--accent-cyan-dim);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* Hamburger button */
.hamburger-btn {
display: none;
flex-direction: column;
justify-content: center;
gap: 4px;
width: 32px;
height: 32px;
padding: 6px;
background: transparent;
border: none;
cursor: pointer;
}
.hamburger-btn span {
display: block;
width: 100%;
height: 2px;
background: var(--text-secondary);
border-radius: 1px;
transition: all var(--transition-fast);
}
.hamburger-btn.open span:nth-child(1) {
transform: rotate(45deg) translate(4px, 4px);
}
.hamburger-btn.open span:nth-child(2) {
opacity: 0;
}
.hamburger-btn.open span:nth-child(3) {
transform: rotate(-45deg) translate(4px, -4px);
}
/* ============================================
CONTENT LAYOUTS
============================================ */
/* Main content with optional sidebar */
.content-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.app-sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-section {
padding: var(--space-4);
border-bottom: 1px solid var(--border-color);
}
.sidebar-section:last-child {
border-bottom: none;
}
.sidebar-section-title {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
/* Main content area */
.app-content {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
.app-content-full {
flex: 1;
overflow: hidden;
position: relative;
}
/* ============================================
DASHBOARD LAYOUTS
============================================ */
/* Full-screen dashboard (maps, etc.) */
.dashboard-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-4);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.dashboard-header-logo {
font-size: var(--text-lg);
font-weight: var(--font-bold);
color: var(--text-primary);
}
.dashboard-header-logo span {
font-size: var(--text-sm);
font-weight: var(--font-normal);
color: var(--text-dim);
margin-left: var(--space-2);
}
.dashboard-main {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.dashboard-map {
flex: 1;
position: relative;
}
.dashboard-sidebar {
width: 320px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-3);
}
/* ============================================
PAGE LAYOUTS
============================================ */
.page-container {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--space-6);
}
.page-header {
margin-bottom: var(--space-6);
}
.page-title {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.page-description {
font-size: var(--text-base);
color: var(--text-secondary);
}
/* ============================================
RESPONSIVE BREAKPOINTS
============================================ */
@media (max-width: 1024px) {
.app-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(-100%);
transition: transform var(--transition-base);
}
.app-sidebar.open {
transform: translateX(0);
}
.dashboard-sidebar {
width: 280px;
}
}
@media (max-width: 768px) {
.app-nav {
display: none;
}
.mobile-nav {
display: flex;
}
.hamburger-btn {
display: flex;
}
.app-header {
padding: 0 var(--space-3);
}
.app-logo-text {
display: none;
}
.header-utilities {
gap: var(--space-1);
}
.page-container {
padding: var(--space-4);
}
.dashboard-sidebar {
position: fixed;
right: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(100%);
transition: transform var(--transition-base);
}
.dashboard-sidebar.open {
transform: translateX(0);
}
}
/* ============================================
OVERLAY (for mobile drawers)
============================================ */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: calc(var(--z-fixed) - 1);
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
}
.drawer-overlay.visible {
opacity: 1;
visibility: visible;
}
/* ============================================
BACK LINK
============================================ */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--text-secondary);
text-decoration: none;
transition: color var(--transition-fast);
}
.back-link:hover {
color: var(--accent-cyan);
}
/* ============================================
MODE NAVIGATION (from index.css)
Used by nav.html partial across all pages
============================================ */
/* Mode Navigation Bar */
.mode-nav {
display: none;
background: #151a23 !important; /* Explicit color - forced to ensure consistency */
border-bottom: 1px solid var(--border-color);
padding: 0 20px;
position: relative;
z-index: 100;
}
@media (min-width: 1024px) {
.mode-nav {
display: flex;
align-items: center;
gap: 8px;
height: 44px;
}
}
.mode-nav-group {
display: flex;
align-items: center;
gap: 4px;
}
.mode-nav-label {
font-size: 9px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 8px;
font-weight: 500;
}
.mode-nav-divider {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 12px;
}
.mode-nav-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--text-secondary);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-btn .nav-icon {
font-size: 14px;
}
.mode-nav-btn .nav-icon svg {
width: 14px;
height: 14px;
}
.mode-nav-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mode-nav-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-btn.active {
background: var(--accent-cyan);
color: var(--bg-primary);
border-color: var(--accent-cyan);
}
.mode-nav-btn.active .nav-icon {
filter: brightness(0);
}
.mode-nav-actions {
display: flex;
align-items: center;
gap: 16px;
}
.nav-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--bg-elevated);
border: 1px solid var(--accent-cyan);
border-radius: 4px;
color: var(--accent-cyan);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.nav-action-btn .nav-icon {
font-size: 12px;
}
.nav-action-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.nav-action-btn:hover {
background: var(--accent-cyan);
color: var(--bg-primary);
}
/* Dropdown Navigation */
.mode-nav-dropdown {
position: relative;
}
.mode-nav-dropdown-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--text-secondary);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-dropdown-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-dropdown-btn .nav-icon {
font-size: 14px;
}
.mode-nav-dropdown-btn .nav-icon svg {
width: 14px;
height: 14px;
}
.mode-nav-dropdown-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mode-nav-dropdown-btn .dropdown-arrow {
font-size: 8px;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mode-nav-dropdown-btn .dropdown-arrow svg {
width: 10px;
height: 10px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: var(--accent-cyan);
color: var(--bg-primary);
border-color: var(--accent-cyan);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
filter: brightness(0);
}
.mode-nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 180px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.15s ease;
z-index: 1000;
padding: 6px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.mode-nav-dropdown-menu .mode-nav-btn {
width: 100%;
justify-content: flex-start;
padding: 10px 12px;
border-radius: 4px;
margin: 0;
}
.mode-nav-dropdown-menu .mode-nav-btn:hover {
background: var(--bg-elevated);
}
.mode-nav-dropdown-menu .mode-nav-btn.active {
background: var(--accent-cyan);
color: var(--bg-primary);
}
/* Nav Bar Utilities (clock, theme, tools) */
.nav-utilities {
display: none;
align-items: center;
gap: 12px;
margin-left: auto;
flex-shrink: 0;
}
@media (min-width: 1024px) {
.nav-utilities {
display: flex;
}
}
.nav-clock {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
flex-shrink: 0;
white-space: nowrap;
}
.nav-clock .utc-label {
font-size: 9px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-clock .utc-time {
color: var(--accent-cyan);
font-weight: 600;
}
.nav-divider {
width: 1px;
height: 20px;
background: var(--border-color);
}
.nav-tools {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.nav-tool-btn {
width: 28px;
height: 28px;
min-width: 28px;
border-radius: 4px;
background: transparent;
border: 1px solid transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.nav-tool-btn:hover {
background: var(--bg-elevated);
border-color: var(--border-color);
color: var(--accent-cyan);
}
.nav-tool-btn svg {
width: 14px;
height: 14px;
}
.nav-tool-btn .icon svg {
width: 14px;
height: 14px;
}
/* Theme toggle icon states in nav bar */
.nav-tool-btn .icon-sun,
.nav-tool-btn .icon-moon {
position: absolute;
transition: opacity 0.2s, transform 0.2s;
font-size: 14px;
}
.nav-tool-btn .icon-sun {
opacity: 0;
transform: rotate(-90deg);
}
.nav-tool-btn .icon-moon {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-sun {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-moon {
opacity: 0;
transform: rotate(90deg);
}
/* Effects toggle icon states */
.nav-tool-btn .icon-effects-off {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-on {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-off {
display: flex;
}
+198
View File
@@ -0,0 +1,198 @@
/**
* INTERCEPT Design Tokens
* Single source of truth for colors, spacing, typography, and effects
* Import this file FIRST in any stylesheet that needs design tokens
*/
:root {
/* ============================================
COLOR PALETTE - Dark Theme (Default)
============================================ */
/* Backgrounds - layered depth system */
--bg-primary: #0a0c10;
--bg-secondary: #0f1218;
--bg-tertiary: #151a23;
--bg-card: #121620;
--bg-elevated: #1a202c;
--bg-overlay: rgba(0, 0, 0, 0.7);
/* Background aliases for components */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
/* Accent colors */
--accent-cyan: #4a9eff;
--accent-cyan-dim: rgba(74, 158, 255, 0.15);
--accent-cyan-hover: #6bb3ff;
--accent-green: #22c55e;
--accent-green-dim: rgba(34, 197, 94, 0.15);
--accent-red: #ef4444;
--accent-red-dim: rgba(239, 68, 68, 0.15);
--accent-orange: #f59e0b;
--accent-orange-dim: rgba(245, 158, 11, 0.15);
--accent-amber: #d4a853;
--accent-amber-dim: rgba(212, 168, 83, 0.15);
--accent-yellow: #eab308;
--accent-purple: #a855f7;
/* Text hierarchy */
--text-primary: #e8eaed;
--text-secondary: #9ca3af;
--text-dim: #4b5563;
--text-muted: #374151;
--text-inverse: #0a0c10;
/* Borders */
--border-color: #1f2937;
--border-light: #374151;
--border-glow: rgba(74, 158, 255, 0.2);
--border-focus: var(--accent-cyan);
/* Status colors */
--status-online: #22c55e;
--status-warning: #f59e0b;
--status-error: #ef4444;
--status-offline: #6b7280;
--status-info: #3b82f6;
/* ============================================
SPACING SCALE
============================================ */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
/* ============================================
TYPOGRAPHY
============================================ */
--font-sans: 'Space Mono', ui-monospace, 'SF Mono', monospace;
--font-mono: 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', monospace;
/* Font sizes */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 20px;
--text-3xl: 24px;
--text-4xl: 30px;
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* Line heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* ============================================
BORDERS & RADIUS
============================================ */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
/* ============================================
SHADOWS
============================================ */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 20px rgba(74, 158, 255, 0.15);
/* ============================================
TRANSITIONS
============================================ */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
/* ============================================
Z-INDEX SCALE
============================================ */
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-toast: 600;
--z-tooltip: 700;
/* ============================================
LAYOUT
============================================ */
--header-height: 60px;
--nav-height: 44px;
--sidebar-width: 280px;
--stats-strip-height: 36px;
--content-max-width: 1400px;
}
/* ============================================
LIGHT THEME OVERRIDES
============================================ */
[data-theme="light"] {
--bg-primary: #f8fafc;
--bg-secondary: #f1f5f9;
--bg-tertiary: #e2e8f0;
--bg-card: #ffffff;
--bg-elevated: #f8fafc;
--bg-overlay: rgba(255, 255, 255, 0.9);
/* Background aliases for components */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--accent-cyan: #2563eb;
--accent-cyan-dim: rgba(37, 99, 235, 0.1);
--accent-cyan-hover: #1d4ed8;
--accent-green: #16a34a;
--accent-green-dim: rgba(22, 163, 74, 0.1);
--accent-red: #dc2626;
--accent-red-dim: rgba(220, 38, 38, 0.1);
--accent-orange: #d97706;
--accent-orange-dim: rgba(217, 119, 6, 0.1);
--accent-amber: #b45309;
--accent-amber-dim: rgba(180, 83, 9, 0.1);
--text-primary: #0f172a;
--text-secondary: #475569;
--text-dim: #94a3b8;
--text-muted: #cbd5e1;
--text-inverse: #f8fafc;
--border-color: #e2e8f0;
--border-light: #cbd5e1;
--border-glow: rgba(37, 99, 235, 0.15);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
--shadow-glow: 0 0 20px rgba(37, 99, 235, 0.1);
}
/* ============================================
REDUCED MOTION
============================================ */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0ms;
--transition-base: 0ms;
--transition-slow: 0ms;
}
}
+18 -67
View File
@@ -1,67 +1,18 @@
/* Local font declarations for offline mode */ /* Local font declarations for offline mode */
/* Inter - Primary UI font */ /* Space Mono - Console font */
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Space Mono';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2'); src: url('/static/vendor/fonts/SpaceMono-Regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Space Mono';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 700;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2'); src: url('/static/vendor/fonts/SpaceMono-Bold.woff2') format('woff2');
} }
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
}
/* JetBrains Mono - Monospace/code font */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
}
+439
View File
@@ -0,0 +1,439 @@
/* ============================================
Global Navigation Styles
Shared across all pages using nav.html
============================================ */
/* Icon base (kept lightweight for nav usage) */
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
}
.icon svg {
width: 100%;
height: 100%;
}
.icon--sm {
width: 14px;
height: 14px;
}
/* Mode Navigation Bar */
.mode-nav {
display: none;
background: linear-gradient(180deg, rgba(17, 22, 32, 0.92), rgba(15, 20, 28, 0.88));
border-bottom: 1px solid var(--border-color, #202833);
padding: 0 20px;
position: relative;
z-index: 100;
backdrop-filter: blur(10px);
}
@media (min-width: 1024px) {
.mode-nav {
display: flex;
align-items: center;
gap: 8px;
height: 44px;
}
}
.mode-nav-label {
font-size: 9px;
color: var(--text-secondary, #b7c1cf);
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 8px;
font-weight: 500;
font-family: var(--font-mono);
}
.mode-nav-divider {
width: 1px;
height: 24px;
background: var(--border-color, #202833);
margin: 0 12px;
}
.mode-nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-secondary, #b7c1cf);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
}
.mode-nav-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.mode-nav-btn:hover {
background: rgba(27, 36, 51, 0.8);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-btn.active {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--accent-cyan, #4d7dbf);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
.mode-nav-btn.active .nav-icon {
color: var(--accent-cyan, #4d7dbf);
}
.mode-nav-actions {
display: flex;
align-items: center;
gap: 16px;
}
.nav-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: rgba(24, 31, 44, 0.85);
border: 1px solid var(--border-light, #2b3645);
border-radius: 6px;
color: var(--text-primary, #e7ebf2);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.nav-action-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.nav-action-btn:hover {
background: rgba(27, 36, 51, 0.95);
color: var(--text-primary, #e7ebf2);
box-shadow: 0 8px 16px rgba(5, 9, 15, 0.35);
border-color: var(--accent-cyan, #4d7dbf);
}
/* Dropdown Navigation */
.mode-nav-dropdown {
position: relative;
}
.mode-nav-dropdown-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-secondary, #b7c1cf);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-dropdown-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.mode-nav-dropdown-btn .dropdown-arrow {
width: 12px;
height: 12px;
margin-left: 4px;
transition: transform 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.mode-nav-dropdown-btn .dropdown-arrow svg {
width: 100%;
height: 100%;
}
.mode-nav-dropdown-btn:hover {
background: rgba(27, 36, 51, 0.8);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--accent-cyan, #4d7dbf);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
color: var(--accent-cyan, #4d7dbf);
}
.mode-nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 180px;
background: rgba(16, 22, 32, 0.98);
border: 1px solid var(--border-color, #202833);
border-radius: 8px;
box-shadow: 0 16px 36px rgba(5, 9, 15, 0.55);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.15s ease;
z-index: 1000;
padding: 6px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.mode-nav-dropdown-menu .mode-nav-btn {
width: 100%;
justify-content: flex-start;
padding: 10px 12px;
border-radius: 6px;
}
.mode-nav-dropdown-menu .mode-nav-btn:hover {
background: rgba(27, 36, 51, 0.85);
}
.mode-nav-dropdown-menu .mode-nav-btn.active {
background: rgba(27, 36, 51, 0.95);
color: var(--text-primary, #e7ebf2);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
/* Nav Bar Utilities */
.nav-utilities {
display: none;
align-items: center;
gap: 12px;
margin-left: auto;
flex-shrink: 0;
}
@media (min-width: 1024px) {
.nav-utilities {
display: flex;
}
}
.nav-clock {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
flex-shrink: 0;
white-space: nowrap;
}
.nav-clock .utc-label {
font-size: 9px;
color: var(--text-dim, #8a97a8);
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-clock .utc-time {
color: var(--accent-cyan, #4d7dbf);
font-weight: 600;
}
.nav-divider {
width: 1px;
height: 20px;
background: var(--border-color, #202833);
}
.nav-tools {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.nav-tool-btn {
width: 28px;
height: 28px;
min-width: 28px;
border-radius: 6px;
background: rgba(20, 33, 53, 0.6);
border: 1px solid rgba(77, 125, 191, 0.12);
color: var(--text-secondary, #b7c1cf);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-tool-btn:hover {
background: rgba(27, 36, 51, 0.9);
border-color: var(--accent-cyan, #4d7dbf);
color: var(--accent-cyan, #4d7dbf);
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
}
/* Position relative needed for absolute positioned icon children */
.nav-tool-btn {
position: relative;
}
.mode-nav-btn:focus-visible,
.mode-nav-dropdown-btn:focus-visible,
.nav-action-btn:focus-visible,
.nav-tool-btn:focus-visible {
outline: 2px solid var(--accent-cyan, #4d7dbf);
outline-offset: 2px;
}
/* Nav tool button SVG sizing and styling */
.nav-tool-btn svg {
width: 14px;
height: 14px;
stroke: currentColor;
}
.nav-tool-btn .icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-tool-btn .icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
}
/* Theme toggle icon states */
.nav-tool-btn .icon-sun,
.nav-tool-btn .icon-moon {
position: absolute;
transition: opacity 0.2s, transform 0.2s;
}
.nav-tool-btn .icon-sun {
opacity: 0;
transform: rotate(-90deg);
}
.nav-tool-btn .icon-moon {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-sun {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-moon {
opacity: 0;
transform: rotate(90deg);
}
/* Effects/animations toggle icon states */
.nav-tool-btn .icon-effects-off {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-on {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-off {
display: flex;
}
/* Main Dashboard Button in Nav */
a.nav-dashboard-btn,
a.nav-dashboard-btn:link,
a.nav-dashboard-btn:visited {
display: inline-flex !important;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
background: rgba(20, 33, 53, 0.6) !important;
border: 1px solid rgba(77, 125, 191, 0.12) !important;
color: #b7c1cf !important;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
text-decoration: none !important;
}
a.nav-dashboard-btn:hover {
background: rgba(27, 36, 51, 0.9) !important;
border-color: #4d7dbf !important;
color: #4d7dbf !important;
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
}
.nav-dashboard-btn .icon {
width: 14px;
height: 14px;
}
.nav-dashboard-btn .icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
}
.nav-dashboard-btn .nav-label {
font-family: var(--font-mono, 'JetBrains Mono', monospace);
letter-spacing: 0.5px;
}
+184
View File
@@ -0,0 +1,184 @@
/**
* Help Modal Styles
* Shared across all pages that include the help modal partial
*/
.help-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 10000;
overflow-y: auto;
padding: 40px 20px;
}
.help-modal.active {
display: block;
}
.help-content {
max-width: 800px;
margin: 0 auto;
background: var(--bg-card, var(--bg-secondary, #0f1218));
border: 1px solid var(--border-color, #1f2937);
border-radius: 8px;
padding: 30px;
position: relative;
}
.help-content h2 {
color: var(--accent-cyan, #4a9eff);
margin-bottom: 20px;
font-size: 24px;
letter-spacing: 2px;
}
.help-content h3 {
color: var(--text-primary, #e8eaed);
margin: 25px 0 15px 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border-color, #1f2937);
padding-bottom: 8px;
}
.help-close {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
color: var(--text-dim, #4b5563);
font-size: 24px;
cursor: pointer;
transition: color 0.2s;
}
.help-close:hover {
color: var(--accent-red, #ef4444);
}
.help-modal .icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin: 15px 0;
}
.help-modal .icon-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: var(--bg-primary, #0a0c10);
border: 1px solid var(--border-color, #1f2937);
border-radius: 4px;
font-size: 12px;
}
.help-modal .icon-item .icon {
font-size: 18px;
width: 30px;
text-align: center;
}
.help-modal .icon-item .desc {
color: var(--text-secondary, #9ca3af);
}
.help-modal .tip-list {
list-style: none;
padding: 0;
margin: 15px 0;
}
.help-modal .tip-list li {
padding: 8px 0;
padding-left: 20px;
position: relative;
color: var(--text-secondary, #9ca3af);
font-size: 13px;
border-bottom: 1px solid var(--border-color, #1f2937);
}
.help-modal .tip-list li:last-child {
border-bottom: none;
}
.help-modal .tip-list li::before {
content: '\203A';
position: absolute;
left: 0;
color: var(--accent-cyan, #4a9eff);
font-weight: bold;
}
.help-tabs {
display: flex;
gap: 0;
margin-bottom: 20px;
border: 1px solid var(--border-color, #1f2937);
border-radius: 4px;
overflow: hidden;
}
.help-tab {
flex: 1;
padding: 10px;
background: var(--bg-primary, #0a0c10);
border: none;
color: var(--text-secondary, #9ca3af);
cursor: pointer;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.15s ease;
position: relative;
}
.help-tab:not(:last-child) {
border-right: 1px solid var(--border-color, #1f2937);
}
.help-tab:hover {
background: var(--bg-tertiary, #151a23);
color: var(--text-primary, #e8eaed);
}
.help-tab.active {
background: var(--bg-tertiary, #151a23);
color: var(--accent-cyan, #4a9eff);
}
.help-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent-cyan, #4a9eff);
}
.help-section {
display: none;
}
.help-section.active {
display: block;
}
/* Ensure code tags are styled */
.help-modal code {
background: var(--bg-tertiary, #151a23);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 11px;
color: var(--accent-cyan, #4a9eff);
}
+197 -70
View File
@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -7,6 +5,8 @@
} }
:root { :root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
/* Tactical dark palette */ /* Tactical dark palette */
--bg-primary: #0a0c10; --bg-primary: #0a0c10;
--bg-secondary: #0f1218; --bg-secondary: #0f1218;
@@ -73,7 +73,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: var(--font-sans);
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
@@ -259,7 +259,7 @@ body {
} }
.welcome-title { .welcome-title {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
@@ -269,7 +269,7 @@ body {
} }
.welcome-tagline { .welcome-tagline {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.9rem; font-size: 0.9rem;
color: var(--accent-cyan); color: var(--accent-cyan);
letter-spacing: 0.15em; letter-spacing: 0.15em;
@@ -278,7 +278,7 @@ body {
.welcome-version { .welcome-version {
display: inline-block; display: inline-block;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.65rem; font-size: 0.65rem;
color: var(--bg-primary); color: var(--bg-primary);
background: var(--accent-cyan); background: var(--accent-cyan);
@@ -297,7 +297,7 @@ body {
} }
.welcome-content h2 { .welcome-content h2 {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary); color: var(--text-secondary);
text-transform: uppercase; text-transform: uppercase;
@@ -333,14 +333,14 @@ body {
} }
.changelog-version { .changelog-version {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.8rem; font-size: 0.8rem;
color: var(--accent-cyan); color: var(--accent-cyan);
font-weight: 600; font-weight: 600;
} }
.changelog-date { .changelog-date {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-dim); color: var(--text-dim);
} }
@@ -352,7 +352,7 @@ body {
} }
.changelog-list li { .changelog-list li {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 6px; margin-bottom: 6px;
@@ -364,7 +364,7 @@ body {
position: absolute; position: absolute;
left: -15px; left: -15px;
color: var(--accent-green); color: var(--accent-green);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Mode Selection Grid */ /* Mode Selection Grid */
@@ -435,7 +435,7 @@ body {
} }
.mode-card .mode-name { .mode-card .mode-name {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
@@ -444,7 +444,7 @@ body {
} }
.mode-card .mode-desc { .mode-card .mode-desc {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 0.65rem; font-size: 0.65rem;
color: var(--text-dim); color: var(--text-dim);
margin-top: 4px; margin-top: 4px;
@@ -463,7 +463,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
color: var(--text-secondary); color: var(--text-secondary);
@@ -517,7 +517,7 @@ body {
} }
.welcome-footer p { .welcome-footer p {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-dim); color: var(--text-dim);
letter-spacing: 0.1em; letter-spacing: 0.1em;
@@ -731,7 +731,7 @@ header h1 {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
color: var(--text-secondary); color: var(--text-secondary);
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@@ -778,7 +778,7 @@ header h1 {
border: 1px solid var(--accent-cyan); border: 1px solid var(--accent-cyan);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
@@ -814,7 +814,7 @@ header h1 {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
color: var(--text-secondary); color: var(--text-secondary);
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@@ -922,7 +922,7 @@ header h1 {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
flex-shrink: 0; flex-shrink: 0;
white-space: nowrap; white-space: nowrap;
@@ -1030,7 +1030,7 @@ header h1 {
.version-badge { .version-badge {
font-size: 0.6rem; font-size: 0.6rem;
font-weight: 500; font-weight: 500;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
letter-spacing: 0.05em; letter-spacing: 0.05em;
color: var(--text-secondary); color: var(--text-secondary);
background: var(--bg-tertiary); background: var(--bg-tertiary);
@@ -1089,7 +1089,7 @@ header h1 .tagline {
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 6px; border-radius: 6px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -1578,7 +1578,7 @@ header h1 .tagline {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
color: var(--text-primary); color: var(--text-primary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
@@ -1590,6 +1590,11 @@ header h1 .tagline {
box-shadow: 0 0 0 2px var(--accent-cyan-dim); box-shadow: 0 0 0 2px var(--accent-cyan-dim);
} }
/* Ensure device select is wide enough for device name + serial */
#deviceSelect {
min-width: 280px;
}
.checkbox-group { .checkbox-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1632,7 +1637,7 @@ header h1 .tagline {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -1652,7 +1657,7 @@ header h1 .tagline {
background: var(--accent-green); background: var(--accent-green);
border: none; border: none;
color: #fff; color: #fff;
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -1689,7 +1694,7 @@ header h1 .tagline {
background: var(--accent-red); background: var(--accent-red);
border: none; border: none;
color: #fff; color: #fff;
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -1752,7 +1757,7 @@ header h1 .tagline {
gap: 8px; gap: 8px;
font-size: 10px; font-size: 10px;
color: var(--text-secondary); color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.stats>div { .stats>div {
@@ -1778,7 +1783,7 @@ header h1 .tagline {
flex: 1; flex: 1;
padding: 10px; padding: 10px;
overflow-y: auto; overflow-y: auto;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
background: var(--bg-primary); background: var(--bg-primary);
min-height: 0; /* Allow shrinking in flex context */ min-height: 0; /* Allow shrinking in flex context */
@@ -1850,7 +1855,7 @@ header h1 .tagline {
.message .address { .message .address {
color: var(--accent-green); color: var(--accent-green);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -1863,7 +1868,7 @@ header h1 .tagline {
} }
.message .content.numeric { .message .content.numeric {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 15px; font-size: 15px;
letter-spacing: 2px; letter-spacing: 2px;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -2084,7 +2089,7 @@ header h1 .tagline {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
transition: all 0.2s ease; transition: all 0.2s ease;
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
} }
.control-btn:hover { .control-btn:hover {
@@ -2352,7 +2357,7 @@ header h1 .tagline {
/* Dark theme for Leaflet */ /* Dark theme for Leaflet */
.leaflet-container { .leaflet-container {
background: #0a0a0a; background: #0a0a0a;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Using actual dark tiles now - no filter needed */ /* Using actual dark tiles now - no filter needed */
@@ -2389,7 +2394,7 @@ header h1 .tagline {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
z-index: 1000; z-index: 1000;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-cyan); color: var(--accent-cyan);
text-shadow: 0 0 5px var(--accent-cyan); text-shadow: 0 0 5px var(--accent-cyan);
@@ -2406,7 +2411,7 @@ header h1 .tagline {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
z-index: 1000; z-index: 1000;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--accent-cyan); color: var(--accent-cyan);
text-shadow: 0 0 5px var(--accent-cyan); text-shadow: 0 0 5px var(--accent-cyan);
@@ -2427,7 +2432,7 @@ header h1 .tagline {
} }
.aircraft-popup { .aircraft-popup {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -2471,7 +2476,7 @@ header h1 .tagline {
background: rgba(0, 0, 0, 0.8) !important; background: rgba(0, 0, 0, 0.8) !important;
border: 1px solid var(--accent-cyan) !important; border: 1px solid var(--accent-cyan) !important;
color: var(--accent-cyan) !important; color: var(--accent-cyan) !important;
font-family: 'JetBrains Mono', monospace !important; font-family: var(--font-mono) !important;
font-size: 10px !important; font-size: 10px !important;
padding: 2px 6px !important; padding: 2px 6px !important;
border-radius: 2px !important; border-radius: 2px !important;
@@ -2499,7 +2504,7 @@ header h1 .tagline {
border-radius: 4px; border-radius: 4px;
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -2715,7 +2720,7 @@ header h1 .tagline {
color: var(--accent-cyan); color: var(--accent-cyan);
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
text-shadow: 0 0 15px var(--accent-cyan-dim); text-shadow: 0 0 15px var(--accent-cyan-dim);
line-height: 1.2; line-height: 1.2;
} }
@@ -3109,7 +3114,7 @@ header h1 .tagline {
} }
.sensor-card .data-value { .sensor-card .data-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
@@ -3159,7 +3164,7 @@ header h1 .tagline {
display: flex; display: flex;
gap: 15px; gap: 15px;
font-size: 10px; font-size: 10px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.recon-stats span { .recon-stats span {
@@ -3209,14 +3214,14 @@ header h1 .tagline {
.device-id { .device-id {
color: var(--text-dim); color: var(--text-dim);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
} }
.device-meta { .device-meta {
text-align: right; text-align: right;
color: var(--text-secondary); color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.device-meta.encrypted { .device-meta.encrypted {
@@ -3292,7 +3297,7 @@ header h1 .tagline {
} }
.hex-dump { .hex-dump {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--text-dim); color: var(--text-dim);
background: var(--bg-primary); background: var(--bg-primary);
@@ -3383,7 +3388,7 @@ header h1 .tagline {
/* WiFi Main Content - 3 columns */ /* WiFi Main Content - 3 columns */
.wifi-main-content { .wifi-main-content {
display: grid; display: grid;
grid-template-columns: 1fr minmax(240px, 280px) minmax(240px, 280px); grid-template-columns: minmax(300px, 1fr) minmax(240px, 280px) minmax(240px, 280px);
gap: 10px; gap: 10px;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -3398,6 +3403,7 @@ header h1 .tagline {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
min-width: 0; /* Prevent content from forcing panel wider */
} }
.wifi-networks-header { .wifi-networks-header {
@@ -3565,6 +3571,8 @@ header h1 .tagline {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
padding: 12px; padding: 12px;
min-width: 0; /* Prevent content from forcing panel wider */
overflow: hidden;
} }
.wifi-radar-panel h5 { .wifi-radar-panel h5 {
@@ -3800,10 +3808,90 @@ header h1 .tagline {
text-transform: uppercase; text-transform: uppercase;
} }
.wifi-client-count-badge {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
background: var(--accent-cyan);
color: var(--bg-primary);
border-radius: 10px;
font-weight: 600;
margin-left: 6px;
vertical-align: middle;
}
.wifi-client-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 200px;
overflow-y: auto;
}
.wifi-client-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.wifi-client-identity {
display: flex;
flex-direction: column;
gap: 2px;
}
.wifi-client-mac {
font-family: monospace;
font-size: 12px;
color: var(--text-primary);
}
.wifi-client-vendor {
font-size: 10px;
color: var(--text-dim);
}
.wifi-client-probes {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.wifi-client-probe-badge {
font-size: 9px;
padding: 2px 6px;
background: var(--bg-tertiary);
border-radius: 3px;
color: var(--text-secondary);
}
.wifi-client-signal {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.wifi-client-rssi {
font-size: 12px;
font-weight: 500;
color: var(--text-primary);
}
.wifi-client-lastseen {
font-size: 9px;
color: var(--text-dim);
}
/* WiFi Responsive */ /* WiFi Responsive */
@media (max-width: 1400px) { @media (max-width: 1400px) {
.wifi-main-content { .wifi-main-content {
grid-template-columns: 1fr 240px 240px; grid-template-columns: minmax(280px, 1fr) 240px 240px;
} }
} }
@@ -3961,7 +4049,7 @@ header h1 .tagline {
} }
.bt-detail-address { .bt-detail-address {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: #00d4ff; color: #00d4ff;
} }
@@ -3975,7 +4063,7 @@ header h1 .tagline {
} }
.bt-detail-rssi-value { .bt-detail-rssi-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
} }
@@ -4070,7 +4158,7 @@ header h1 .tagline {
} }
.bt-detail-services-list { .bt-detail-services-list {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 8px; font-size: 8px;
color: var(--text-dim); color: var(--text-dim);
white-space: nowrap; white-space: nowrap;
@@ -4104,10 +4192,37 @@ header h1 .tagline {
.bt-device-list { .bt-device-list {
border-left-color: var(--accent-purple) !important; border-left-color: var(--accent-purple) !important;
display: flex;
flex-direction: column;
min-width: 280px;
max-width: 320px;
max-height: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.bt-device-list .wifi-device-list-content {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.bt-device-list .wifi-device-list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
} }
.bt-device-list .wifi-device-list-header h5 { .bt-device-list .wifi-device-list-header h5 {
color: var(--accent-purple); color: var(--accent-purple);
margin: 0;
font-size: 13px;
font-weight: 600;
} }
/* Bluetooth Device Filters */ /* Bluetooth Device Filters */
@@ -4117,6 +4232,7 @@ header h1 .tagline {
padding: 8px 12px; padding: 8px 12px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
flex-wrap: wrap; flex-wrap: wrap;
flex-shrink: 0;
} }
.bt-filter-btn { .bt-filter-btn {
@@ -4289,7 +4405,7 @@ header h1 .tagline {
} }
.bt-rssi-value { .bt-rssi-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
min-width: 28px; min-width: 28px;
@@ -4648,7 +4764,7 @@ header h1 .tagline {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
font-size: 10px; font-size: 10px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
.security-legend-item { .security-legend-item {
@@ -4695,7 +4811,7 @@ header h1 .tagline {
} }
.signal-value { .signal-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 28px; font-size: 28px;
color: var(--accent-cyan); color: var(--accent-cyan);
text-shadow: 0 0 10px var(--accent-cyan-dim); text-shadow: 0 0 10px var(--accent-cyan-dim);
@@ -4848,7 +4964,7 @@ body::before {
color: #000; color: #000;
border: none; border: none;
padding: 12px 40px; padding: 12px 40px;
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
letter-spacing: 2px; letter-spacing: 2px;
@@ -5211,7 +5327,7 @@ body::before {
.meter-value { .meter-value {
font-size: 10px; font-size: 10px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
color: var(--text-secondary); color: var(--text-secondary);
width: 50px; width: 50px;
text-align: right; text-align: right;
@@ -5368,7 +5484,7 @@ body::before {
} }
.freq-digits { .freq-digits {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 56px; font-size: 56px;
font-weight: 700; font-weight: 700;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -5389,7 +5505,7 @@ body::before {
} }
.freq-unit { .freq-unit {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 20px; font-size: 20px;
color: var(--text-secondary); color: var(--text-secondary);
margin-left: 8px; margin-left: 8px;
@@ -5533,7 +5649,7 @@ body::before {
} }
.knob-value { .knob-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -5658,7 +5774,7 @@ body::before {
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-secondary); color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
border-radius: 4px; border-radius: 4px;
@@ -5720,13 +5836,13 @@ body::before {
} }
.signal-arc-label { .signal-arc-label {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 8px; font-size: 8px;
fill: var(--text-muted); fill: var(--text-muted);
} }
.signal-arc-value { .signal-arc-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
fill: var(--accent-cyan); fill: var(--accent-cyan);
@@ -5758,7 +5874,7 @@ body::before {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
@@ -5894,7 +6010,7 @@ body::before {
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
padding: 10px 15px; padding: 10px 15px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -5983,7 +6099,7 @@ body::before {
} }
.module-header { .module-header {
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -6008,7 +6124,7 @@ body::before {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
color: var(--text-primary); color: var(--text-primary);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
padding: 6px 8px; padding: 6px 8px;
text-align: center; text-align: center;
@@ -6047,16 +6163,27 @@ body::before {
cursor: not-allowed; cursor: not-allowed;
} }
.radio-action-btn.scan { .radio-action-btn.scan,
.radio-action-btn.listen {
background: var(--accent-green); background: var(--accent-green);
border-color: var(--accent-green); border-color: var(--accent-green);
color: #000; color: #000;
} }
.radio-action-btn.scan:hover:not(:disabled) { .radio-action-btn.scan:hover:not(:disabled),
.radio-action-btn.listen:hover:not(:disabled) {
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
} }
.radio-action-btn.listen.active {
background: var(--accent-red);
border-color: var(--accent-red);
}
.radio-action-btn.listen.active:hover:not(:disabled) {
box-shadow: 0 0 20px var(--accent-red-dim);
}
/* Statistics Box */ /* Statistics Box */
.stat-box { .stat-box {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
@@ -6066,7 +6193,7 @@ body::before {
} }
.stat-value { .stat-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 22px; font-size: 22px;
font-weight: bold; font-weight: bold;
} }
@@ -6114,7 +6241,7 @@ body::before {
.tune-btn { .tune-btn {
padding: 4px 8px; padding: 4px 8px;
font-size: 10px; font-size: 10px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-secondary); color: var(--text-secondary);
@@ -6144,13 +6271,13 @@ body::before {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Listening Mode Selector Buttons */ /* Listening Mode Selector Buttons */
.radio-mode-btn { .radio-mode-btn {
padding: 12px 24px; padding: 12px 24px;
font-family: 'Orbitron', 'JetBrains Mono', monospace; font-family: 'Orbitron', 'Space Mono', monospace;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -6191,7 +6318,7 @@ body::before {
/* Frequency Preset Buttons */ /* Frequency Preset Buttons */
.preset-freq-btn { .preset-freq-btn {
padding: 8px 14px; padding: 8px 14px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
@@ -6255,4 +6382,4 @@ body::before {
[data-animations="off"] .logo-dot, [data-animations="off"] .logo-dot,
[data-animations="off"] .welcome-logo { [data-animations="off"] .welcome-logo {
animation: none !important; animation: none !important;
} }
+6 -6
View File
@@ -37,7 +37,7 @@
/* Typography */ /* Typography */
.landing-title { .landing-title {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 2.2rem; font-size: 2.2rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.4em; letter-spacing: 0.4em;
@@ -48,7 +48,7 @@
} }
.landing-tagline { .landing-tagline {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
color: var(--accent-cyan); color: var(--accent-cyan);
font-size: 0.9rem; font-size: 0.9rem;
letter-spacing: 0.15em; letter-spacing: 0.15em;
@@ -71,7 +71,7 @@
/* Hacker Style Error */ /* Hacker Style Error */
.flash-error { .flash-error {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--accent-red); color: var(--accent-red);
background: rgba(239, 68, 68, 0.1); background: rgba(239, 68, 68, 0.1);
@@ -94,7 +94,7 @@
color: var(--accent-cyan); color: var(--accent-cyan);
padding: 12px; padding: 12px;
margin-bottom: 15px; margin-bottom: 15px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
outline: none; outline: none;
box-sizing: border-box; /* Crucial for visibility */ box-sizing: border-box; /* Crucial for visibility */
@@ -106,7 +106,7 @@
border: 2px solid var(--accent-cyan); border: 2px solid var(--accent-cyan);
color: var(--accent-cyan); color: var(--accent-cyan);
padding: 15px; padding: 15px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-weight: 600; font-weight: 600;
letter-spacing: 3px; letter-spacing: 3px;
cursor: pointer; cursor: pointer;
@@ -116,7 +116,7 @@
.landing-version { .landing-version {
margin-top: 25px; margin-top: 25px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.3);
letter-spacing: 2px; letter-spacing: 2px;
+328 -328
View File
@@ -1,328 +1,328 @@
/* APRS Function Bar (Stats Strip) Styles */ /* APRS Function Bar (Stats Strip) Styles */
.aprs-strip { .aprs-strip {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%); background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 6px; border-radius: 6px;
padding: 6px 12px; padding: 6px 12px;
margin-bottom: 10px; margin-bottom: 10px;
overflow-x: auto; overflow-x: auto;
} }
.aprs-strip-inner { .aprs-strip-inner {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
min-width: max-content; min-width: max-content;
} }
.aprs-strip .strip-stat { .aprs-strip .strip-stat {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 4px 10px; padding: 4px 10px;
background: rgba(74, 158, 255, 0.05); background: rgba(74, 158, 255, 0.05);
border: 1px solid rgba(74, 158, 255, 0.15); border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px; border-radius: 4px;
min-width: 55px; min-width: 55px;
} }
.aprs-strip .strip-stat:hover { .aprs-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1); background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3); border-color: rgba(74, 158, 255, 0.3);
} }
.aprs-strip .strip-value { .aprs-strip .strip-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--accent-cyan); color: var(--accent-cyan);
line-height: 1.2; line-height: 1.2;
} }
.aprs-strip .strip-label { .aprs-strip .strip-label {
font-size: 8px; font-size: 8px;
font-weight: 600; font-weight: 600;
color: var(--text-dim); color: var(--text-dim);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
margin-top: 1px; margin-top: 1px;
} }
.aprs-strip .strip-divider { .aprs-strip .strip-divider {
width: 1px; width: 1px;
height: 28px; height: 28px;
background: var(--border-color); background: var(--border-color);
margin: 0 4px; margin: 0 4px;
} }
/* Signal stat coloring */ /* Signal stat coloring */
.aprs-strip .signal-stat.good .strip-value { color: var(--accent-green); } .aprs-strip .signal-stat.good .strip-value { color: var(--accent-green); }
.aprs-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); } .aprs-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); }
.aprs-strip .signal-stat.poor .strip-value { color: var(--accent-red); } .aprs-strip .signal-stat.poor .strip-value { color: var(--accent-red); }
/* Controls */ /* Controls */
.aprs-strip .strip-control { .aprs-strip .strip-control {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
} }
.aprs-strip .strip-select { .aprs-strip .strip-select {
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-primary); color: var(--text-primary);
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
font-size: 10px; font-size: 10px;
cursor: pointer; cursor: pointer;
} }
.aprs-strip .strip-select:hover { .aprs-strip .strip-select:hover {
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
} }
.aprs-strip .strip-input-label { .aprs-strip .strip-input-label {
font-size: 9px; font-size: 9px;
color: var(--text-muted); color: var(--text-muted);
font-weight: 600; font-weight: 600;
} }
.aprs-strip .strip-input { .aprs-strip .strip-input {
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-primary); color: var(--text-primary);
padding: 4px 6px; padding: 4px 6px;
border-radius: 4px; border-radius: 4px;
font-size: 10px; font-size: 10px;
width: 50px; width: 50px;
text-align: center; text-align: center;
} }
.aprs-strip .strip-input:hover, .aprs-strip .strip-input:hover,
.aprs-strip .strip-input:focus { .aprs-strip .strip-input:focus {
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
outline: none; outline: none;
} }
/* Tool Status Indicators */ /* Tool Status Indicators */
.aprs-strip .strip-tools { .aprs-strip .strip-tools {
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.aprs-strip .strip-tool { .aprs-strip .strip-tool {
font-size: 9px; font-size: 9px;
font-weight: 600; font-weight: 600;
padding: 3px 6px; padding: 3px 6px;
border-radius: 3px; border-radius: 3px;
background: rgba(255, 59, 48, 0.2); background: rgba(255, 59, 48, 0.2);
color: var(--accent-red); color: var(--accent-red);
border: 1px solid rgba(255, 59, 48, 0.3); border: 1px solid rgba(255, 59, 48, 0.3);
} }
.aprs-strip .strip-tool.ok { .aprs-strip .strip-tool.ok {
background: rgba(0, 255, 136, 0.1); background: rgba(0, 255, 136, 0.1);
color: var(--accent-green); color: var(--accent-green);
border-color: rgba(0, 255, 136, 0.3); border-color: rgba(0, 255, 136, 0.3);
} }
/* Buttons */ /* Buttons */
.aprs-strip .strip-btn { .aprs-strip .strip-btn {
background: rgba(74, 158, 255, 0.1); background: rgba(74, 158, 255, 0.1);
border: 1px solid rgba(74, 158, 255, 0.2); border: 1px solid rgba(74, 158, 255, 0.2);
color: var(--text-primary); color: var(--text-primary);
padding: 6px 12px; padding: 6px 12px;
border-radius: 4px; border-radius: 4px;
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
} }
.aprs-strip .strip-btn:hover:not(:disabled) { .aprs-strip .strip-btn:hover:not(:disabled) {
background: rgba(74, 158, 255, 0.2); background: rgba(74, 158, 255, 0.2);
border-color: rgba(74, 158, 255, 0.4); border-color: rgba(74, 158, 255, 0.4);
} }
.aprs-strip .strip-btn.primary { .aprs-strip .strip-btn.primary {
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%); background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
border: none; border: none;
color: #000; color: #000;
} }
.aprs-strip .strip-btn.primary:hover:not(:disabled) { .aprs-strip .strip-btn.primary:hover:not(:disabled) {
filter: brightness(1.1); filter: brightness(1.1);
} }
.aprs-strip .strip-btn.stop { .aprs-strip .strip-btn.stop {
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%); background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
border: none; border: none;
color: #fff; color: #fff;
} }
.aprs-strip .strip-btn.stop:hover:not(:disabled) { .aprs-strip .strip-btn.stop:hover:not(:disabled) {
filter: brightness(1.1); filter: brightness(1.1);
} }
.aprs-strip .strip-btn:disabled { .aprs-strip .strip-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
/* Status indicator */ /* Status indicator */
.aprs-strip .strip-status { .aprs-strip .strip-status {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 4px 8px; padding: 4px 8px;
background: rgba(0,0,0,0.2); background: rgba(0,0,0,0.2);
border-radius: 4px; border-radius: 4px;
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
color: var(--text-secondary); color: var(--text-secondary);
} }
.aprs-strip .status-dot { .aprs-strip .status-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--text-muted); background: var(--text-muted);
} }
.aprs-strip .status-dot.listening { .aprs-strip .status-dot.listening {
background: var(--accent-cyan); background: var(--accent-cyan);
animation: aprs-strip-pulse 1.5s ease-in-out infinite; animation: aprs-strip-pulse 1.5s ease-in-out infinite;
} }
.aprs-strip .status-dot.tracking { .aprs-strip .status-dot.tracking {
background: var(--accent-green); background: var(--accent-green);
animation: aprs-strip-pulse 1.5s ease-in-out infinite; animation: aprs-strip-pulse 1.5s ease-in-out infinite;
} }
.aprs-strip .status-dot.error { .aprs-strip .status-dot.error {
background: var(--accent-red); background: var(--accent-red);
} }
@keyframes aprs-strip-pulse { @keyframes aprs-strip-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; } 0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
50% { opacity: 0.6; box-shadow: none; } 50% { opacity: 0.6; box-shadow: none; }
} }
/* Time display */ /* Time display */
.aprs-strip .strip-time { .aprs-strip .strip-time {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
padding: 4px 8px; padding: 4px 8px;
background: rgba(0,0,0,0.2); background: rgba(0,0,0,0.2);
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
} }
/* APRS Status Bar Styles (Sidebar - legacy) */ /* APRS Status Bar Styles (Sidebar - legacy) */
.aprs-status-bar { .aprs-status-bar {
margin-top: 12px; margin-top: 12px;
padding: 10px; padding: 10px;
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
} }
.aprs-status-indicator { .aprs-status-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.aprs-status-dot { .aprs-status-dot {
width: 10px; width: 10px;
height: 10px; height: 10px;
border-radius: 50%; border-radius: 50%;
background: var(--text-muted); background: var(--text-muted);
} }
.aprs-status-dot.standby { background: var(--text-muted); } .aprs-status-dot.standby { background: var(--text-muted); }
.aprs-status-dot.listening { .aprs-status-dot.listening {
background: var(--accent-cyan); background: var(--accent-cyan);
animation: aprs-pulse 1.5s ease-in-out infinite; animation: aprs-pulse 1.5s ease-in-out infinite;
} }
.aprs-status-dot.tracking { background: var(--accent-green); } .aprs-status-dot.tracking { background: var(--accent-green); }
.aprs-status-dot.error { background: var(--accent-red); } .aprs-status-dot.error { background: var(--accent-red); }
@keyframes aprs-pulse { @keyframes aprs-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); } 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
50% { opacity: 0.6; box-shadow: 0 0 8px 4px rgba(74, 158, 255, 0.3); } 50% { opacity: 0.6; box-shadow: 0 0 8px 4px rgba(74, 158, 255, 0.3); }
} }
.aprs-status-text { .aprs-status-text {
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
letter-spacing: 1px; letter-spacing: 1px;
} }
.aprs-status-stats { .aprs-status-stats {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
font-size: 9px; font-size: 9px;
} }
.aprs-stat { .aprs-stat {
color: var(--text-secondary); color: var(--text-secondary);
} }
.aprs-stat-label { .aprs-stat-label {
color: var(--text-muted); color: var(--text-muted);
} }
/* Signal Meter Styles */ /* Signal Meter Styles */
.aprs-signal-meter { .aprs-signal-meter {
margin-top: 12px; margin-top: 12px;
padding: 10px; padding: 10px;
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
} }
.aprs-meter-header { .aprs-meter-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.aprs-meter-label { .aprs-meter-label {
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
letter-spacing: 1px; letter-spacing: 1px;
color: var(--text-secondary); color: var(--text-secondary);
} }
.aprs-meter-value { .aprs-meter-value {
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
font-family: monospace; font-family: monospace;
color: var(--accent-cyan); color: var(--accent-cyan);
min-width: 24px; min-width: 24px;
} }
.aprs-meter-burst { .aprs-meter-burst {
font-size: 9px; font-size: 9px;
font-weight: bold; font-weight: bold;
color: var(--accent-yellow); color: var(--accent-yellow);
background: rgba(255, 193, 7, 0.2); background: rgba(255, 193, 7, 0.2);
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
animation: burst-flash 0.3s ease-out; animation: burst-flash 0.3s ease-out;
} }
@keyframes burst-flash { @keyframes burst-flash {
0% { opacity: 1; transform: scale(1.1); } 0% { opacity: 1; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); } 100% { opacity: 1; transform: scale(1); }
} }
.aprs-meter-bar-container { .aprs-meter-bar-container {
position: relative; position: relative;
height: 16px; height: 16px;
background: rgba(0,0,0,0.4); background: rgba(0,0,0,0.4);
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
margin-bottom: 4px; margin-bottom: 4px;
} }
.aprs-meter-bar { .aprs-meter-bar {
height: 100%; height: 100%;
width: 0%; width: 0%;
background: linear-gradient(90deg, background: linear-gradient(90deg,
var(--accent-green) 0%, var(--accent-green) 0%,
var(--accent-cyan) 50%, var(--accent-cyan) 50%,
var(--accent-yellow) 75%, var(--accent-yellow) 75%,
var(--accent-red) 100% var(--accent-red) 100%
); );
border-radius: 3px; border-radius: 3px;
transition: width 0.1s ease-out; transition: width 0.1s ease-out;
} }
.aprs-meter-bar.no-signal { .aprs-meter-bar.no-signal {
opacity: 0.3; opacity: 0.3;
} }
.aprs-meter-ticks { .aprs-meter-ticks {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 8px; font-size: 8px;
color: var(--text-muted); color: var(--text-muted);
padding: 0 2px; padding: 0 2px;
} }
.aprs-meter-status { .aprs-meter-status {
font-size: 9px; font-size: 9px;
color: var(--text-muted); color: var(--text-muted);
text-align: center; text-align: center;
margin-top: 6px; margin-top: 6px;
} }
.aprs-meter-status.active { .aprs-meter-status.active {
color: var(--accent-green); color: var(--accent-green);
} }
.aprs-meter-status.no-signal { .aprs-meter-status.no-signal {
color: var(--accent-yellow); color: var(--accent-yellow);
} }
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -27,7 +27,7 @@
} }
.spy-stations-title { .spy-stations-title {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
@@ -101,7 +101,7 @@
} }
.spy-station-name { .spy-station-name {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
@@ -117,7 +117,7 @@
/* Type Badge */ /* Type Badge */
.spy-station-badge { .spy-station-badge {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 9px; font-size: 9px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -173,7 +173,7 @@
} }
.spy-meta-mode { .spy-meta-mode {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
color: var(--accent-orange); color: var(--accent-orange);
} }
@@ -186,7 +186,7 @@
} }
.spy-freq-list { .spy-freq-list {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-cyan); color: var(--accent-cyan);
line-height: 1.6; line-height: 1.6;
@@ -199,7 +199,7 @@
} }
.spy-freq-item { .spy-freq-item {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
color: var(--accent-cyan); color: var(--accent-cyan);
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -236,7 +236,7 @@
} }
.spy-freq-select { .spy-freq-select {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
padding: 6px 8px; padding: 6px 8px;
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -273,7 +273,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
+876 -876
View File
File diff suppressed because it is too large Load Diff
+1463 -1463
View File
File diff suppressed because it is too large Load Diff
+660 -660
View File
File diff suppressed because it is too large Load Diff
+49 -11
View File
@@ -5,6 +5,8 @@
} }
:root { :root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10; --bg-dark: #0a0c10;
--bg-panel: #0f1218; --bg-panel: #0f1218;
--bg-card: #151a23; --bg-card: #151a23;
@@ -23,7 +25,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--font-sans);
background: var(--bg-dark); background: var(--bg-dark);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
@@ -93,7 +95,7 @@ body {
} }
.logo { .logo {
font-family: 'Inter', sans-serif; font-family: var(--font-sans);
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
letter-spacing: 3px; letter-spacing: 3px;
@@ -142,7 +144,7 @@ body {
border: 1px solid rgba(0, 212, 255, 0.3); border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 4px; border-radius: 4px;
padding: 4px 10px; padding: 4px 10px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -162,10 +164,45 @@ body {
.status-bar { .status-bar {
display: flex; display: flex;
gap: 20px; gap: 12px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
flex-wrap: nowrap;
}
.location-selector {
display: flex;
align-items: center;
gap: 8px;
}
.location-selector .location-label {
color: var(--text-secondary);
font-size: 10px;
}
.location-selector .location-select {
padding: 4px 8px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 11px;
font-family: var(--font-mono);
}
.location-selector .location-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 6px var(--accent-green);
}
.location-selector .location-status-dot.offline {
background: var(--accent-red);
box-shadow: 0 0 6px var(--accent-red);
} }
.status-item { .status-item {
@@ -211,6 +248,7 @@ body {
} }
/* Main dashboard grid */ /* Main dashboard grid */
/* Header ~52px + Nav 44px = ~96px, using 100px for safety */
.dashboard { .dashboard {
position: relative; position: relative;
z-index: 10; z-index: 10;
@@ -218,7 +256,7 @@ body {
grid-template-columns: 1fr 1fr 340px; grid-template-columns: 1fr 1fr 340px;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
gap: 0; gap: 0;
height: calc(100vh - 60px); height: calc(100vh - 100px);
min-height: 500px; min-height: 500px;
} }
@@ -457,7 +495,7 @@ body {
} }
.telemetry-value { .telemetry-value {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
color: var(--accent-cyan); color: var(--accent-cyan);
} }
@@ -543,7 +581,7 @@ body {
} }
.pass-time { .pass-time {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Bottom controls bar */ /* Bottom controls bar */
@@ -579,7 +617,7 @@ body {
border: 1px solid rgba(0, 212, 255, 0.3); border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 4px; border-radius: 4px;
color: var(--accent-cyan); color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -696,7 +734,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto; height: auto;
min-height: calc(100vh - 60px); min-height: calc(100vh - 100px);
} }
.polar-container, .polar-container,
@@ -748,4 +786,4 @@ body.embedded .panel {
body.embedded .controls-bar { body.embedded .controls-bar {
padding: 10px 15px; padding: 10px 15px;
} }
+444 -427
View File
@@ -1,427 +1,444 @@
/* Settings Modal Styles */ /* Settings Modal Styles */
.settings-modal { .settings-modal {
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
z-index: 10000; z-index: 10000;
overflow-y: auto; overflow-y: auto;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
.settings-modal.active { .settings-modal.active {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
padding: 40px 20px; padding: 40px 20px;
} }
.settings-content { .settings-content {
background: var(--bg-dark, #0a0a0f); background: var(--bg-dark, #0a0a0f);
border: 1px solid var(--border-color, #1a1a2e); border: 1px solid var(--border-color, #1a1a2e);
border-radius: 8px; border-radius: 8px;
max-width: 600px; max-width: 600px;
width: 100%; width: 100%;
position: relative; position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
} }
.settings-header { .settings-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid var(--border-color, #1a1a2e); border-bottom: 1px solid var(--border-color, #1a1a2e);
} }
.settings-header h2 { .settings-header h2 {
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.settings-header h2 .icon { .settings-header h2 .icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
} }
.settings-close { .settings-close {
background: none; background: none;
border: none; border: none;
color: var(--text-muted, #666); color: var(--text-muted, #666);
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
line-height: 1; line-height: 1;
transition: color 0.2s; transition: color 0.2s;
} }
.settings-close:hover { .settings-close:hover {
color: var(--accent-red, #ff4444); color: var(--accent-red, #ff4444);
} }
/* Settings Tabs */ /* Settings Tabs */
.settings-tabs { .settings-tabs {
display: flex; display: flex;
border-bottom: 1px solid var(--border-color, #1a1a2e); border-bottom: 1px solid var(--border-color, #1a1a2e);
padding: 0 20px; padding: 0 20px;
gap: 4px; gap: 4px;
} }
.settings-tab { .settings-tab {
background: none; background: none;
border: none; border: none;
padding: 12px 16px; padding: 12px 16px;
color: var(--text-muted, #666); color: var(--text-muted, #666);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
transition: color 0.2s; transition: color 0.2s;
} }
.settings-tab:hover { .settings-tab:hover {
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
} }
.settings-tab.active { .settings-tab.active {
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
} }
.settings-tab.active::after { .settings-tab.active::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -1px; bottom: -1px;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--accent-cyan, #00d4ff); background: var(--accent-cyan, #00d4ff);
} }
/* Settings Sections */ /* Settings Sections */
.settings-section { .settings-section {
display: none; display: none;
padding: 20px; padding: 20px;
} }
.settings-section.active { .settings-section.active {
display: block; display: block;
} }
.settings-group { .settings-group {
margin-bottom: 24px; margin-bottom: 24px;
} }
.settings-group:last-child { .settings-group:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.settings-group-title { .settings-group-title {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: var(--text-muted, #666); color: var(--text-muted, #666);
margin-bottom: 12px; margin-bottom: 12px;
} }
/* Settings Row */ /* Settings Row */
.settings-row { .settings-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05);
} }
.settings-row:last-child { .settings-row:last-child {
border-bottom: none; border-bottom: none;
} }
.settings-label { .settings-label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} }
.settings-label-text { .settings-label-text {
font-size: 13px; font-size: 13px;
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
} }
.settings-label-desc { .settings-label-desc {
font-size: 11px; font-size: 11px;
color: var(--text-muted, #666); color: var(--text-muted, #666);
} }
/* Toggle Switch */ /* Toggle Switch */
.toggle-switch { .toggle-switch {
position: relative; position: relative;
width: 44px; width: 44px;
height: 24px; height: 24px;
flex-shrink: 0; flex-shrink: 0;
} }
.toggle-switch input { .toggle-switch input {
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
} }
.toggle-slider { .toggle-slider {
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: var(--bg-tertiary, #1a1a2e); background-color: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e); border: 1px solid var(--border-color, #2a2a3e);
transition: 0.3s; transition: 0.3s;
border-radius: 24px; border-radius: 24px;
} }
.toggle-slider:before { .toggle-slider:before {
position: absolute; position: absolute;
content: ""; content: "";
height: 18px; height: 18px;
width: 18px; width: 18px;
left: 2px; left: 2px;
bottom: 2px; bottom: 2px;
background-color: var(--text-muted, #666); background-color: var(--text-muted, #666);
transition: 0.3s; transition: 0.3s;
border-radius: 50%; border-radius: 50%;
} }
.toggle-switch input:checked + .toggle-slider { .toggle-switch input:checked + .toggle-slider {
background-color: var(--accent-cyan, #00d4ff); background-color: var(--accent-cyan, #00d4ff);
border-color: var(--accent-cyan, #00d4ff); border-color: var(--accent-cyan, #00d4ff);
} }
.toggle-switch input:checked + .toggle-slider:before { .toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px); transform: translateX(20px);
background-color: white; background-color: white;
} }
.toggle-switch input:focus + .toggle-slider { .toggle-switch input:focus + .toggle-slider {
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3); box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
} }
/* Select Dropdown */ /* Select Dropdown */
.settings-select { .settings-select {
background: var(--bg-tertiary, #1a1a2e); background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e); border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px; border-radius: 4px;
padding: 8px 12px; padding: 8px 12px;
font-size: 13px; font-size: 13px;
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
min-width: 160px; min-width: 160px;
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 8px center; background-position: right 8px center;
padding-right: 32px; padding-right: 32px;
} }
.settings-select:focus { .settings-select:focus {
outline: none; outline: none;
border-color: var(--accent-cyan, #00d4ff); border-color: var(--accent-cyan, #00d4ff);
} }
/* Text Input */ /* Text Input */
.settings-input { .settings-input {
background: var(--bg-tertiary, #1a1a2e); background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e); border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px; border-radius: 4px;
padding: 8px 12px; padding: 8px 12px;
font-size: 13px; font-size: 13px;
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
width: 200px; width: 200px;
} }
.settings-input:focus { .settings-input:focus {
outline: none; outline: none;
border-color: var(--accent-cyan, #00d4ff); border-color: var(--accent-cyan, #00d4ff);
} }
.settings-input::placeholder { .settings-input::placeholder {
color: var(--text-muted, #666); color: var(--text-muted, #666);
} }
/* Asset Status */ /* Asset Status */
.asset-status { .asset-status {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-top: 12px; margin-top: 12px;
padding: 12px; padding: 12px;
background: var(--bg-secondary, #0f0f1a); background: var(--bg-secondary, #0f0f1a);
border-radius: 6px; border-radius: 6px;
} }
.asset-status-row { .asset-status-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 12px; font-size: 12px;
} }
.asset-name { .asset-name {
color: var(--text-muted, #888); color: var(--text-muted, #888);
} }
.asset-badge { .asset-badge {
padding: 2px 8px; padding: 2px 8px;
border-radius: 10px; border-radius: 10px;
font-size: 10px; font-size: 10px;
font-weight: 500; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
} }
.asset-badge.available { .asset-badge.available {
background: rgba(0, 255, 136, 0.15); background: rgba(0, 255, 136, 0.15);
color: var(--accent-green, #00ff88); color: var(--accent-green, #00ff88);
} }
.asset-badge.missing { .asset-badge.missing {
background: rgba(255, 68, 68, 0.15); background: rgba(255, 68, 68, 0.15);
color: var(--accent-red, #ff4444); color: var(--accent-red, #ff4444);
} }
.asset-badge.checking { .asset-badge.checking {
background: rgba(255, 170, 0, 0.15); background: rgba(255, 170, 0, 0.15);
color: var(--accent-orange, #ffaa00); color: var(--accent-orange, #ffaa00);
} }
/* Check Assets Button */ /* Check Assets Button */
.check-assets-btn { .check-assets-btn {
background: var(--bg-tertiary, #1a1a2e); background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e); border: 1px solid var(--border-color, #2a2a3e);
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
padding: 8px 16px; padding: 8px 16px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
margin-top: 12px; margin-top: 12px;
transition: all 0.2s; transition: all 0.2s;
} }
.check-assets-btn:hover { .check-assets-btn:hover {
border-color: var(--accent-cyan, #00d4ff); border-color: var(--accent-cyan, #00d4ff);
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
} }
.check-assets-btn:disabled { .check-assets-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
/* About Section */ /* GPS Detection Spinner */
.about-info { .detecting-spinner {
font-size: 13px; display: inline-block;
color: var(--text-muted, #888); width: 12px;
line-height: 1.6; height: 12px;
} border: 2px solid currentColor;
border-top-color: transparent;
.about-info p { border-radius: 50%;
margin: 0 0 12px 0; animation: detecting-spin 0.8s linear infinite;
} vertical-align: middle;
margin-right: 6px;
.about-info a { }
color: var(--accent-cyan, #00d4ff);
text-decoration: none; @keyframes detecting-spin {
} to { transform: rotate(360deg); }
}
.about-info a:hover {
text-decoration: underline; /* About Section */
} .about-info {
font-size: 13px;
.about-version { color: var(--text-muted, #888);
font-family: 'JetBrains Mono', monospace; line-height: 1.6;
color: var(--accent-cyan, #00d4ff); }
}
.about-info p {
/* Donate Button */ margin: 0 0 12px 0;
.donate-btn { }
display: inline-flex;
align-items: center; .about-info a {
justify-content: center; color: var(--accent-cyan, #00d4ff);
padding: 10px 20px; text-decoration: none;
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%); }
border: none;
border-radius: 6px; .about-info a:hover {
color: #000; text-decoration: underline;
font-size: 13px; }
font-weight: 600;
text-decoration: none; .about-version {
cursor: pointer; font-family: var(--font-mono);
transition: all 0.2s ease; color: var(--accent-cyan, #00d4ff);
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3); }
}
/* Donate Button */
.donate-btn:hover { .donate-btn {
transform: translateY(-1px); display: inline-flex;
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4); align-items: center;
filter: brightness(1.1); justify-content: center;
} padding: 10px 20px;
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
.donate-btn:active { border: none;
transform: translateY(0); border-radius: 6px;
} color: #000;
font-size: 13px;
/* Tile Provider Custom URL */ font-weight: 600;
.custom-url-row { text-decoration: none;
margin-top: 8px; cursor: pointer;
padding-top: 8px; transition: all 0.2s ease;
} box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
}
.custom-url-row .settings-input {
width: 100%; .donate-btn:hover {
} transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
/* Info Callout */ filter: brightness(1.1);
.settings-info { }
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2); .donate-btn:active {
border-radius: 6px; transform: translateY(0);
padding: 12px; }
margin-top: 16px;
font-size: 12px; /* Tile Provider Custom URL */
color: var(--text-muted, #888); .custom-url-row {
} margin-top: 8px;
padding-top: 8px;
.settings-info strong { }
color: var(--accent-cyan, #00d4ff);
} .custom-url-row .settings-input {
width: 100%;
/* Responsive */ }
@media (max-width: 640px) {
.settings-modal.active { /* Info Callout */
padding: 20px 10px; .settings-info {
} background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2);
.settings-content { border-radius: 6px;
max-width: 100%; padding: 12px;
} margin-top: 16px;
font-size: 12px;
.settings-row { color: var(--text-muted, #888);
flex-direction: column; }
align-items: flex-start;
gap: 8px; .settings-info strong {
} color: var(--accent-cyan, #00d4ff);
}
.settings-select,
.settings-input { /* Responsive */
width: 100%; @media (max-width: 640px) {
} .settings-modal.active {
} padding: 20px 10px;
}
.settings-content {
max-width: 100%;
}
.settings-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.settings-select,
.settings-input {
width: 100%;
}
}
+5
View File
@@ -207,9 +207,14 @@ const ProximityRadar = (function() {
const pulseClass = isNew ? 'radar-dot-pulse' : ''; const pulseClass = isNew ? 'radar-dot-pulse' : '';
const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey; const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey;
// Hit area size (prevents hover flicker when scaling)
const hitAreaSize = Math.max(dotSize * 2, 15);
return ` return `
<g class="radar-device ${pulseClass}${isSelected ? ' selected' : ''}" data-device-key="${escapeAttr(device.device_key)}" <g class="radar-device ${pulseClass}${isSelected ? ' selected' : ''}" data-device-key="${escapeAttr(device.device_key)}"
transform="translate(${x}, ${y})" style="cursor: pointer;"> transform="translate(${x}, ${y})" style="cursor: pointer;">
<!-- Invisible hit area to prevent hover flicker -->
<circle class="radar-device-hitarea" r="${hitAreaSize}" fill="transparent" />
${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8"> ${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
<animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/> <animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/>
<animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/> <animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/>
+494 -494
View File
@@ -1,36 +1,36 @@
/** /**
* Intercept - Core Application Logic * Intercept - Core Application Logic
* Global state, mode switching, and shared functionality * Global state, mode switching, and shared functionality
*/ */
// ============== GLOBAL STATE ============== // ============== GLOBAL STATE ==============
// Mode state flags // Mode state flags
let eventSource = null; let eventSource = null;
let isRunning = false; let isRunning = false;
let isSensorRunning = false; let isSensorRunning = false;
let isAdsbRunning = false; let isAdsbRunning = false;
let isWifiRunning = false; let isWifiRunning = false;
let isBtRunning = false; let isBtRunning = false;
let currentMode = 'pager'; let currentMode = 'pager';
// Message counters // Message counters
let msgCount = 0; let msgCount = 0;
let pocsagCount = 0; let pocsagCount = 0;
let flexCount = 0; let flexCount = 0;
let sensorCount = 0; let sensorCount = 0;
let filteredCount = 0; let filteredCount = 0;
// Device list (populated from server via Jinja2) // Device list (populated from server via Jinja2)
let deviceList = []; let deviceList = [];
// Auto-scroll setting // Auto-scroll setting
let autoScroll = localStorage.getItem('autoScroll') !== 'false'; let autoScroll = localStorage.getItem('autoScroll') !== 'false';
// Mute setting // Mute setting
let muted = localStorage.getItem('audioMuted') === 'true'; let muted = localStorage.getItem('audioMuted') === 'true';
// Observer location (load from localStorage or default to London) // Observer location (load from localStorage or default to London)
let observerLocation = (function() { let observerLocation = (function() {
if (window.ObserverLocation && ObserverLocation.getForModule) { if (window.ObserverLocation && ObserverLocation.getForModule) {
return ObserverLocation.getForModule('observerLocation'); return ObserverLocation.getForModule('observerLocation');
@@ -44,464 +44,464 @@ let observerLocation = (function() {
} }
return { lat: 51.5074, lon: -0.1278 }; return { lat: 51.5074, lon: -0.1278 };
})(); })();
// Message storage for export // Message storage for export
let allMessages = []; let allMessages = [];
// Track unique sensor devices // Track unique sensor devices
let uniqueDevices = new Set(); let uniqueDevices = new Set();
// SDR device usage tracking // SDR device usage tracking
let sdrDeviceUsage = {}; let sdrDeviceUsage = {};
// ============== DISCLAIMER HANDLING ============== // ============== DISCLAIMER HANDLING ==============
function checkDisclaimer() { function checkDisclaimer() {
const accepted = localStorage.getItem('disclaimerAccepted'); const accepted = localStorage.getItem('disclaimerAccepted');
if (accepted === 'true') { if (accepted === 'true') {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden'); document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
} }
} }
function acceptDisclaimer() { function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true'); localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden'); document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
} }
function declineDisclaimer() { function declineDisclaimer() {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden'); document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden'); document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
} }
// ============== HEADER CLOCK ============== // ============== HEADER CLOCK ==============
function updateHeaderClock() { function updateHeaderClock() {
const now = new Date(); const now = new Date();
const utc = now.toISOString().substring(11, 19); const utc = now.toISOString().substring(11, 19);
document.getElementById('headerUtcTime').textContent = utc; document.getElementById('headerUtcTime').textContent = utc;
} }
// ============== MODE SWITCHING ============== // ============== MODE SWITCHING ==============
function switchMode(mode) { function switchMode(mode) {
// Stop any running scans when switching modes // Stop any running scans when switching modes
if (isRunning && typeof stopDecoding === 'function') stopDecoding(); if (isRunning && typeof stopDecoding === 'function') stopDecoding();
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding(); if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan(); if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan(); if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan(); if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
currentMode = mode; currentMode = mode;
// Remove active from all nav buttons, then add to the correct one // Remove active from all nav buttons, then add to the correct one
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
const modeMap = { const modeMap = {
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft', 'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth', 'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
'listening': 'listening', 'meshtastic': 'meshtastic' 'listening': 'listening', 'meshtastic': 'meshtastic'
}; };
document.querySelectorAll('.mode-nav-btn').forEach(btn => { document.querySelectorAll('.mode-nav-btn').forEach(btn => {
const label = btn.querySelector('.nav-label'); const label = btn.querySelector('.nav-label');
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) { if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
btn.classList.add('active'); btn.classList.add('active');
} }
}); });
// Toggle mode content visibility // Toggle mode content visibility
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager'); document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor'); document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft'); document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite'); document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi'); document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth'); document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening'); document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs'); document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm'); document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr'); document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations'); document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic'); document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
// Toggle stats visibility // Toggle stats visibility
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none'; document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none'; document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none'; document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none'; document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none'; document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
// Hide signal meter - individual panels show signal strength where needed // Hide signal meter - individual panels show signal strength where needed
document.getElementById('signalMeter').style.display = 'none'; document.getElementById('signalMeter').style.display = 'none';
// Show/hide dashboard buttons in nav bar // Show/hide dashboard buttons in nav bar
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none'; document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none'; document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
// Update active mode indicator // Update active mode indicator
const modeNames = { const modeNames = {
'pager': 'PAGER', 'pager': 'PAGER',
'sensor': '433MHZ', 'sensor': '433MHZ',
'aircraft': 'AIRCRAFT', 'aircraft': 'AIRCRAFT',
'satellite': 'SATELLITE', 'satellite': 'SATELLITE',
'wifi': 'WIFI', 'wifi': 'WIFI',
'bluetooth': 'BLUETOOTH', 'bluetooth': 'BLUETOOTH',
'listening': 'LISTENING POST', 'listening': 'LISTENING POST',
'tscm': 'TSCM', 'tscm': 'TSCM',
'aprs': 'APRS', 'aprs': 'APRS',
'meshtastic': 'MESHTASTIC' 'meshtastic': 'MESHTASTIC'
}; };
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode]; document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
// Update mobile nav buttons // Update mobile nav buttons
updateMobileNavButtons(mode); updateMobileNavButtons(mode);
// Close mobile drawer when mode is switched (on mobile) // Close mobile drawer when mode is switched (on mobile)
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') { if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
window.closeMobileDrawer(); window.closeMobileDrawer();
} }
// Toggle layout containers // Toggle layout containers
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none'; document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none'; document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
// Respect the "Show Radar Display" checkbox for aircraft mode // Respect the "Show Radar Display" checkbox for aircraft mode
const showRadar = document.getElementById('adsbEnableMap')?.checked; const showRadar = document.getElementById('adsbEnableMap')?.checked;
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none'; document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none'; document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none'; document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
// Update output panel title based on mode // Update output panel title based on mode
const titles = { const titles = {
'pager': 'Pager Decoder', 'pager': 'Pager Decoder',
'sensor': '433MHz Sensor Monitor', 'sensor': '433MHz Sensor Monitor',
'aircraft': 'ADS-B Aircraft Tracker', 'aircraft': 'ADS-B Aircraft Tracker',
'satellite': 'Satellite Monitor', 'satellite': 'Satellite Monitor',
'wifi': 'WiFi Scanner', 'wifi': 'WiFi Scanner',
'bluetooth': 'Bluetooth Scanner', 'bluetooth': 'Bluetooth Scanner',
'listening': 'Listening Post', 'listening': 'Listening Post',
'meshtastic': 'Meshtastic Mesh Monitor' 'meshtastic': 'Meshtastic Mesh Monitor'
}; };
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor'; document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
// Show/hide Device Intelligence for modes that use it // Show/hide Device Intelligence for modes that use it
const reconBtn = document.getElementById('reconBtn'); const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]'); const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') { if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
document.getElementById('reconPanel').style.display = 'none'; document.getElementById('reconPanel').style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none'; if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none'; if (intelBtn) intelBtn.style.display = 'none';
} else { } else {
if (reconBtn) reconBtn.style.display = 'inline-block'; if (reconBtn) reconBtn.style.display = 'inline-block';
if (intelBtn) intelBtn.style.display = 'inline-block'; if (intelBtn) intelBtn.style.display = 'inline-block';
if (typeof reconEnabled !== 'undefined' && reconEnabled) { if (typeof reconEnabled !== 'undefined' && reconEnabled) {
document.getElementById('reconPanel').style.display = 'block'; document.getElementById('reconPanel').style.display = 'block';
} }
} }
// Show RTL-SDR device section for modes that use it // Show RTL-SDR device section for modes that use it
document.getElementById('rtlDeviceSection').style.display = document.getElementById('rtlDeviceSection').style.display =
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none'; (mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
// Toggle mode-specific tool status displays // Toggle mode-specific tool status displays
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none'; document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none'; document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none'; document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
// Hide waterfall and output console for modes with their own visualizations // Hide waterfall and output console for modes with their own visualizations
document.querySelector('.waterfall-container').style.display = document.querySelector('.waterfall-container').style.display =
(mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block'; (mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
document.getElementById('output').style.display = document.getElementById('output').style.display =
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block'; (mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex'; document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
// Load interfaces and initialize visualizations when switching modes // Load interfaces and initialize visualizations when switching modes
if (mode === 'wifi') { if (mode === 'wifi') {
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces(); if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
if (typeof initRadar === 'function') initRadar(); if (typeof initRadar === 'function') initRadar();
if (typeof initWatchList === 'function') initWatchList(); if (typeof initWatchList === 'function') initWatchList();
} else if (mode === 'bluetooth') { } else if (mode === 'bluetooth') {
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces(); if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
if (typeof initBtRadar === 'function') initBtRadar(); if (typeof initBtRadar === 'function') initBtRadar();
} else if (mode === 'aircraft') { } else if (mode === 'aircraft') {
if (typeof checkAdsbTools === 'function') checkAdsbTools(); if (typeof checkAdsbTools === 'function') checkAdsbTools();
if (typeof initAircraftRadar === 'function') initAircraftRadar(); if (typeof initAircraftRadar === 'function') initAircraftRadar();
} else if (mode === 'satellite') { } else if (mode === 'satellite') {
if (typeof initPolarPlot === 'function') initPolarPlot(); if (typeof initPolarPlot === 'function') initPolarPlot();
if (typeof initSatelliteList === 'function') initSatelliteList(); if (typeof initSatelliteList === 'function') initSatelliteList();
} else if (mode === 'listening') { } else if (mode === 'listening') {
if (typeof checkScannerTools === 'function') checkScannerTools(); if (typeof checkScannerTools === 'function') checkScannerTools();
if (typeof checkAudioTools === 'function') checkAudioTools(); if (typeof checkAudioTools === 'function') checkAudioTools();
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect(); if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect(); if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
} else if (mode === 'meshtastic') { } else if (mode === 'meshtastic') {
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init(); if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
} }
} }
// ============== SECTION COLLAPSE ============== // ============== SECTION COLLAPSE ==============
function toggleSection(el) { function toggleSection(el) {
el.closest('.section').classList.toggle('collapsed'); el.closest('.section').classList.toggle('collapsed');
} }
// ============== THEME MANAGEMENT ============== // ============== THEME MANAGEMENT ==============
function toggleTheme() { function toggleTheme() {
const html = document.documentElement; const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme'); const currentTheme = html.getAttribute('data-theme') || 'dark';
const newTheme = currentTheme === 'light' ? 'dark' : 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme); html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme); localStorage.setItem('intercept-theme', newTheme);
// Update button text // Update button text
const btn = document.getElementById('themeToggle'); const btn = document.getElementById('themeToggle');
if (btn) { if (btn) {
btn.textContent = newTheme === 'light' ? '🌙' : '☀️'; btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
} }
} }
function loadTheme() { function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark'; const savedTheme = localStorage.getItem('intercept-theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme); document.documentElement.setAttribute('data-theme', savedTheme);
const btn = document.getElementById('themeToggle'); const btn = document.getElementById('themeToggle');
if (btn) { if (btn) {
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️'; btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
} }
} }
// ============== AUTO-SCROLL ============== // ============== AUTO-SCROLL ==============
function toggleAutoScroll() { function toggleAutoScroll() {
autoScroll = !autoScroll; autoScroll = !autoScroll;
localStorage.setItem('autoScroll', autoScroll); localStorage.setItem('autoScroll', autoScroll);
updateAutoScrollButton(); updateAutoScrollButton();
} }
function updateAutoScrollButton() { function updateAutoScrollButton() {
const btn = document.getElementById('autoScrollBtn'); const btn = document.getElementById('autoScrollBtn');
if (btn) { if (btn) {
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF'; btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
btn.classList.toggle('active', autoScroll); btn.classList.toggle('active', autoScroll);
} }
} }
// ============== SDR DEVICE MANAGEMENT ============== // ============== SDR DEVICE MANAGEMENT ==============
function getSelectedDevice() { function getSelectedDevice() {
return document.getElementById('deviceSelect').value; return document.getElementById('deviceSelect').value;
} }
function getSelectedSDRType() { function getSelectedSDRType() {
return document.getElementById('sdrTypeSelect').value; return document.getElementById('sdrTypeSelect').value;
} }
function reserveDevice(deviceIndex, modeId) { function reserveDevice(deviceIndex, modeId) {
sdrDeviceUsage[modeId] = deviceIndex; sdrDeviceUsage[modeId] = deviceIndex;
} }
function releaseDevice(modeId) { function releaseDevice(modeId) {
delete sdrDeviceUsage[modeId]; delete sdrDeviceUsage[modeId];
} }
function checkDeviceAvailability(requestingMode) { function checkDeviceAvailability(requestingMode) {
const selectedDevice = parseInt(getSelectedDevice()); const selectedDevice = parseInt(getSelectedDevice());
for (const [mode, device] of Object.entries(sdrDeviceUsage)) { for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
if (mode !== requestingMode && device === selectedDevice) { if (mode !== requestingMode && device === selectedDevice) {
alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`); alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`);
return false; return false;
} }
} }
return true; return true;
} }
// ============== BIAS-T SETTINGS ============== // ============== BIAS-T SETTINGS ==============
function saveBiasTSetting() { function saveBiasTSetting() {
const enabled = document.getElementById('biasT')?.checked || false; const enabled = document.getElementById('biasT')?.checked || false;
localStorage.setItem('biasTEnabled', enabled); localStorage.setItem('biasTEnabled', enabled);
} }
function getBiasTEnabled() { function getBiasTEnabled() {
return document.getElementById('biasT')?.checked || false; return document.getElementById('biasT')?.checked || false;
} }
function loadBiasTSetting() { function loadBiasTSetting() {
const saved = localStorage.getItem('biasTEnabled'); const saved = localStorage.getItem('biasTEnabled');
if (saved === 'true') { if (saved === 'true') {
const checkbox = document.getElementById('biasT'); const checkbox = document.getElementById('biasT');
if (checkbox) checkbox.checked = true; if (checkbox) checkbox.checked = true;
} }
} }
// ============== REMOTE SDR ============== // ============== REMOTE SDR ==============
function toggleRemoteSDR() { function toggleRemoteSDR() {
const useRemote = document.getElementById('useRemoteSDR').checked; const useRemote = document.getElementById('useRemoteSDR').checked;
const configDiv = document.getElementById('remoteSDRConfig'); const configDiv = document.getElementById('remoteSDRConfig');
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect'); const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
if (useRemote) { if (useRemote) {
configDiv.style.display = 'block'; configDiv.style.display = 'block';
localControls.forEach(el => el.disabled = true); localControls.forEach(el => el.disabled = true);
} else { } else {
configDiv.style.display = 'none'; configDiv.style.display = 'none';
localControls.forEach(el => el.disabled = false); localControls.forEach(el => el.disabled = false);
} }
} }
function getRemoteSDRConfig() { function getRemoteSDRConfig() {
const useRemote = document.getElementById('useRemoteSDR')?.checked; const useRemote = document.getElementById('useRemoteSDR')?.checked;
if (!useRemote) return null; if (!useRemote) return null;
const host = document.getElementById('rtlTcpHost')?.value || 'localhost'; const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234'); const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
if (!host || isNaN(port)) { if (!host || isNaN(port)) {
alert('Please enter valid rtl_tcp host and port'); alert('Please enter valid rtl_tcp host and port');
return false; return false;
} }
return { host, port }; return { host, port };
} }
// ============== OUTPUT DISPLAY ============== // ============== OUTPUT DISPLAY ==============
function showInfo(text) { function showInfo(text) {
const output = document.getElementById('output'); const output = document.getElementById('output');
if (!output) return; if (!output) return;
const placeholder = output.querySelector('.placeholder'); const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove(); if (placeholder) placeholder.remove();
const infoEl = document.createElement('div'); const infoEl = document.createElement('div');
infoEl.className = 'info-msg'; infoEl.className = 'info-msg';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;'; infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.textContent = text; infoEl.textContent = text;
output.insertBefore(infoEl, output.firstChild); output.insertBefore(infoEl, output.firstChild);
} }
function showError(text) { function showError(text) {
const output = document.getElementById('output'); const output = document.getElementById('output');
if (!output) return; if (!output) return;
const placeholder = output.querySelector('.placeholder'); const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove(); if (placeholder) placeholder.remove();
const errorEl = document.createElement('div'); const errorEl = document.createElement('div');
errorEl.className = 'error-msg'; errorEl.className = 'error-msg';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;'; errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.textContent = '⚠ ' + text; errorEl.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild); output.insertBefore(errorEl, output.firstChild);
} }
// ============== INITIALIZATION ============== // ============== INITIALIZATION ==============
// ============== MOBILE NAVIGATION ============== // ============== MOBILE NAVIGATION ==============
function initMobileNav() { function initMobileNav() {
const hamburgerBtn = document.getElementById('hamburgerBtn'); const hamburgerBtn = document.getElementById('hamburgerBtn');
const sidebar = document.getElementById('mainSidebar'); const sidebar = document.getElementById('mainSidebar');
const overlay = document.getElementById('drawerOverlay'); const overlay = document.getElementById('drawerOverlay');
if (!hamburgerBtn || !sidebar || !overlay) return; if (!hamburgerBtn || !sidebar || !overlay) return;
function openDrawer() { function openDrawer() {
sidebar.classList.add('open'); sidebar.classList.add('open');
overlay.classList.add('visible'); overlay.classList.add('visible');
hamburgerBtn.classList.add('active'); hamburgerBtn.classList.add('active');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} }
function closeDrawer() { function closeDrawer() {
sidebar.classList.remove('open'); sidebar.classList.remove('open');
overlay.classList.remove('visible'); overlay.classList.remove('visible');
hamburgerBtn.classList.remove('active'); hamburgerBtn.classList.remove('active');
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
function toggleDrawer() { function toggleDrawer() {
if (sidebar.classList.contains('open')) { if (sidebar.classList.contains('open')) {
closeDrawer(); closeDrawer();
} else { } else {
openDrawer(); openDrawer();
} }
} }
hamburgerBtn.addEventListener('click', toggleDrawer); hamburgerBtn.addEventListener('click', toggleDrawer);
overlay.addEventListener('click', closeDrawer); overlay.addEventListener('click', closeDrawer);
// Close drawer when resizing to desktop // Close drawer when resizing to desktop
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) { if (window.innerWidth >= 1024) {
closeDrawer(); closeDrawer();
} }
}); });
// Expose for external use // Expose for external use
window.toggleMobileDrawer = toggleDrawer; window.toggleMobileDrawer = toggleDrawer;
window.closeMobileDrawer = closeDrawer; window.closeMobileDrawer = closeDrawer;
} }
function setViewportHeight() { function setViewportHeight() {
// Fix for iOS Safari address bar height // Fix for iOS Safari address bar height
const vh = window.innerHeight * 0.01; const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`); document.documentElement.style.setProperty('--vh', `${vh}px`);
} }
function updateMobileNavButtons(mode) { function updateMobileNavButtons(mode) {
// Update mobile nav bar buttons // Update mobile nav bar buttons
document.querySelectorAll('.mobile-nav-btn').forEach(btn => { document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
const btnMode = btn.getAttribute('data-mode'); const btnMode = btn.getAttribute('data-mode');
btn.classList.toggle('active', btnMode === mode); btn.classList.toggle('active', btnMode === mode);
}); });
} }
function initApp() { function initApp() {
// Check disclaimer // Check disclaimer
checkDisclaimer(); checkDisclaimer();
// Load theme // Load theme
loadTheme(); loadTheme();
// Start clock // Start clock
updateHeaderClock(); updateHeaderClock();
setInterval(updateHeaderClock, 1000); setInterval(updateHeaderClock, 1000);
// Load bias-T setting // Load bias-T setting
loadBiasTSetting(); loadBiasTSetting();
// Initialize observer location inputs // Initialize observer location inputs
const adsbLatInput = document.getElementById('adsbObsLat'); const adsbLatInput = document.getElementById('adsbObsLat');
const adsbLonInput = document.getElementById('adsbObsLon'); const adsbLonInput = document.getElementById('adsbObsLon');
const obsLatInput = document.getElementById('obsLat'); const obsLatInput = document.getElementById('obsLat');
const obsLonInput = document.getElementById('obsLon'); const obsLonInput = document.getElementById('obsLon');
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4); if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4); if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4); if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4); if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
// Update UI state // Update UI state
updateAutoScrollButton(); updateAutoScrollButton();
// Make sections collapsible // Make sections collapsible
document.querySelectorAll('.section h3').forEach(h3 => { document.querySelectorAll('.section h3').forEach(h3 => {
h3.addEventListener('click', function() { h3.addEventListener('click', function() {
this.parentElement.classList.toggle('collapsed'); this.parentElement.classList.toggle('collapsed');
}); });
}); });
// Collapse all sections by default (except SDR Device which is first) // Collapse all sections by default (except SDR Device which is first)
document.querySelectorAll('.section').forEach((section, index) => { document.querySelectorAll('.section').forEach((section, index) => {
if (index > 0) { if (index > 0) {
section.classList.add('collapsed'); section.classList.add('collapsed');
} }
}); });
// Initialize mobile navigation // Initialize mobile navigation
initMobileNav(); initMobileNav();
// Set viewport height for mobile browsers // Set viewport height for mobile browsers
setViewportHeight(); setViewportHeight();
window.addEventListener('resize', setViewportHeight); window.addEventListener('resize', setViewportHeight);
} }
// Run initialization when DOM is ready // Run initialization when DOM is ready
document.addEventListener('DOMContentLoaded', initApp); document.addEventListener('DOMContentLoaded', initApp);
+48
View File
@@ -0,0 +1,48 @@
(() => {
const dropdowns = Array.from(document.querySelectorAll('.mode-nav-dropdown'));
if (!dropdowns.length) return;
const closeAll = () => {
dropdowns.forEach((dropdown) => dropdown.classList.remove('open'));
};
const openDropdown = (dropdown) => {
if (!dropdown.classList.contains('open')) {
closeAll();
dropdown.classList.add('open');
}
};
document.addEventListener('click', (event) => {
const menuLink = event.target.closest('.mode-nav-dropdown-menu a');
if (menuLink) {
event.preventDefault();
event.stopPropagation();
window.location.href = menuLink.href;
return;
}
const button = event.target.closest('.mode-nav-dropdown-btn');
if (button) {
event.preventDefault();
const dropdown = button.closest('.mode-nav-dropdown');
if (!dropdown) return;
if (dropdown.classList.contains('open')) {
dropdown.classList.remove('open');
} else {
openDropdown(dropdown);
}
return;
}
if (!event.target.closest('.mode-nav-dropdown')) {
closeAll();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeAll();
}
});
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -97,7 +97,7 @@ const Meshtastic = (function() {
/** /**
* Initialize the Leaflet map * Initialize the Leaflet map
*/ */
function initMap() { async function initMap() {
if (meshMap) return; if (meshMap) return;
const mapContainer = document.getElementById('meshMap'); const mapContainer = document.getElementById('meshMap');
@@ -111,7 +111,9 @@ const Meshtastic = (function() {
window.meshMap = meshMap; window.meshMap = meshMap;
// Use settings manager for tile layer (allows runtime changes) // Use settings manager for tile layer (allows runtime changes)
if (typeof Settings !== 'undefined' && Settings.createTileLayer) { if (typeof Settings !== 'undefined') {
// Wait for settings to load from server before applying tiles
await Settings.init();
Settings.createTileLayer().addTo(meshMap); Settings.createTileLayer().addTo(meshMap);
Settings.registerMap(meshMap); Settings.registerMap(meshMap);
} else { } else {
+1 -1
View File
@@ -84,7 +84,7 @@ const SpyStations = (function() {
modeContainer.innerHTML = modes.map(m => ` modeContainer.innerHTML = modes.map(m => `
<label class="inline-checkbox"> <label class="inline-checkbox">
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()"> <input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
<span style="font-family: 'JetBrains Mono', monospace; font-size: 10px;">${m}</span> <span style="font-family: 'Space Mono', monospace; font-size: 10px;">${m}</span>
</label> </label>
`).join(''); `).join('');
} }
+39 -35
View File
@@ -37,20 +37,20 @@ const SSTV = (function() {
/** /**
* Load location into input fields * Load location into input fields
*/ */
function loadLocationInputs() { function loadLocationInputs() {
const latInput = document.getElementById('sstvObsLat'); const latInput = document.getElementById('sstvObsLat');
const lonInput = document.getElementById('sstvObsLon'); const lonInput = document.getElementById('sstvObsLon');
let storedLat = localStorage.getItem('observerLat'); let storedLat = localStorage.getItem('observerLat');
let storedLon = localStorage.getItem('observerLon'); let storedLon = localStorage.getItem('observerLon');
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
const shared = ObserverLocation.getShared(); const shared = ObserverLocation.getShared();
storedLat = shared.lat.toString(); storedLat = shared.lat.toString();
storedLon = shared.lon.toString(); storedLon = shared.lon.toString();
} }
if (latInput && storedLat) latInput.value = storedLat; if (latInput && storedLat) latInput.value = storedLat;
if (lonInput && storedLon) lonInput.value = storedLon; if (lonInput && storedLon) lonInput.value = storedLon;
// Add change handlers to save and refresh // Add change handlers to save and refresh
if (latInput) latInput.addEventListener('change', saveLocationFromInputs); if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
@@ -60,23 +60,23 @@ const SSTV = (function() {
/** /**
* Save location from input fields * Save location from input fields
*/ */
function saveLocationFromInputs() { function saveLocationFromInputs() {
const latInput = document.getElementById('sstvObsLat'); const latInput = document.getElementById('sstvObsLat');
const lonInput = document.getElementById('sstvObsLon'); const lonInput = document.getElementById('sstvObsLon');
const lat = parseFloat(latInput?.value); const lat = parseFloat(latInput?.value);
const lon = parseFloat(lonInput?.value); const lon = parseFloat(lonInput?.value);
if (!isNaN(lat) && lat >= -90 && lat <= 90 && if (!isNaN(lat) && lat >= -90 && lat <= 90 &&
!isNaN(lon) && lon >= -180 && lon <= 180) { !isNaN(lon) && lon >= -180 && lon <= 180) {
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
ObserverLocation.setShared({ lat, lon }); ObserverLocation.setShared({ lat, lon });
} else { } else {
localStorage.setItem('observerLat', lat.toString()); localStorage.setItem('observerLat', lat.toString());
localStorage.setItem('observerLon', lon.toString()); localStorage.setItem('observerLon', lon.toString());
} }
loadIssSchedule(); // Refresh pass predictions loadIssSchedule(); // Refresh pass predictions
} }
} }
/** /**
@@ -103,12 +103,12 @@ const SSTV = (function() {
if (latInput) latInput.value = lat; if (latInput) latInput.value = lat;
if (lonInput) lonInput.value = lon; if (lonInput) lonInput.value = lon;
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) { if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) }); ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) });
} else { } else {
localStorage.setItem('observerLat', lat); localStorage.setItem('observerLat', lat);
localStorage.setItem('observerLon', lon); localStorage.setItem('observerLon', lon);
} }
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false; btn.disabled = false;
@@ -159,7 +159,7 @@ const SSTV = (function() {
/** /**
* Initialize Leaflet map for ISS tracking * Initialize Leaflet map for ISS tracking
*/ */
function initMap() { async function initMap() {
const mapContainer = document.getElementById('sstvIssMap'); const mapContainer = document.getElementById('sstvIssMap');
if (!mapContainer || issMap) return; if (!mapContainer || issMap) return;
@@ -173,10 +173,14 @@ const SSTV = (function() {
attributionControl: false, attributionControl: false,
worldCopyJump: true worldCopyJump: true
}); });
window.issMap = issMap;
// Add tile layer using settings manager if available // Add tile layer using settings manager if available
if (typeof Settings !== 'undefined' && Settings.createTileLayer) { if (typeof Settings !== 'undefined') {
// Wait for settings to load from server before applying tiles
await Settings.init();
Settings.createTileLayer().addTo(issMap); Settings.createTileLayer().addTo(issMap);
Settings.registerMap(issMap);
} else { } else {
// Fallback to dark theme tiles // Fallback to dark theme tiles
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
+130
View File
@@ -887,6 +887,9 @@ const WiFiMode = (function() {
clients.set(client.mac, client); clients.set(client.mac, client);
updateStats(); updateStats();
// Update client display if this client belongs to the selected network
updateClientInList(client);
if (onClientUpdate) onClientUpdate(client); if (onClientUpdate) onClientUpdate(client);
} }
@@ -1135,6 +1138,9 @@ const WiFiMode = (function() {
// Show the drawer // Show the drawer
elements.detailDrawer.classList.add('open'); elements.detailDrawer.classList.add('open');
// Fetch and display clients for this network
fetchClientsForNetwork(network.bssid);
} }
function closeDetail() { function closeDetail() {
@@ -1147,6 +1153,130 @@ const WiFiMode = (function() {
}); });
} }
// ==========================================================================
// Client Display
// ==========================================================================
async function fetchClientsForNetwork(bssid) {
if (!elements.detailClientList) return;
try {
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
let response;
if (isAgentMode) {
// Route through agent proxy
response = await fetch(`/controller/agents/${currentAgent}/wifi/v2/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
} else {
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
}
if (!response.ok) {
// Hide client list on error
elements.detailClientList.style.display = 'none';
return;
}
const data = await response.json();
// Handle agent response format (may be nested in 'result')
const result = isAgentMode && data.result ? data.result : data;
const clientList = result.clients || [];
if (clientList.length > 0) {
renderClientList(clientList, bssid);
elements.detailClientList.style.display = 'block';
} else {
elements.detailClientList.style.display = 'none';
}
} catch (error) {
console.debug('[WiFiMode] Error fetching clients:', error);
elements.detailClientList.style.display = 'none';
}
}
function renderClientList(clientList, bssid) {
const container = elements.detailClientList?.querySelector('.wifi-client-list');
const countBadge = document.getElementById('wifiClientCountBadge');
if (!container) return;
// Update count badge
if (countBadge) {
countBadge.textContent = clientList.length;
}
// Render client cards
container.innerHTML = clientList.map(client => {
const rssi = client.rssi_current;
const signalClass = rssi >= -50 ? 'signal-strong' :
rssi >= -70 ? 'signal-medium' :
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
// Format last seen time
const lastSeen = client.last_seen ? formatTime(client.last_seen) : '--';
// Build probed SSIDs badges
let probesHtml = '';
if (client.probed_ssids && client.probed_ssids.length > 0) {
const probes = client.probed_ssids.slice(0, 5); // Show max 5
probesHtml = `
<div class="wifi-client-probes">
${probes.map(ssid => `<span class="wifi-client-probe-badge">${escapeHtml(ssid)}</span>`).join('')}
${client.probed_ssids.length > 5 ? `<span class="wifi-client-probe-badge">+${client.probed_ssids.length - 5}</span>` : ''}
</div>
`;
}
return `
<div class="wifi-client-card" data-mac="${escapeHtml(client.mac)}">
<div class="wifi-client-identity">
<span class="wifi-client-mac">${escapeHtml(client.mac)}</span>
<span class="wifi-client-vendor">${escapeHtml(client.vendor || 'Unknown vendor')}</span>
${probesHtml}
</div>
<div class="wifi-client-signal">
<span class="wifi-client-rssi ${signalClass}">${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'}</span>
<span class="wifi-client-lastseen">${lastSeen}</span>
</div>
</div>
`;
}).join('');
}
function updateClientInList(client) {
// Check if this client belongs to the currently selected network
if (!selectedNetwork || client.associated_bssid !== selectedNetwork) {
return;
}
const container = elements.detailClientList?.querySelector('.wifi-client-list');
if (!container) return;
const existingCard = container.querySelector(`[data-mac="${client.mac}"]`);
if (existingCard) {
// Update existing card's RSSI and last seen
const rssiEl = existingCard.querySelector('.wifi-client-rssi');
const lastSeenEl = existingCard.querySelector('.wifi-client-lastseen');
if (rssiEl && client.rssi_current !== null && client.rssi_current !== undefined) {
const rssi = client.rssi_current;
const signalClass = rssi >= -50 ? 'signal-strong' :
rssi >= -70 ? 'signal-medium' :
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
rssiEl.textContent = rssi + ' dBm';
rssiEl.className = 'wifi-client-rssi ' + signalClass;
}
if (lastSeenEl && client.last_seen) {
lastSeenEl.textContent = formatTime(client.last_seen);
}
} else {
// New client for this network - re-fetch the full list
fetchClientsForNetwork(selectedNetwork);
}
}
// ========================================================================== // ==========================================================================
// Statistics // Statistics
// ========================================================================== // ==========================================================================
+4916 -4899
View File
File diff suppressed because it is too large Load Diff
+23 -4
View File
@@ -4,8 +4,15 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADS-B History // INTERCEPT</title> <title>ADS-B History // INTERCEPT</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> {% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <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/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
</head> </head>
<body> <body>
@@ -22,6 +29,9 @@
</div> </div>
</header> </header>
{% set active_mode = 'adsb' %}
{% include 'partials/nav.html' with context %}
<main class="history-shell"> <main class="history-shell">
<section class="summary-strip"> <section class="summary-strip">
<div class="summary-card"> <div class="summary-card">
@@ -462,7 +472,7 @@
if (!points.length) { if (!points.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)'; ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "JetBrains Mono", monospace'; ctx.font = '12px "Space Mono", monospace';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2); ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return; return;
} }
@@ -470,7 +480,7 @@
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined); const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
if (!series.length) { if (!series.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)'; ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "JetBrains Mono", monospace'; ctx.font = '12px "Space Mono", monospace';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2); ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return; return;
} }
@@ -511,7 +521,7 @@
} }
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)'; ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
ctx.font = '11px "JetBrains Mono", monospace'; ctx.font = '11px "Space Mono", monospace';
ctx.fillText(`${maxVal} ${unit}`, 12, padding); ctx.fillText(`${maxVal} ${unit}`, 12, padding);
ctx.fillText(`${minVal} ${unit}`, 12, height - padding); ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
} }
@@ -761,5 +771,14 @@
} }
}); });
</script> </script>
<!-- Settings Modal -->
{% include 'partials/settings-modal.html' %}
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body> </body>
</html> </html>
+568 -588
View File
File diff suppressed because it is too large Load Diff
+23 -6
View File
@@ -8,7 +8,7 @@
{% if offline_settings.fonts_source == 'local' %} {% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %} {% else %}
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %} {% endif %}
<!-- Leaflet.js - Conditional CDN/Local loading --> <!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %} {% if offline_settings.assets_source == 'local' %}
@@ -18,8 +18,13 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
{% endif %} {% endif %}
<!-- Core CSS variables -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <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/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<script> <script>
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }}; window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
</script> </script>
@@ -46,11 +51,12 @@
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All <input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
</label> </label>
</div> </div>
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
<a href="/" class="back-link">Main Dashboard</a>
</div> </div>
</header> </header>
{% set active_mode = 'ais' %}
{% include 'partials/nav.html' with context %}
<div class="stats-strip"> <div class="stats-strip">
<div class="stats-strip-inner"> <div class="stats-strip-inner">
<div class="strip-stat"> <div class="strip-stat">
@@ -384,7 +390,7 @@
}; };
// Initialize map // Initialize map
function initMap() { async function initMap() {
if (observerLocation) { if (observerLocation) {
document.getElementById('obsLat').value = observerLocation.lat; document.getElementById('obsLat').value = observerLocation.lat;
document.getElementById('obsLon').value = observerLocation.lon; document.getElementById('obsLon').value = observerLocation.lon;
@@ -398,7 +404,9 @@
// Use settings manager for tile layer (allows runtime changes) // Use settings manager for tile layer (allows runtime changes)
window.vesselMap = vesselMap; window.vesselMap = vesselMap;
if (typeof Settings !== 'undefined' && Settings.createTileLayer) { if (typeof Settings !== 'undefined') {
// Wait for settings to load from server before applying tiles
await Settings.init();
Settings.createTileLayer().addTo(vesselMap); Settings.createTileLayer().addTo(vesselMap);
Settings.registerMap(vesselMap); Settings.registerMap(vesselMap);
} else { } else {
@@ -1495,7 +1503,7 @@
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
font-size: 11px; font-size: 11px;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
cursor: pointer; cursor: pointer;
} }
.agent-select-sm:focus { .agent-select-sm:focus {
@@ -1547,8 +1555,17 @@
} }
</style> </style>
<!-- Settings Modal -->
{% include 'partials/settings-modal.html' %}
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<!-- Agent Manager --> <!-- Agent Manager -->
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script> <script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
<script> <script>
// AIS-specific agent integration // AIS-specific agent integration
let aisCurrentAgent = 'local'; let aisCurrentAgent = 'local';
+24
View File
@@ -0,0 +1,24 @@
{#
Card/Panel Component
Reusable container with optional header and footer
Variables:
- title: Optional card header title
- indicator: If true, shows status indicator dot in header
- indicator_active: If true, indicator is active/green
- no_padding: If true, removes body padding
#}
<div class="panel">
{% if title %}
<div class="panel-header">
<span>{{ title }}</span>
{% if indicator %}
<div class="panel-indicator {% if indicator_active %}active{% endif %}"></div>
{% endif %}
</div>
{% endif %}
<div class="panel-content{% if no_padding %}" style="padding: 0;{% else %}{% endif %}">
{{ caller() }}
</div>
</div>
+38
View File
@@ -0,0 +1,38 @@
{#
Empty State Component
Display when no data is available
Variables:
- icon: Optional SVG icon (default: generic empty icon)
- title: Main message (default: "No data")
- description: Optional helper text
- action_text: Optional button text
- action_onclick: Optional button onclick handler
- action_href: Optional button link
#}
<div class="empty-state">
<div class="empty-state-icon">
{% if icon %}
{{ icon|safe }}
{% else %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12h8"/>
</svg>
{% endif %}
</div>
<div class="empty-state-title">{{ title|default('No data') }}</div>
{% if description %}
<div class="empty-state-description">{{ description }}</div>
{% endif %}
{% if action_text %}
<div class="empty-state-action">
{% if action_href %}
<a href="{{ action_href }}" class="btn btn-primary btn-sm">{{ action_text }}</a>
{% elif action_onclick %}
<button class="btn btn-primary btn-sm" onclick="{{ action_onclick }}">{{ action_text }}</button>
{% endif %}
</div>
{% endif %}
</div>
+27
View File
@@ -0,0 +1,27 @@
{#
Loading State Component
Display while data is being fetched
Variables:
- text: Optional loading text (default: "Loading...")
- size: 'sm', 'md', or 'lg' (default: 'md')
- overlay: If true, renders as full overlay
#}
{% if overlay %}
<div class="loading-overlay">
<div class="loading-content">
<div class="spinner {% if size == 'sm' %}spinner-sm{% elif size == 'lg' %}spinner-lg{% endif %}"></div>
{% if text %}
<div class="loading-text mt-3 text-secondary text-sm">{{ text }}</div>
{% endif %}
</div>
</div>
{% else %}
<div class="loading-inline flex items-center gap-3">
<div class="spinner {% if size == 'sm' %}spinner-sm{% elif size == 'lg' %}spinner-lg{% endif %}"></div>
{% if text %}
<span class="text-secondary text-sm">{{ text }}</span>
{% endif %}
</div>
{% endif %}
+47
View File
@@ -0,0 +1,47 @@
{#
Stats Strip Component
Horizontal bar displaying key metrics
Variables:
- stats: List of stat objects with 'id', 'value', 'label', and optional 'title'
- show_divider: Show divider after stats (default: true)
- status_dot_id: Optional ID for status indicator dot
- status_text_id: Optional ID for status text
- time_id: Optional ID for time display
#}
<div class="stats-strip">
<div class="stats-strip-inner">
{% for stat in stats %}
<div class="strip-stat" {% if stat.title %}title="{{ stat.title }}"{% endif %}>
<span class="strip-value" id="{{ stat.id }}">{{ stat.value|default('0') }}</span>
<span class="strip-label">{{ stat.label }}</span>
</div>
{% endfor %}
{% if show_divider|default(true) %}
<div class="strip-divider"></div>
{% endif %}
{# Additional content from caller #}
{% if caller is defined %}
{{ caller() }}
{% endif %}
{% if status_dot_id or status_text_id %}
<div class="strip-divider"></div>
<div class="strip-status">
{% if status_dot_id %}
<div class="status-dot inactive" id="{{ status_dot_id }}"></div>
{% endif %}
{% if status_text_id %}
<span id="{{ status_text_id }}">STANDBY</span>
{% endif %}
</div>
{% endif %}
{% if time_id %}
<div class="strip-time" id="{{ time_id }}">--:--:-- UTC</div>
{% endif %}
</div>
</div>
+27
View File
@@ -0,0 +1,27 @@
{#
Status Badge Component
Compact status indicator with dot and text
Variables:
- status: 'online', 'offline', 'warning', 'error' (default: 'offline')
- text: Status text to display
- id: Optional ID for the text element (for JS updates)
- dot_id: Optional ID for the dot element (for JS updates)
- pulse: If true, adds pulse animation to dot
#}
{% set status_class = {
'online': 'online',
'active': 'online',
'offline': 'offline',
'warning': 'warning',
'error': 'error',
'inactive': 'inactive'
}.get(status|default('offline'), 'inactive') %}
<div class="status-badge flex items-center gap-2">
<div class="status-dot {{ status_class }}{% if pulse %} pulse{% endif %}"
{% if dot_id %}id="{{ dot_id }}"{% endif %}></div>
<span class="text-sm"
{% if id %}id="{{ id }}"{% endif %}>{{ text|default('Unknown') }}</span>
</div>
+143 -151
View File
@@ -22,7 +22,7 @@
{% if offline_settings.fonts_source == 'local' %} {% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %} {% else %}
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %} {% endif %}
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading --> <!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %} {% if offline_settings.assets_source == 'local' %}
@@ -39,6 +39,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
{% endif %} {% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <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') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
@@ -290,7 +291,7 @@
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre> ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);"> <div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
<p <p
style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #888; text-align: left; margin: 0;"> style="font-family: var(--font-mono); font-size: 11px; color: #888; text-align: left; margin: 0;">
<span style="color: var(--accent-red);">root@intercepted:</span><span <span style="color: var(--accent-red);">root@intercepted:</span><span
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br> style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
<span style="color: #666;">[sudo] password for user: ********</span><br> <span style="color: #666;">[sudo] password for user: ********</span><br>
@@ -348,107 +349,9 @@
</header> </header>
<!-- Mode Navigation Bar --> <!-- Mode Navigation Bar -->
<nav class="mode-nav"> {% set is_index_page = true %}
<div class="mode-nav-dropdown" data-group="sdr"> {% set active_mode = 'pager' %}
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('sdr')"> {% include 'partials/nav.html' with context %}
<span class="nav-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="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
<span class="nav-label">SDR / RF</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
<button class="mode-nav-btn active" onclick="switchMode('pager')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="nav-label">Pager</span></button>
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-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="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span><span class="nav-label">433MHz</span></button>
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span><span class="nav-label">Meters</span></button>
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span><span class="nav-label">Aircraft</span></a>
<a href="/ais/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span><span class="nav-label">Vessels</span></a>
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span><span class="nav-label">APRS</span></button>
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-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"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="nav-label">Listening Post</span></button>
<button class="mode-nav-btn" onclick="switchMode('spystations')"><span class="nav-icon icon"><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></span><span class="nav-label">Spy Stations</span></button>
<button class="mode-nav-btn" onclick="switchMode('meshtastic')"><span class="nav-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="nav-label">Meshtastic</span></button>
</div>
</div>
<div class="mode-nav-dropdown" data-group="wireless">
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('wireless')">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
<span class="nav-label">Wireless</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
<button class="mode-nav-btn" onclick="switchMode('wifi')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span><span class="nav-label">WiFi</span></button>
<button class="mode-nav-btn" onclick="switchMode('bluetooth')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span><span class="nav-label">Bluetooth</span></button>
</div>
</div>
<div class="mode-nav-dropdown" data-group="security">
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('security')">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<span class="nav-label">Security</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
<button class="mode-nav-btn" onclick="switchMode('tscm')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="nav-label">TSCM</span></button>
</div>
</div>
<div class="mode-nav-dropdown" data-group="space">
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('space')">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
<span class="nav-label">Space</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon icon"><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></span><span class="nav-label">Satellite</span></button>
<button class="mode-nav-btn" onclick="switchMode('sstv')"><span class="nav-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="nav-label">ISS SSTV</span></button>
</div>
</div>
<div class="mode-nav-actions">
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn"
style="display: none;">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span><span class="nav-label">Full Dashboard</span>
</a>
</div>
<div class="nav-utilities">
<div class="nav-clock">
<span class="utc-label">UTC</span>
<span class="utc-time" id="headerUtcTime">--:--:--</span>
</div>
<div class="nav-divider"></div>
<div class="nav-tools">
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
</button>
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
<span class="icon-sun 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="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
</button>
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;"><span class="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></a>
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span></a>
<button class="nav-tool-btn" onclick="showSettings()" title="Settings"><span class="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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span></button>
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="nav-tool-btn nav-tool-btn--donate" title="Support the Project"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg></span></a>
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
</button>
</div>
</div>
</nav>
<!-- Mobile Navigation Bar (simplified mode switching) -->
<nav class="mobile-nav-bar" id="mobileNavBar">
<button class="mobile-nav-btn active" data-mode="pager" onclick="switchMode('pager')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span> Pager</button>
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><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> 433MHz</button>
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Meters</button>
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span> Aircraft</a>
<a href="/ais/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span> Vessels</a>
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span> APRS</button>
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> WiFi</button>
<button class="mobile-nav-btn" data-mode="bluetooth" onclick="switchMode('bluetooth')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span> BT</button>
<button class="mobile-nav-btn" data-mode="tscm" onclick="switchMode('tscm')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span> TSCM</button>
<button class="mobile-nav-btn" data-mode="satellite" onclick="switchMode('satellite')"><span class="icon icon--sm"><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></span> Sat</button>
<button class="mobile-nav-btn" data-mode="sstv" onclick="switchMode('sstv')"><span class="icon icon--sm"><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></span> SSTV</button>
<button class="mobile-nav-btn" data-mode="listening" onclick="switchMode('listening')"><span class="icon icon--sm"><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></span> Scanner</button>
<button class="mobile-nav-btn" data-mode="spystations" onclick="switchMode('spystations')"><span class="icon icon--sm"><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></span> Spy</button>
<button class="mobile-nav-btn" data-mode="meshtastic" onclick="switchMode('meshtastic')"><span class="icon icon--sm"><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></span> Mesh</button>
</nav>
<!-- Mobile Drawer Overlay --> <!-- Mobile Drawer Overlay -->
<div class="drawer-overlay" id="drawerOverlay"></div> <div class="drawer-overlay" id="drawerOverlay"></div>
@@ -508,8 +411,7 @@
{% if devices %} {% if devices %}
{% for device in devices %} {% for device in devices %}
<option value="{{ device.index }}" <option value="{{ device.index }}"
data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}{% if device.serial and device.serial != 'N/A' and device.serial != 'Unknown' %} (SN: {{ device.serial }}){% endif %}</option>
device.name }}</option>
{% endfor %} {% endfor %}
{% else %} {% else %}
<option value="0">No devices found</option> <option value="0">No devices found</option>
@@ -806,7 +708,7 @@
</div> </div>
</div> </div>
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;"> <div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
<h6>Connected Clients</h6> <h6>Connected Clients <span class="wifi-client-count-badge" id="wifiClientCountBadge"></span></h6>
<div class="wifi-client-list"></div> <div class="wifi-client-list"></div>
</div> </div>
</div> </div>
@@ -1088,7 +990,7 @@
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;"> style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
PACKET LOG</h5> PACKET LOG</h5>
<div id="aprsPacketLog" <div id="aprsPacketLog"
style="flex: 1; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;"> style="flex: 1; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
<div style="color: var(--text-muted);">Waiting for packets...</div> <div style="color: var(--text-muted);">Waiting for packets...</div>
</div> </div>
</div> </div>
@@ -1108,7 +1010,7 @@
STOPPED</div> STOPPED</div>
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;"> <div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
<div class="freq-digits" id="mainScannerFreq" <div class="freq-digits" id="mainScannerFreq"
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: 'JetBrains Mono', monospace; letter-spacing: 3px;"> style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: var(--font-mono); letter-spacing: 3px;">
118.000</div> 118.000</div>
<span class="freq-unit" <span class="freq-unit"
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span> style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
@@ -1154,6 +1056,10 @@
Audio Output</div> Audio Output</div>
</div> </div>
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio> <audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
<button id="audioUnlockBtn" type="button"
style="display: none; margin-top: 8px; width: 100%; padding: 6px 10px; font-size: 10px; letter-spacing: 1px; background: var(--accent-cyan); color: #000; border: 1px solid var(--accent-cyan); border-radius: 4px; cursor: pointer;">
CLICK TO ENABLE AUDIO
</button>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;"> <div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span> <span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
<div <div
@@ -1166,6 +1072,13 @@
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">-- style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
dB</span> dB</span>
</div> </div>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<span style="font-size: 7px; color: var(--text-muted); letter-spacing: 1px;">SNR THRESH</span>
<input type="range" id="snrThresholdSlider" min="6" max="20" step="1" value="8"
style="flex: 1;" />
<span id="snrThresholdValue"
style="font-size: 8px; color: var(--text-muted); min-width: 26px; text-align: right;">8</span>
</div>
<!-- Signal Alert inline --> <!-- Signal Alert inline -->
<div id="mainSignalAlert" <div id="mainSignalAlert"
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;"> style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
@@ -1260,10 +1173,10 @@
<!-- SQL, Gain, Vol Knobs --> <!-- SQL, Gain, Vol Knobs -->
<div style="display: flex; gap: 15px;"> <div style="display: flex; gap: 15px;">
<div class="knob-container"> <div class="knob-container">
<div class="radio-knob" id="radioSquelchKnob" data-value="30" data-min="0" <div class="radio-knob" id="radioSquelchKnob" data-value="0" data-min="0"
data-max="100" data-step="1"></div> data-max="100" data-step="1"></div>
<div class="knob-label">SQL</div> <div class="knob-label">SQL</div>
<div class="knob-value" id="radioSquelchValue">30</div> <div class="knob-value" id="radioSquelchValue">0</div>
</div> </div>
<div class="knob-container"> <div class="knob-container">
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0" <div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
@@ -1336,7 +1249,7 @@
START</div> START</div>
<input type="number" id="radioScanStart" value="118" step="0.1" <input type="number" id="radioScanStart" value="118" step="0.1"
class="radio-input" class="radio-input"
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);"> style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
</div> </div>
<span <span
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span> style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
@@ -1345,15 +1258,17 @@
END</div> END</div>
<input type="number" id="radioScanEnd" value="137" step="0.1" <input type="number" id="radioScanEnd" value="137" step="0.1"
class="radio-input" class="radio-input"
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);"> style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
</div> </div>
</div> </div>
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()" <button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()">
style="padding: 12px; font-size: 13px; width: 100%; font-weight: bold;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN</button> <span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()" </button>
style="padding: 10px; font-size: 12px; width: 100%; background: var(--accent-green); border: none; color: #fff;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN</button> <button class="radio-action-btn listen" id="radioListenBtn" onclick="toggleDirectListen()">
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1411,19 +1326,19 @@
<div style="display: flex; align-items: center; justify-content: space-between;"> <div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span> <span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
<span <span
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;" style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainSignalCount">0</span> id="mainSignalCount">0</span>
</div> </div>
<div style="display: flex; align-items: center; justify-content: space-between;"> <div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span> <span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
<span <span
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;" style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainFreqsScanned">0</span> id="mainFreqsScanned">0</span>
</div> </div>
<div style="display: flex; align-items: center; justify-content: space-between;"> <div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span> <span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
<span <span
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;" style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainScanCycles">0</span> id="mainScanCycles">0</span>
</div> </div>
</div> </div>
@@ -2118,7 +2033,7 @@
// After fade out, hide welcome and switch to mode // After fade out, hide welcome and switch to mode
setTimeout(() => { setTimeout(() => {
welcome.style.display = 'none'; welcome.style.display = 'none';
switchMode(mode); switchMode(mode, { updateUrl: true });
}, 400); }, 400);
} }
@@ -2127,6 +2042,53 @@
document.getElementById('disclaimerModal').style.display = 'flex'; document.getElementById('disclaimerModal').style.display = 'flex';
} }
// Mode from query string (e.g., /?mode=wifi)
let pendingStartMode = null;
const validModes = new Set([
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
'spystations', 'meshtastic', 'wifi', 'bluetooth',
'tscm', 'satellite', 'sstv'
]);
function getModeFromQuery() {
const params = new URLSearchParams(window.location.search);
const mode = params.get('mode');
if (!mode || !validModes.has(mode)) return null;
return mode;
}
function applyModeFromQuery() {
const mode = getModeFromQuery();
if (!mode) return;
const accepted = localStorage.getItem('disclaimerAccepted') === 'true';
if (accepted) {
const welcome = document.getElementById('welcomePage');
if (welcome) welcome.style.display = 'none';
switchMode(mode, { updateUrl: false });
updateModeUrl(mode, true);
} else {
pendingStartMode = mode;
}
}
function applySettingsFromQuery() {
const params = new URLSearchParams(window.location.search);
if (params.get('settings') === '1') {
// Remove settings param from URL to avoid reopening on refresh
params.delete('settings');
const newUrl = params.toString()
? window.location.pathname + '?' + params.toString()
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
// Open settings modal after a brief delay to ensure page is ready
setTimeout(() => {
if (typeof showSettings === 'function') {
showSettings();
}
}, 100);
}
}
function acceptDisclaimer() { function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true'); localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden'); document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
@@ -2138,7 +2100,14 @@
const gateStyle = document.getElementById('disclaimer-gate'); const gateStyle = document.getElementById('disclaimer-gate');
if (gateStyle) gateStyle.remove(); if (gateStyle) gateStyle.remove();
// Ensure welcome page is visible // Ensure welcome page is visible
document.getElementById('welcomePage').style.display = ''; const welcome = document.getElementById('welcomePage');
if (welcome) welcome.style.display = '';
if (pendingStartMode) {
// Bypass welcome and jump to requested mode
welcome.style.display = 'none';
switchMode(pendingStartMode, { updateUrl: true });
pendingStartMode = null;
}
}, 300); }, 300);
} }
@@ -2457,6 +2426,12 @@
// Start SDR device status polling // Start SDR device status polling
startSdrStatusPolling(); startSdrStatusPolling();
// Apply mode from URL query (e.g., /?mode=wifi)
applyModeFromQuery();
// Check for settings=1 query param (from dashboard settings button)
applySettingsFromQuery();
}); });
// Toggle section collapse // Toggle section collapse
@@ -2512,8 +2487,20 @@
} }
}); });
function updateModeUrl(mode, replace = false) {
if (!validModes.has(mode)) return;
const url = new URL(window.location.href);
url.searchParams.set('mode', mode);
if (replace) {
window.history.replaceState({ mode }, '', url);
} else {
window.history.pushState({ mode }, '', url);
}
}
// Mode switching // Mode switching
function switchMode(mode) { function switchMode(mode, options = {}) {
const { updateUrl = true } = options;
// Only stop local scans if in local mode (not agent mode) // Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
if (!isAgentMode) { if (!isAgentMode) {
@@ -2526,6 +2513,9 @@
} }
currentMode = mode; currentMode = mode;
if (updateUrl) {
updateModeUrl(mode);
}
// Sync mode state with current agent/local after switching // Sync mode state with current agent/local after switching
if (isAgentMode && typeof syncAgentModeStates === 'function') { if (isAgentMode && typeof syncAgentModeStates === 'function') {
@@ -2758,6 +2748,13 @@
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap(); if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
}); });
window.addEventListener('popstate', function () {
const mode = getModeFromQuery();
if (mode && mode !== currentMode) {
switchMode(mode, { updateUrl: false });
}
});
// Also handle orientation changes explicitly for mobile // Also handle orientation changes explicitly for mobile
window.addEventListener('orientationchange', function () { window.addEventListener('orientationchange', function () {
setTimeout(() => { setTimeout(() => {
@@ -3696,9 +3693,10 @@
if (filteredDevices.length === 0) { if (filteredDevices.length === 0) {
select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`; select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`;
} else { } else {
select.innerHTML = filteredDevices.map(d => select.innerHTML = filteredDevices.map(d => {
`<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}</option>` const serialSuffix = d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (SN: ${d.serial})` : '';
).join(''); return `<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}${serialSuffix}</option>`;
}).join('');
} }
// Update capabilities display // Update capabilities display
@@ -3764,7 +3762,7 @@
return `<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border-color);"> return `<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
${statusDot} ${statusDot}
<span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}</span> <span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}${d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (${d.serial})` : ''}</span>
</div> </div>
<div style="display: flex; align-items: center; gap: 6px;"> <div style="display: flex; align-items: center; gap: 6px;">
<span style="font-size: 10px; color: ${modeColor}; font-weight: bold;">${modeName}</span> <span style="font-size: 10px; color: ${modeColor}; font-weight: bold;">${modeName}</span>
@@ -4283,7 +4281,7 @@
const infoEl = document.createElement('div'); const infoEl = document.createElement('div');
infoEl.className = 'info-msg'; infoEl.className = 'info-msg';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;'; infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.textContent = text; infoEl.textContent = text;
output.insertBefore(infoEl, output.firstChild); output.insertBefore(infoEl, output.firstChild);
} }
@@ -4299,7 +4297,7 @@
const errorEl = document.createElement('div'); const errorEl = document.createElement('div');
errorEl.className = 'error-msg'; errorEl.className = 'error-msg';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;'; errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.textContent = '⚠ ' + text; errorEl.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild); output.insertBefore(errorEl, output.firstChild);
} }
@@ -6915,7 +6913,7 @@
// Draw total in center // Draw total in center
ctx.fillStyle = '#fff'; ctx.fillStyle = '#fff';
ctx.font = 'bold 16px JetBrains Mono'; ctx.font = 'bold 16px Space Mono';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(total, cx, cy); ctx.fillText(total, cx, cy);
@@ -7947,7 +7945,7 @@
}); });
} }
function initAprsMap() { async function initAprsMap() {
if (aprsMap) return; if (aprsMap) return;
const mapContainer = document.getElementById('aprsMap'); const mapContainer = document.getElementById('aprsMap');
@@ -7962,7 +7960,9 @@
window.aprsMap = aprsMap; window.aprsMap = aprsMap;
// Use settings manager for tile layer (allows runtime changes) // Use settings manager for tile layer (allows runtime changes)
if (typeof Settings !== 'undefined' && Settings.createTileLayer) { if (typeof Settings !== 'undefined') {
// Wait for settings to load from server before applying tiles
await Settings.init();
Settings.createTileLayer().addTo(aprsMap); Settings.createTileLayer().addTo(aprsMap);
Settings.registerMap(aprsMap); Settings.registerMap(aprsMap);
} else { } else {
@@ -8685,7 +8685,7 @@
// Label // Label
if (el > 0) { if (el > 0) {
ctx.fillStyle = '#444'; ctx.fillStyle = '#444';
ctx.font = '10px JetBrains Mono'; ctx.font = '10px Space Mono';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(el + '°', cx, cy - r + 12); ctx.fillText(el + '°', cx, cy - r + 12);
} }
@@ -8758,7 +8758,7 @@
// Label // Label
ctx.fillStyle = '#fff'; ctx.fillStyle = '#fff';
ctx.font = '11px JetBrains Mono'; ctx.font = '11px Space Mono';
ctx.fillText(pass.satellite, maxX + 10, maxY - 5); ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
} }
} }
@@ -8870,7 +8870,7 @@
let observerMarker = null; let observerMarker = null;
let satPositionInterval = null; let satPositionInterval = null;
function initGroundTrackMap() { async function initGroundTrackMap() {
const mapContainer = document.getElementById('groundTrackMap'); const mapContainer = document.getElementById('groundTrackMap');
if (!mapContainer || groundTrackMap) return; if (!mapContainer || groundTrackMap) return;
@@ -8883,7 +8883,9 @@
window.groundTrackMap = groundTrackMap; window.groundTrackMap = groundTrackMap;
// Use settings manager for tile layer (allows runtime changes) // Use settings manager for tile layer (allows runtime changes)
if (typeof Settings !== 'undefined' && Settings.createTileLayer) { if (typeof Settings !== 'undefined') {
// Wait for settings to load from server before applying tiles
await Settings.init();
Settings.createTileLayer().addTo(groundTrackMap); Settings.createTileLayer().addTo(groundTrackMap);
Settings.registerMap(groundTrackMap); Settings.registerMap(groundTrackMap);
} else { } else {
@@ -9343,14 +9345,10 @@
// Theme toggle functions // Theme toggle functions
function toggleTheme() { function toggleTheme() {
const html = document.documentElement; const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme'); const currentTheme = html.getAttribute('data-theme') || 'dark';
const newTheme = currentTheme === 'light' ? 'dark' : 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light';
if (newTheme === 'dark') { html.setAttribute('data-theme', newTheme);
html.removeAttribute('data-theme');
} else {
html.setAttribute('data-theme', newTheme);
}
// Save to localStorage for instant load on next visit // Save to localStorage for instant load on next visit
localStorage.setItem('intercept-theme', newTheme); localStorage.setItem('intercept-theme', newTheme);
@@ -9382,10 +9380,8 @@
// Load saved theme and animations on page load // Load saved theme and animations on page load
(function () { (function () {
// First apply localStorage theme for instant load (no flash) // First apply localStorage theme for instant load (no flash)
const localTheme = localStorage.getItem('intercept-theme'); const localTheme = localStorage.getItem('intercept-theme') || 'dark';
if (localTheme === 'light') { document.documentElement.setAttribute('data-theme', localTheme);
document.documentElement.setAttribute('data-theme', 'light');
}
// Apply animations preference // Apply animations preference
const localAnimations = localStorage.getItem('intercept-animations'); const localAnimations = localStorage.getItem('intercept-animations');
@@ -9401,11 +9397,7 @@
const serverTheme = data.value; const serverTheme = data.value;
if (serverTheme !== localTheme) { if (serverTheme !== localTheme) {
// Server has different theme, apply it // Server has different theme, apply it
if (serverTheme === 'light') { document.documentElement.setAttribute('data-theme', serverTheme);
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.removeAttribute('data-theme');
}
localStorage.setItem('intercept-theme', serverTheme); localStorage.setItem('intercept-theme', serverTheme);
} }
} }
@@ -12205,7 +12197,7 @@
<textarea id="tleInput" placeholder="ISS (ZARYA) <textarea id="tleInput" placeholder="ISS (ZARYA)
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002 1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450" 2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450"
style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px; resize: vertical;"></textarea> style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: var(--font-mono); font-size: 11px; resize: vertical;"></textarea>
<button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add <button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add
Satellite</button> Satellite</button>
</div> </div>
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en" data-theme="{{ theme|default('dark') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}iNTERCEPT{% endblock %} // iNTERCEPT</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
{# Fonts - Conditional CDN/Local loading #}
{% if offline_settings and offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
{% endif %}
{# Core CSS (Design System) #}
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/components.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
{# Responsive styles #}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
{# Page-specific CSS #}
{% block styles %}{% endblock %}
{# Page-specific head content #}
{% block head %}{% endblock %}
</head>
<body>
<div class="app-shell">
{# Global Header #}
{% block header %}
<header class="app-header">
<div class="app-header-left">
<button class="hamburger-btn" id="hamburgerBtn" aria-label="Toggle navigation menu">
<span></span>
<span></span>
<span></span>
</button>
<a href="/" class="app-logo">
<svg class="app-logo-icon" width="40" height="40" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 30 Q5 50, 15 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M22 35 Q14 50, 22 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M29 40 Q23 50, 29 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M85 30 Q95 50, 85 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M78 35 Q86 50, 78 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M71 40 Q77 50, 71 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="22" r="6" fill="var(--accent-green)"/>
<rect x="44" y="35" width="12" height="45" rx="2" fill="var(--accent-cyan)"/>
<rect x="38" y="35" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
</svg>
<span class="app-logo-text">
<span class="app-logo-title">iNTERCEPT</span>
<span class="app-logo-tagline">// See the Invisible</span>
</span>
</a>
{% if version %}
<span class="badge badge-primary">v{{ version }}</span>
{% endif %}
</div>
<div class="app-header-right">
{% block header_right %}
<div class="header-clock">
<span class="header-clock-label">UTC</span>
<span id="headerUtcTime">--:--:--</span>
</div>
{% endblock %}
</div>
</header>
{% endblock %}
{# Global Navigation - opt-in for pages that need it #}
{# Override this block and include 'partials/nav.html' in child templates #}
{% block navigation %}{% endblock %}
{# Main Content Area #}
<main class="app-main">
{% block main %}
<div class="content-wrapper">
{# Optional Sidebar #}
{% block sidebar %}{% endblock %}
{# Page Content #}
<div class="app-content">
{% block content %}{% endblock %}
</div>
</div>
{% endblock %}
</main>
{# Toast/Notification Container #}
<div id="toastContainer" class="toast-container"></div>
</div>
{# Core JavaScript #}
<script>
// UTC Clock
function updateUtcClock() {
const now = new Date();
const utc = now.toISOString().slice(11, 19);
const clockEl = document.getElementById('headerUtcTime');
if (clockEl) clockEl.textContent = utc;
}
setInterval(updateUtcClock, 1000);
updateUtcClock();
// Mobile menu toggle
const hamburgerBtn = document.getElementById('hamburgerBtn');
const drawerOverlay = document.getElementById('drawerOverlay');
if (hamburgerBtn) {
hamburgerBtn.addEventListener('click', function() {
this.classList.toggle('open');
document.querySelector('.app-sidebar')?.classList.toggle('open');
drawerOverlay?.classList.toggle('visible');
});
}
if (drawerOverlay) {
drawerOverlay.addEventListener('click', function() {
hamburgerBtn?.classList.remove('open');
document.querySelector('.app-sidebar')?.classList.remove('open');
this.classList.remove('visible');
});
}
// Theme toggle
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme') || 'dark';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
// Apply saved theme
const savedTheme = localStorage.getItem('intercept-theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
// Nav dropdown handling
function toggleNavDropdown(groupName) {
const group = document.querySelector(`.nav-group[data-group="${groupName}"]`);
if (!group) return;
// Close other dropdowns
document.querySelectorAll('.nav-group.open').forEach(g => {
if (g !== group) g.classList.remove('open');
});
group.classList.toggle('open');
}
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.nav-group')) {
document.querySelectorAll('.nav-group.open').forEach(g => g.classList.remove('open'));
}
});
</script>
{# Page-specific JavaScript #}
{% block scripts %}{% endblock %}
</body>
</html>
+226
View File
@@ -0,0 +1,226 @@
{% extends 'layout/base.html' %}
{#
Dashboard Base Template
Extended layout for full-screen dashboard pages (ADSB, AIS, Satellite, etc.)
Features: Full-height layout, stats strip, sidebar overlay on mobile
Variables:
- active_mode: The current mode for nav highlighting (e.g., 'adsb', 'ais', 'satellite')
#}
{% block styles %}
{{ super() }}
<style>
/* Dashboard-specific overrides */
html, body {
height: 100%;
overflow: hidden;
}
.app-shell {
height: 100vh;
overflow: hidden;
}
.app-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Radar/Grid background effect */
.dashboard-bg {
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
}
.radar-bg {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at center, transparent 0%, var(--bg-primary) 70%),
repeating-linear-gradient(0deg, transparent, transparent 50px, var(--border-color) 50px, var(--border-color) 51px),
repeating-linear-gradient(90deg, transparent, transparent 50px, var(--border-color) 50px, var(--border-color) 51px);
opacity: 0.3;
}
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(var(--border-color) 1px, transparent 1px),
linear-gradient(90deg, var(--border-color) 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.15;
}
.scanline {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.5;
animation: scanline 8s linear infinite;
}
@keyframes scanline {
0% { top: 0; }
100% { top: 100%; }
}
/* Animations toggle */
[data-animations="off"] .scanline,
[data-animations="off"] .radar-bg,
[data-animations="off"] .grid-bg {
display: none;
}
/* Dashboard main content */
.dashboard-content {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.dashboard-map-container {
flex: 1;
position: relative;
}
.dashboard-sidebar {
width: 320px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-3);
}
@media (max-width: 1024px) {
.dashboard-sidebar {
width: 280px;
}
}
@media (max-width: 768px) {
.dashboard-sidebar {
position: fixed;
right: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(100%);
transition: transform var(--transition-base);
}
.dashboard-sidebar.open {
transform: translateX(0);
}
}
</style>
{% endblock %}
{% block header %}
<header class="app-header" style="padding: 0 var(--space-3); height: 48px;">
<div class="app-header-left" style="gap: var(--space-3);">
<a href="/" class="app-logo" style="gap: var(--space-2);">
<svg class="app-logo-icon" width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 30 Q5 50, 15 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M22 35 Q14 50, 22 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M29 40 Q23 50, 29 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M85 30 Q95 50, 85 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M78 35 Q86 50, 78 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M71 40 Q77 50, 71 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="22" r="6" fill="var(--accent-green)"/>
<rect x="44" y="35" width="12" height="45" rx="2" fill="var(--accent-cyan)"/>
<rect x="38" y="35" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
</svg>
</a>
<div class="dashboard-header-title">
<span style="font-size: var(--text-lg); font-weight: var(--font-bold); color: var(--text-primary);">
{% block dashboard_title %}DASHBOARD{% endblock %}
</span>
<span style="font-size: var(--text-sm); color: var(--text-dim); margin-left: var(--space-2);">
// iNTERCEPT
</span>
</div>
</div>
<div class="app-header-right">
{% block dashboard_header_center %}{% endblock %}
<div class="header-utilities" style="gap: var(--space-2);">
{% block agent_selector %}{% endblock %}
</div>
</div>
</header>
{% endblock %}
{% block navigation %}
{# Include the unified nav partial with active_mode set #}
{% include 'partials/nav.html' with context %}
{% endblock %}
{% block main %}
{# Background effects #}
<div class="dashboard-bg">
{% block dashboard_bg %}
<div class="radar-bg"></div>
{% endblock %}
<div class="scanline"></div>
</div>
{# Stats strip #}
{% block stats_strip %}{% endblock %}
{# Dashboard content #}
<div class="dashboard-content">
{% block dashboard_content %}{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// Dashboard-specific scripts
(function() {
// Mobile sidebar toggle
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.querySelector('.dashboard-sidebar');
const overlay = document.getElementById('drawerOverlay');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('visible');
});
}
if (overlay) {
overlay.addEventListener('click', function() {
sidebar?.classList.remove('open');
this.classList.remove('visible');
});
}
// UTC Clock update
function updateUtcClock() {
const now = new Date();
const utc = now.toISOString().slice(11, 19) + ' UTC';
document.querySelectorAll('[id$="utcTime"], [id$="UtcTime"]').forEach(el => {
el.textContent = utc;
});
}
setInterval(updateUtcClock, 1000);
updateUtcClock();
})();
</script>
{% endblock %}
File diff suppressed because it is too large Load Diff
+318
View File
@@ -0,0 +1,318 @@
{#
Help Modal Partial
Provides consistent help modal across all pages
#}
<!-- Help Modal -->
<div id="helpModal" class="help-modal" onclick="if(event.target === this) hideHelp()">
<div class="help-content">
<button class="help-close" onclick="hideHelp()">&times;</button>
<h2>iNTERCEPT Help</h2>
<div class="help-tabs">
<button class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')">Icons</button>
<button class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')">Modes</button>
<button class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')">WiFi</button>
<button class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')">Tips</button>
</div>
<!-- Icons Section -->
<div id="help-icons" class="help-section active">
<h3>Stats Bar Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">&#128223;</span><span class="desc">POCSAG messages decoded</span></div>
<div class="icon-item"><span class="icon">&#128224;</span><span class="desc">FLEX messages decoded</span></div>
<div class="icon-item"><span class="icon">&#128232;</span><span class="desc">Total messages received</span></div>
<div class="icon-item"><span class="icon">&#127777;&#65039;</span><span class="desc">Unique sensors detected</span></div>
<div class="icon-item"><span class="icon">&#128202;</span><span class="desc">Device types found</span></div>
<div class="icon-item"><span class="icon">&#128752;&#65039;</span><span class="desc">Satellites monitored</span></div>
<div class="icon-item"><span class="icon">&#128225;</span><span class="desc">WiFi Access Points</span></div>
<div class="icon-item"><span class="icon">&#128100;</span><span class="desc">Connected WiFi clients</span></div>
<div class="icon-item"><span class="icon">&#129309;</span><span class="desc">Captured handshakes</span></div>
<div class="icon-item"><span class="icon">&#128641;</span><span class="desc">Detected drones (click for details)</span></div>
<div class="icon-item"><span class="icon">&#9888;&#65039;</span><span class="desc">Rogue APs (click for details)</span></div>
<div class="icon-item"><span class="icon">&#128309;</span><span class="desc">Bluetooth devices</span></div>
<div class="icon-item"><span class="icon">&#128205;</span><span class="desc">BLE beacons / APRS stations</span></div>
</div>
<h3>Mode Tab Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">&#128223;</span><span class="desc">Pager - POCSAG/FLEX decoder</span></div>
<div class="icon-item"><span class="icon">&#128225;</span><span class="desc">433MHz - Sensor decoder</span></div>
<div class="icon-item"><span class="icon">&#9889;</span><span class="desc">Meters - Utility meter decoder</span></div>
<div class="icon-item"><span class="icon">&#9992;&#65039;</span><span class="desc">Aircraft - ADS-B tracking &amp; history</span></div>
<div class="icon-item"><span class="icon">&#128674;</span><span class="desc">Vessels - AIS &amp; VHF DSC distress</span></div>
<div class="icon-item"><span class="icon">&#128251;</span><span class="desc">Spy Stations - Number stations database</span></div>
<div class="icon-item"><span class="icon">&#128205;</span><span class="desc">APRS - Amateur radio tracking</span></div>
<div class="icon-item"><span class="icon">&#128752;&#65039;</span><span class="desc">Satellite - Pass prediction</span></div>
<div class="icon-item"><span class="icon">&#128246;</span><span class="desc">WiFi - Network scanner</span></div>
<div class="icon-item"><span class="icon">&#128309;</span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
<div class="icon-item"><span class="icon">&#128251;</span><span class="desc">Listening Post - SDR scanner</span></div>
<div class="icon-item"><span class="icon">&#128269;</span><span class="desc">TSCM - Counter-surveillance</span></div>
</div>
</div>
<!-- Modes Section -->
<div id="help-modes" class="help-section">
<h3>Pager Mode</h3>
<ul class="tip-list">
<li>Decodes POCSAG and FLEX pager signals using RTL-SDR</li>
<li>Set frequency to local pager frequencies (common: 152-158 MHz)</li>
<li>Messages are displayed in real-time as they're decoded</li>
<li>Use presets for common pager frequencies</li>
</ul>
<h3>433MHz Sensor Mode</h3>
<ul class="tip-list">
<li>Decodes wireless sensors on 433.92 MHz ISM band</li>
<li>Detects temperature, humidity, weather stations, tire pressure monitors</li>
<li>Supports many common protocols (Acurite, LaCrosse, Oregon Scientific, etc.)</li>
<li>Device intelligence builds profiles of recurring devices</li>
</ul>
<h3>Utility Meter Mode</h3>
<ul class="tip-list">
<li>Decodes utility meter transmissions (water, gas, electric) using rtlamr</li>
<li>Supports ERT protocol on 912 MHz (North America) or 868 MHz (Europe)</li>
<li>Displays meter IDs and consumption data in real-time</li>
<li>Supports SCM, SCM+, IDM, NetIDM, and R900 message types</li>
</ul>
<h3>Aircraft (Dashboard)</h3>
<ul class="tip-list">
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
<li>Features radar scope, map view, airband audio, and ACARS decoding</li>
<li>Optional history mode persists data to Postgres for long-term analysis</li>
<li>Access history dashboard at <code>/adsb/history</code></li>
</ul>
<h3>Vessels (Dashboard)</h3>
<ul class="tip-list">
<li>Opens the AIS Dashboard for maritime vessel tracking</li>
<li>Displays vessel name, MMSI, callsign, destination, and navigation data</li>
<li><strong>VHF DSC Channel 70:</strong> Monitors maritime distress frequency (156.525 MHz)</li>
<li>Decodes DSC messages: Distress, Urgency, Safety, and Routine calls</li>
<li>MMSI country identification via Maritime Identification Digits (MID)</li>
<li>Visual alerts for DISTRESS and URGENCY messages with map markers</li>
</ul>
<h3>Spy Stations</h3>
<ul class="tip-list">
<li>Database of number stations and diplomatic HF networks</li>
<li>Browse stations from priyom.org with frequencies and schedules</li>
<li>Filter by type (number/diplomatic), country, and mode</li>
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
<li>Click "Tune" to listen via Listening Post mode</li>
</ul>
<h3>APRS Mode</h3>
<ul class="tip-list">
<li>Decodes APRS (Automatic Packet Reporting System) on VHF</li>
<li>Tracks amateur radio operators transmitting position data</li>
<li>Regional frequencies: 144.390 MHz (N. America), 144.800 MHz (Europe)</li>
<li>Uses Direwolf or multimon-ng for packet decoding</li>
<li>Interactive map shows station positions in real-time</li>
</ul>
<h3>Satellite Mode</h3>
<ul class="tip-list">
<li>Track satellites using TLE (Two-Line Element) data</li>
<li>Add satellites manually or fetch from Celestrak by category</li>
<li>Categories: Amateur, Weather, ISS, Starlink, GPS, and more</li>
<li>View next pass predictions with elevation and duration</li>
</ul>
<h3>WiFi Mode</h3>
<ul class="tip-list">
<li>Requires a WiFi adapter capable of monitor mode</li>
<li>Click "Enable Monitor" to put adapter in monitor mode</li>
<li>Scans all channels or lock to a specific channel</li>
<li>Detects drones by SSID patterns and manufacturer OUI</li>
<li>Rogue AP detection flags same SSID on multiple BSSIDs</li>
<li>Click network rows to target for deauth or handshake capture</li>
</ul>
<h3>Bluetooth Mode</h3>
<ul class="tip-list">
<li>Scans for classic Bluetooth and BLE devices</li>
<li>Shows device names, addresses, and signal strength</li>
<li>Manufacturer lookup from MAC address OUI</li>
<li>Radar visualization shows device proximity</li>
</ul>
<h3>Listening Post Mode</h3>
<ul class="tip-list">
<li>Wideband SDR scanner with spectrum visualization</li>
<li>Tune to any frequency supported by your SDR hardware</li>
<li>AM/FM/USB/LSB demodulation modes</li>
<li>Bookmark frequencies for quick recall</li>
<li>Quick tune presets for emergency and marine channels</li>
</ul>
<h3>TSCM Mode</h3>
<ul class="tip-list">
<li>Technical Surveillance Countermeasures sweep</li>
<li>Scans for unknown RF transmitters, WiFi devices, Bluetooth</li>
<li>Baseline comparison to detect new/anomalous devices</li>
<li>Threat classification: Critical, High, Medium, Low</li>
<li>Useful for security audits and bug sweeps</li>
<li><em style="color: var(--text-muted);">Note: This feature is in early development</em></li>
</ul>
<h3>Meshtastic Mode</h3>
<ul class="tip-list">
<li>Integrates with Meshtastic LoRa mesh network devices</li>
<li>Connect Heltec, T-Beam, RAK, or other compatible devices via USB</li>
<li>Real-time message streaming with RSSI and SNR metrics</li>
<li>Configure channels with encryption keys</li>
<li>View connected nodes and message history</li>
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
</ul>
<h3>Network Monitor</h3>
<ul class="tip-list">
<li>Aggregates data from multiple remote INTERCEPT agents</li>
<li>View all WiFi, Bluetooth, ADS-B, AIS data in one unified view</li>
<li>Real-time streaming via Server-Sent Events (SSE)</li>
<li>Location estimation using multi-agent trilateration</li>
<li>Manage agents at <code>/controller/manage</code></li>
</ul>
</div>
<!-- WiFi Section -->
<div id="help-wifi" class="help-section">
<h3>Monitor Mode</h3>
<ul class="tip-list">
<li><strong>Enable Monitor:</strong> Puts WiFi adapter in monitor mode for passive scanning</li>
<li><strong>Kill Processes:</strong> Optional - stops NetworkManager/wpa_supplicant (may drop other connections)</li>
<li>Some adapters rename when entering monitor mode (e.g., wlan0 &rarr; wlan0mon)</li>
</ul>
<h3>Handshake Capture</h3>
<ul class="tip-list">
<li>Click "Capture" on a network to start targeted handshake capture</li>
<li>Status panel shows capture progress and file location</li>
<li>Use deauth to force clients to reconnect (only on authorized networks!)</li>
<li>Handshake files saved to /tmp/intercept_handshake_*.cap</li>
</ul>
<h3>Drone Detection</h3>
<ul class="tip-list">
<li>Drones detected by SSID patterns (DJI, Parrot, Autel, etc.)</li>
<li>Also detected by manufacturer OUI in MAC address</li>
<li>Distance estimated from signal strength (approximate)</li>
<li>Click drone count in stats bar to see all detected drones</li>
</ul>
<h3>Rogue AP Detection</h3>
<ul class="tip-list">
<li>Flags networks where same SSID appears on multiple BSSIDs</li>
<li>Could indicate evil twin attack or legitimate multi-AP setup</li>
<li>Click rogue count to see which SSIDs are flagged</li>
</ul>
<h3>Proximity Alerts</h3>
<ul class="tip-list">
<li>Add MAC addresses to watch list for alerts when detected</li>
<li>Watch list persists in browser localStorage</li>
<li>Useful for tracking specific devices</li>
</ul>
<h3>Client Probe Analysis</h3>
<ul class="tip-list">
<li>Shows what networks client devices are looking for</li>
<li>Orange highlights indicate sensitive/private network names</li>
<li>Reveals user location history (home, work, hotels, airports)</li>
<li>Useful for security awareness and pen test reports</li>
</ul>
</div>
<!-- Tips Section -->
<div id="help-tips" class="help-section">
<h3>General Tips</h3>
<ul class="tip-list">
<li><strong>Collapsible sections:</strong> Click any section header (&nabla;) to collapse/expand</li>
<li><strong>Sound alerts:</strong> Toggle sound on/off in settings for each mode</li>
<li><strong>Export data:</strong> Use export buttons to save captured data as JSON</li>
<li><strong>Device Intelligence:</strong> Tracks device patterns over time</li>
<li><strong>Theme toggle:</strong> Click the theme button in header to switch dark/light mode</li>
<li><strong>Settings:</strong> Click the gear icon in the header to access settings</li>
<li><strong>Offline mode:</strong> Enable in Settings to use local assets without internet</li>
</ul>
<h3>Keyboard Shortcuts</h3>
<ul class="tip-list">
<li><strong>F1</strong> - Open this help page</li>
<li><strong>?</strong> - Open help (when not typing in a field)</li>
<li><strong>Escape</strong> - Close help and modal dialogs</li>
</ul>
<h3>Requirements</h3>
<ul class="tip-list">
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
<li>Run as root/sudo for full hardware access</li>
</ul>
<h3>Legal Notice</h3>
<ul class="tip-list">
<li>Only use on networks and devices you own or have authorization to test</li>
<li>Passive monitoring may be legal; active attacks require authorization</li>
<li>Check local laws regarding radio frequency monitoring</li>
</ul>
</div>
</div>
</div>
<script>
// Help modal functions - defined here so all pages have them
(function() {
// Only define if not already defined (index.html defines its own)
if (typeof window.showHelp === 'undefined') {
window.showHelp = function() {
document.getElementById('helpModal').classList.add('active');
document.body.style.overflow = 'hidden';
};
}
if (typeof window.hideHelp === 'undefined') {
window.hideHelp = function() {
document.getElementById('helpModal').classList.remove('active');
document.body.style.overflow = '';
};
}
if (typeof window.switchHelpTab === 'undefined') {
window.switchHelpTab = function(tab) {
document.querySelectorAll('.help-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.help-section').forEach(s => s.classList.remove('active'));
document.querySelector('.help-tab[data-tab="' + tab + '"]').classList.add('active');
document.getElementById('help-' + tab).classList.add('active');
};
}
// Keyboard shortcuts for help (only add once)
if (!window._helpKeyboardSetup) {
window._helpKeyboardSetup = true;
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') hideHelp();
// Open help with F1 or ? key (when not typing in an input)
var helpModal = document.getElementById('helpModal');
if (helpModal && (e.key === 'F1' || (e.key === '?' && !e.target.matches('input, textarea, select'))) && !helpModal.classList.contains('active')) {
e.preventDefault();
showHelp();
}
});
}
})();
</script>
+1 -1
View File
@@ -19,7 +19,7 @@
</div> </div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Frequency</span> <span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Frequency</span>
<span id="lpQuickFreq" style="font-size: 14px; font-family: 'JetBrains Mono', monospace; color: var(--text-primary);">---.--- MHz</span> <span id="lpQuickFreq" style="font-size: 14px; font-family: var(--font-mono); color: var(--text-primary);">---.--- MHz</span>
</div> </div>
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Signals</span> <span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Signals</span>
+304
View File
@@ -0,0 +1,304 @@
{#
Global Navigation Partial
Single source of truth for app navigation
Compatible with:
- index.html (uses switchMode() for mode panels)
- Dashboard pages (uses navigation links)
Variables:
- active_mode: Current active mode (e.g., 'pager', 'adsb', 'wifi')
- is_index_page: If true, Satellite/SSTV use switchMode (panel mode)
If false (default), Satellite links to dashboard
#}
{% set is_index_page = is_index_page|default(false) %}
{% macro mode_item(mode, label, icon_svg, href=None) -%}
{%- set is_active = 'active' if active_mode == mode else '' -%}
{%- if href %}
<a href="{{ href }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
<span class="nav-label">{{ label }}</span>
</a>
{%- elif is_index_page %}
<button class="mode-nav-btn {{ is_active }}" onclick="switchMode('{{ mode }}')">
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
<span class="nav-label">{{ label }}</span>
</button>
{%- else %}
<a href="/?mode={{ mode }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
<span class="nav-label">{{ label }}</span>
</a>
{%- endif %}
{%- endmacro %}
{% macro mobile_item(mode, label, icon_svg, href=None) -%}
{%- set is_active = 'active' if active_mode == mode else '' -%}
{%- if href %}
<a href="{{ href }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
</a>
{%- elif is_index_page %}
<button class="mobile-nav-btn {{ is_active }}" data-mode="{{ mode }}" onclick="switchMode('{{ mode }}')">
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
</button>
{%- else %}
<a href="/?mode={{ mode }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
</a>
{%- endif %}
{%- endmacro %}
{# Desktop Navigation - uses existing CSS class names for compatibility #}
<nav class="mode-nav" id="mainNav">
{# SDR / RF Group #}
<div class="mode-nav-dropdown" data-group="sdr">
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('sdr')"{% endif %}>
<span class="nav-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="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
<span class="nav-label">SDR / RF</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
{{ mode_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
{{ mode_item('sensor', '433MHz', '<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="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
{{ 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>') }}
</div>
</div>
{# Wireless Group #}
<div class="mode-nav-dropdown" data-group="wireless">
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('wireless')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
<span class="nav-label">Wireless</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
{{ mode_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg>') }}
{{ mode_item('bluetooth', 'Bluetooth', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
</div>
</div>
{# Security Group #}
<div class="mode-nav-dropdown" data-group="security">
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('security')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<span class="nav-label">Security</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
{{ mode_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
</div>
</div>
{# Space Group #}
<div class="mode-nav-dropdown" data-group="space">
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('space')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
<span class="nav-label">Space</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="mode-nav-dropdown-menu">
{% if is_index_page %}
{{ 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>') }}
{% else %}
{{ 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>') }}
</div>
</div>
{# Dynamic dashboard button (shown when in satellite mode) #}
<div class="mode-nav-actions">
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span>
<span class="nav-label">Full Dashboard</span>
</a>
</div>
{# Nav Utilities (clock, theme, tools) #}
<div class="nav-utilities">
<div class="nav-clock">
<span class="utc-label">UTC</span>
<span class="utc-time" id="headerUtcTime">--:--:--</span>
</div>
<div class="nav-divider"></div>
<a href="/" class="nav-dashboard-btn" title="Return to Main Dashboard" style="text-decoration: none;">
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>
<span class="nav-label">Main Dashboard</span>
</a>
<div class="nav-divider"></div>
<div class="nav-tools">
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
</button>
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
<span class="icon-sun 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="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
</button>
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;">
<span class="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>
</a>
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;">
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span>
</a>
<button class="nav-tool-btn" onclick="showSettings()" title="Settings">
<span class="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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
</button>
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
</button>
</div>
</div>
</nav>
{# Mobile Navigation Bar #}
<nav class="mobile-nav-bar" id="mobileNavBar">
{{ mobile_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
{{ mobile_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
{{ mobile_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
{% if is_index_page %}
{{ 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>') }}
{% else %}
{{ 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('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>') }}
</nav>
{# JavaScript stub for pages that don't have switchMode defined #}
<script>
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
if (typeof switchMode === 'undefined') {
window.switchMode = function(mode) {
// On dashboard pages, navigate to main page with mode param
window.location.href = '/?mode=' + mode;
};
}
if (typeof toggleNavDropdown === 'undefined') {
window.toggleNavDropdown = function(groupName) {
const dropdown = document.querySelector(`.mode-nav-dropdown[data-group="${groupName}"]`);
if (!dropdown) return;
// Close other dropdowns
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => {
if (d !== dropdown) d.classList.remove('open');
});
dropdown.classList.toggle('open');
};
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.mode-nav-dropdown')) {
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => d.classList.remove('open'));
}
});
}
if (typeof toggleAnimations === 'undefined') {
window.toggleAnimations = function() {
const html = document.documentElement;
const current = html.getAttribute('data-animations') || 'on';
const next = current === 'on' ? 'off' : 'on';
html.setAttribute('data-animations', next);
localStorage.setItem('animations', next);
};
}
if (typeof toggleTheme === 'undefined') {
window.toggleTheme = function() {
const html = document.documentElement;
const current = html.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('intercept-theme', next);
};
}
if (typeof showSettings === 'undefined') {
window.showSettings = function() {
// Try to open settings modal if it exists on this page
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.add('active');
if (typeof Settings !== 'undefined' && Settings.init) {
Settings.init().then(() => {
if (Settings.checkAssets) Settings.checkAssets();
});
}
} else {
// Fall back to navigating to main page settings
window.location.href = '/?settings=1';
}
};
}
if (typeof hideSettings === 'undefined') {
window.hideSettings = function() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.remove('active');
}
};
}
// showHelp is defined by the help-modal.html partial
if (typeof logout === 'undefined') {
window.logout = function(e) {
if (e) e.preventDefault();
if (confirm('Are you sure you want to logout?')) {
window.location.href = '/logout';
}
};
}
// Apply saved preferences and start clock
(function() {
const savedTheme = localStorage.getItem('intercept-theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
const savedAnimations = localStorage.getItem('intercept-animations');
if (savedAnimations) {
document.documentElement.setAttribute('data-animations', savedAnimations);
}
// UTC Clock update (if not already defined by parent page)
if (typeof window._navClockStarted === 'undefined') {
window._navClockStarted = true;
function updateNavUtcClock() {
const now = new Date();
const utc = now.toISOString().slice(11, 19);
const el = document.getElementById('headerUtcTime');
if (el) el.textContent = utc;
}
setInterval(updateNavUtcClock, 1000);
updateNavUtcClock();
}
})();
</script>
+37
View File
@@ -0,0 +1,37 @@
{#
Page Header Partial
Consistent page title with optional description and actions
Variables:
- title: Page title (required)
- description: Optional description text
- back_url: Optional back link URL
- back_text: Optional back link text (default: "Back")
#}
<div class="page-header">
{% if back_url %}
<a href="{{ back_url }}" class="back-link mb-4">
<span class="icon icon--sm">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</span>
{{ back_text|default('Back') }}
</a>
{% endif %}
<div class="flex items-center justify-between">
<div>
<h1 class="page-title">{{ title }}</h1>
{% if description %}
<p class="page-description">{{ description }}</p>
{% endif %}
</div>
{% if caller is defined %}
<div class="page-actions">
{{ caller() }}
</div>
{% endif %}
</div>
</div>
+317 -318
View File
@@ -1,318 +1,317 @@
<!-- Settings Modal --> <!-- Settings Modal -->
<div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()"> <div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()">
<div class="settings-content"> <div class="settings-content">
<div class="settings-header"> <div class="settings-header">
<h2> <h2>
<span class="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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span> <span class="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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
Settings Settings
</h2> </h2>
<button class="settings-close" onclick="hideSettings()">&times;</button> <button class="settings-close" onclick="hideSettings()">&times;</button>
</div> </div>
<div class="settings-tabs"> <div class="settings-tabs">
<button class="settings-tab active" data-tab="offline" onclick="switchSettingsTab('offline')">Offline</button> <button class="settings-tab active" data-tab="offline" onclick="switchSettingsTab('offline')">Offline</button>
<button class="settings-tab" data-tab="location" onclick="switchSettingsTab('location')">Location</button> <button class="settings-tab" data-tab="location" onclick="switchSettingsTab('location')">Location</button>
<button class="settings-tab" data-tab="display" onclick="switchSettingsTab('display')">Display</button> <button class="settings-tab" data-tab="display" onclick="switchSettingsTab('display')">Display</button>
<button class="settings-tab" data-tab="updates" onclick="switchSettingsTab('updates')">Updates</button> <button class="settings-tab" data-tab="updates" onclick="switchSettingsTab('updates')">Updates</button>
<button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button> <button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button>
<button class="settings-tab" data-tab="about" onclick="switchSettingsTab('about')">About</button> <button class="settings-tab" data-tab="about" onclick="switchSettingsTab('about')">About</button>
</div> </div>
<!-- Offline Section --> <!-- Offline Section -->
<div id="settings-offline" class="settings-section active"> <div id="settings-offline" class="settings-section active">
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Offline Mode</div> <div class="settings-group-title">Offline Mode</div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Enable Offline Mode</span> <span class="settings-label-text">Enable Offline Mode</span>
<span class="settings-label-desc">Use local assets instead of CDN</span> <span class="settings-label-desc">Use local assets instead of CDN</span>
</div> </div>
<label class="toggle-switch"> <label class="toggle-switch">
<input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)"> <input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)">
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Asset Sources</div> <div class="settings-group-title">Asset Sources</div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">JavaScript/CSS Libraries</span> <span class="settings-label-text">JavaScript/CSS Libraries</span>
<span class="settings-label-desc">Leaflet, Chart.js</span> <span class="settings-label-desc">Leaflet, Chart.js</span>
</div> </div>
<select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)"> <select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)">
<option value="cdn">CDN (Online)</option> <option value="cdn">CDN (Online)</option>
<option value="local">Local</option> <option value="local">Local</option>
</select> </select>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Web Fonts</span> <span class="settings-label-text">Web Fonts</span>
<span class="settings-label-desc">Inter, JetBrains Mono</span> <span class="settings-label-desc">Space Mono</span>
</div> </div>
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)"> <select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
<option value="cdn">Google Fonts (Online)</option> <option value="cdn">Google Fonts (Online)</option>
<option value="local">Local</option> <option value="local">Local</option>
</select> </select>
</div> </div>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Map Tiles</div> <div class="settings-group-title">Map Tiles</div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Tile Provider</span> <span class="settings-label-text">Tile Provider</span>
<span class="settings-label-desc">Map background imagery</span> <span class="settings-label-desc">Map background imagery</span>
</div> </div>
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)"> <select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
<option value="openstreetmap">OpenStreetMap</option> <option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_dark">CartoDB Dark</option> <option value="cartodb_dark">CartoDB Dark</option>
<option value="cartodb_light">CartoDB Positron</option> <option value="cartodb_light">CartoDB Positron</option>
<option value="esri_world">ESRI World Imagery</option> <option value="esri_world">ESRI World Imagery</option>
<option value="custom">Custom URL</option> <option value="custom">Custom URL</option>
</select> </select>
</div> </div>
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;"> <div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
<div class="settings-label" style="width: 100%;"> <div class="settings-label" style="width: 100%;">
<span class="settings-label-text">Custom Tile URL</span> <span class="settings-label-text">Custom Tile URL</span>
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span> <span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
<input type="text" id="customTileUrl" class="settings-input" <input type="text" id="customTileUrl" class="settings-input"
placeholder="http://tile-server/{z}/{x}/{y}.png" placeholder="http://tile-server/{z}/{x}/{y}.png"
onchange="Settings.setCustomTileUrl(this.value)"> onchange="Settings.setCustomTileUrl(this.value)">
</div> </div>
</div> </div>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Local Asset Status</div> <div class="settings-group-title">Local Asset Status</div>
<div class="asset-status" id="assetStatus"> <div class="asset-status" id="assetStatus">
<div class="asset-status-row"> <div class="asset-status-row">
<span class="asset-name">Leaflet JS/CSS</span> <span class="asset-name">Leaflet JS/CSS</span>
<span class="asset-badge checking" id="statusLeaflet">Checking...</span> <span class="asset-badge checking" id="statusLeaflet">Checking...</span>
</div> </div>
<div class="asset-status-row"> <div class="asset-status-row">
<span class="asset-name">Chart.js</span> <span class="asset-name">Chart.js</span>
<span class="asset-badge checking" id="statusChartjs">Checking...</span> <span class="asset-badge checking" id="statusChartjs">Checking...</span>
</div> </div>
<div class="asset-status-row"> <div class="asset-status-row">
<span class="asset-name">Inter Font</span> <span class="asset-name">Inter Font</span>
<span class="asset-badge checking" id="statusInter">Checking...</span> <span class="asset-badge checking" id="statusInter">Checking...</span>
</div> </div>
<div class="asset-status-row"> <div class="asset-status-row">
<span class="asset-name">JetBrains Mono</span> <span class="asset-name">Space Mono</span>
<span class="asset-badge checking" id="statusJetbrains">Checking...</span> <span class="asset-badge checking" id="statusJetbrains">Checking...</span>
</div> </div>
</div> </div>
<button class="check-assets-btn" onclick="Settings.checkAssets()"> <button class="check-assets-btn" onclick="Settings.checkAssets()">
Check Assets Check Assets
</button> </button>
</div> </div>
<div class="settings-info"> <div class="settings-info">
<strong>Note:</strong> Changes to asset sources require a page reload to take effect. <strong>Note:</strong> Changes to asset sources require a page reload to take effect.
Local assets must be available in <code>/static/vendor/</code>. Local assets must be available in <code>/static/vendor/</code>.
</div> </div>
</div> </div>
<!-- Location Section --> <!-- Location Section -->
<div id="settings-location" class="settings-section"> <div id="settings-location" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Observer Location</div> <div class="settings-group-title">Observer Location</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;"> <p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Set your geographic coordinates for satellite pass predictions and ISS tracking. Set your geographic coordinates for satellite pass predictions and ISS tracking.
</p> </p>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Latitude</span> <span class="settings-label-text">Latitude</span>
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span> <span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
</div> </div>
<input type="number" id="observerLatInput" class="settings-input" <input type="number" id="observerLatInput" class="settings-input"
step="0.0001" min="-90" max="90" placeholder="51.5074" step="0.0001" min="-90" max="90" placeholder="51.5074"
style="width: 120px; text-align: right;"> style="width: 120px; text-align: right;">
</div> </div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Longitude</span> <span class="settings-label-text">Longitude</span>
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span> <span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
</div> </div>
<input type="number" id="observerLonInput" class="settings-input" <input type="number" id="observerLonInput" class="settings-input"
step="0.0001" min="-180" max="180" placeholder="-0.1278" step="0.0001" min="-180" max="180" placeholder="-0.1278"
style="width: 120px; text-align: right;"> style="width: 120px; text-align: right;">
</div> </div>
<div style="display: flex; gap: 10px; margin-top: 15px;"> <div style="display: flex; gap: 10px; margin-top: 15px;">
<button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;"> <button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<line x1="12" y1="2" x2="12" y2="6"/> <line x1="12" y1="2" x2="12" y2="6"/>
<line x1="12" y1="18" x2="12" y2="22"/> <line x1="12" y1="18" x2="12" y2="22"/>
<line x1="2" y1="12" x2="6" y2="12"/> <line x1="2" y1="12" x2="6" y2="12"/>
<line x1="18" y1="12" x2="22" y2="12"/> <line x1="18" y1="12" x2="22" y2="12"/>
</svg> </svg>
Use GPS Use GPS
</button> </button>
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;"> <button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
Save Location Save Location
</button> </button>
</div> </div>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Current Location</div> <div class="settings-group-title">Current Location</div>
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12px;"> <div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: var(--font-mono); font-size: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;"> <div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: var(--text-dim);">Latitude</span> <span style="color: var(--text-dim);">Latitude</span>
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span> <span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
</div> </div>
<div style="display: flex; justify-content: space-between;"> <div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Longitude</span> <span style="color: var(--text-dim);">Longitude</span>
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span> <span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
</div> </div>
</div> </div>
</div> </div>
<div class="settings-info"> <div class="settings-info">
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking. <strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
Your location is stored locally and never sent to external servers. Your location is stored locally and never sent to external servers.
</div> </div>
</div> </div>
<!-- Display Section --> <!-- Display Section -->
<div id="settings-display" class="settings-section"> <div id="settings-display" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Visual Preferences</div> <div class="settings-group-title">Visual Preferences</div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Theme</span> <span class="settings-label-text">Theme</span>
<span class="settings-label-desc">Color scheme preference</span> <span class="settings-label-desc">Color scheme preference</span>
</div> </div>
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)"> <select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
<option value="dark">Dark</option> <option value="dark">Dark</option>
<option value="light">Light</option> <option value="light">Light</option>
</select> </select>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Animations</span> <span class="settings-label-text">Animations</span>
<span class="settings-label-desc">Enable visual effects and animations</span> <span class="settings-label-desc">Enable visual effects and animations</span>
</div> </div>
<label class="toggle-switch"> <label class="toggle-switch">
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)"> <input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<!-- Updates Section --> <!-- Updates Section -->
<div id="settings-updates" class="settings-section"> <div id="settings-updates" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Update Status</div> <div class="settings-group-title">Update Status</div>
<div id="updateStatusContent" style="padding: 10px 0;"> <div id="updateStatusContent" style="padding: 10px 0;">
<div style="text-align: center; padding: 20px; color: var(--text-dim);"> <div style="text-align: center; padding: 20px; color: var(--text-dim);">
Loading update status... Loading update status...
</div> </div>
</div> </div>
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;"> <button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
Check Now Check Now
</button> </button>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Update Settings</div> <div class="settings-group-title">Update Settings</div>
<div class="settings-row"> <div class="settings-row">
<div class="settings-label"> <div class="settings-label">
<span class="settings-label-text">Auto-Check for Updates</span> <span class="settings-label-text">Auto-Check for Updates</span>
<span class="settings-label-desc">Periodically check GitHub for new releases</span> <span class="settings-label-desc">Periodically check GitHub for new releases</span>
</div> </div>
<label class="toggle-switch"> <label class="toggle-switch">
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)"> <input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
</div> </div>
<div class="settings-info"> <div class="settings-info">
<strong>Note:</strong> Updates are fetched from GitHub and applied via git pull. <strong>Note:</strong> Updates are fetched from GitHub and applied via git pull.
Make sure you have git installed and the application is in a git repository. Make sure you have git installed and the application is in a git repository.
</div> </div>
</div> </div>
<!-- Tools Section --> <!-- Tools Section -->
<div id="settings-tools" class="settings-section"> <div id="settings-tools" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Tool Dependencies</div> <div class="settings-group-title">Tool Dependencies</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;"> <p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Check which external tools are installed for each mode. Check which external tools are installed for each mode.
<span style="color: var(--accent-green);"></span> = Installed, <span style="color: var(--accent-green);"></span> = Installed,
<span style="color: var(--accent-red);"></span> = Missing <span style="color: var(--accent-red);"></span> = Missing
</p> </p>
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;"> <div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
<div style="text-align: center; padding: 30px; color: var(--text-dim);"> <div style="text-align: center; padding: 30px; color: var(--text-dim);">
Loading dependencies... Loading dependencies...
</div> </div>
</div> </div>
</div> </div>
<div class="settings-group" style="margin-top: 15px;"> <div class="settings-group" style="margin-top: 15px;">
<div class="settings-group-title">Quick Install (Debian/Ubuntu)</div> <div class="settings-group-title">Quick Install (Debian/Ubuntu)</div>
<div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;"> <div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;">
<div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div> <div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div>
<div style="margin-top: 5px;">pip install skyfield flask</div> <div style="margin-top: 5px;">pip install skyfield flask</div>
</div> </div>
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);"> <div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);">
<strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source. <strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source.
See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation. See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation.
</div> </div>
</div> </div>
</div> </div>
<!-- About Section --> <!-- About Section -->
<div id="settings-about" class="settings-section"> <div id="settings-about" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="about-info"> <div class="about-info">
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p> <p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
<p>Version: <span class="about-version">{{ version }}</span></p> <p>Version: <span class="about-version">{{ version }}</span></p>
<p> <p>
A unified web interface for software-defined radio (SDR) tools, A unified web interface for software-defined radio (SDR) tools,
supporting pager decoding, sensor monitoring, aircraft tracking, supporting pager decoding, sensor monitoring, aircraft tracking,
WiFi/Bluetooth scanning, and more. WiFi/Bluetooth scanning, and more.
</p> </p>
<p> <p>
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a> <a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
</p> </p>
</div> </div>
</div> </div>
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Support the Project</div> <div class="settings-group-title">Support the Project</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;"> <p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
If you find iNTERCEPT useful, consider supporting its development. If you find iNTERCEPT useful, consider supporting its development.
</p> </p>
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn"> <a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/> <path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/> <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
<line x1="6" y1="2" x2="6" y2="4"/> <line x1="6" y1="2" x2="6" y2="4"/>
<line x1="10" y1="2" x2="10" y2="4"/> <line x1="10" y1="2" x2="10" y2="4"/>
<line x1="14" y1="2" x2="14" y2="4"/> <line x1="14" y1="2" x2="14" y2="4"/>
</svg> </svg>
Buy Me a Coffee Buy Me a Coffee
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
+84 -2
View File
@@ -9,11 +9,12 @@ import logging
import os import os
import re import re
import subprocess import subprocess
import sys
import time import time
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from urllib.request import urlopen, Request from urllib.error import HTTPError, URLError
from urllib.error import URLError, HTTPError from urllib.request import Request, urlopen
import config import config
from utils.database import get_setting, set_setting from utils.database import get_setting, set_setting
@@ -509,6 +510,7 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
'success': True, 'success': True,
'updated': True, 'updated': True,
'message': 'Update successful! Please restart the application.', 'message': 'Update successful! Please restart the application.',
'restart_required': True,
'requirements_changed': requirements_changed, 'requirements_changed': requirements_changed,
'stashed': stashed, 'stashed': stashed,
'stash_restored': stashed, 'stash_restored': stashed,
@@ -527,3 +529,83 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
'success': False, 'success': False,
'error': str(e) 'error': str(e)
} }
def restart_application() -> dict[str, Any]:
"""
Restart the application using os.execv to replace the current process.
This function:
1. Cleans up all running decoder processes
2. Stops the cleanup manager
3. Replaces the current process with a fresh Python interpreter
Returns:
Dict with status (though this is typically not reached due to execv)
"""
import app as app_module
from utils.cleanup import cleanup_manager
from utils.process import cleanup_all_processes
logger.info("Application restart requested")
try:
# Step 1: Kill all decoder processes
logger.info("Stopping all decoder processes...")
cleanup_all_processes()
# Step 2: Clear global process state
with app_module.process_lock:
app_module.current_process = None
with app_module.sensor_lock:
app_module.sensor_process = None
with app_module.wifi_lock:
app_module.wifi_process = None
with app_module.adsb_lock:
app_module.adsb_process = None
with app_module.ais_lock:
app_module.ais_process = None
with app_module.acars_lock:
app_module.acars_process = None
with app_module.aprs_lock:
app_module.aprs_process = None
app_module.aprs_rtl_process = None
with app_module.dsc_lock:
app_module.dsc_process = None
app_module.dsc_rtl_process = None
# Step 3: Clear SDR device registry
with app_module.sdr_device_registry_lock:
app_module.sdr_device_registry.clear()
# Step 4: Stop cleanup manager
logger.info("Stopping cleanup manager...")
cleanup_manager.stop()
# Step 5: Prepare for restart using os.execv
# Get the Python executable and script path
python_executable = sys.executable
script_path = os.path.abspath(sys.argv[0])
# Build argument list (preserve original command-line args)
args = [python_executable, script_path] + sys.argv[1:]
logger.info(f"Restarting with: {' '.join(args)}")
# Flush any pending log output
logging.shutdown()
# Use os.execv to replace the current process
# This will not return - the process is replaced entirely
os.execv(python_executable, args)
# This code is never reached
return {'success': True, 'message': 'Restarting...'}
except Exception as e:
logger.error(f"Restart failed: {e}")
return {
'success': False,
'error': str(e),
'message': 'Failed to restart application. Please restart manually.'
}