Merge branch 'codex/new-ui'

# Conflicts:
#	static/css/index.css
This commit is contained in:
Smittix
2026-02-04 15:11:24 +00:00
52 changed files with 28450 additions and 23160 deletions
+7 -3
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
+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
```
+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
+18 -16
View File
@@ -5,6 +5,8 @@
} }
:root { :root {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains 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;
@@ -94,7 +96,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,7 +134,7 @@ body {
display: flex; display: flex;
gap: 20px; gap: 20px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -624,7 +626,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 +682,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 +702,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 +792,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 +803,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 +881,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 +913,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 +1025,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 +1059,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);
@@ -1407,7 +1409,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 +1547,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 +1786,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 +1940,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: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains 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,
+345 -343
View File
@@ -1,343 +1,345 @@
/* /*
* Agents Management CSS * Agents Management CSS
* Styles for the remote agent management interface * Styles for the remote agent management interface
*/ */
/* CSS Variables (inherited from main theme) */ /* CSS Variables (inherited from main theme) */
:root { :root {
--bg-primary: #0a0a0f; --font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--bg-secondary: #12121a; --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--text-primary: #e0e0e0; --bg-primary: #0a0a0f;
--text-secondary: #888; --bg-secondary: #12121a;
--border-color: #1a1a2e; --text-primary: #e0e0e0;
--accent-cyan: #00d4ff; --text-secondary: #888;
--accent-green: #00ff88; --border-color: #1a1a2e;
--accent-red: #ff3366; --accent-cyan: #00d4ff;
--accent-orange: #ff9f1c; --accent-green: #00ff88;
} --accent-red: #ff3366;
--accent-orange: #ff9f1c;
/* Agent indicator in navigation */ }
.agent-indicator {
display: flex; /* Agent indicator in navigation */
align-items: center; .agent-indicator {
gap: 8px; display: flex;
padding: 6px 12px; align-items: center;
background: rgba(0, 212, 255, 0.1); gap: 8px;
border: 1px solid rgba(0, 212, 255, 0.3); padding: 6px 12px;
border-radius: 20px; background: rgba(0, 212, 255, 0.1);
cursor: pointer; border: 1px solid rgba(0, 212, 255, 0.3);
transition: all 0.2s; border-radius: 20px;
} cursor: pointer;
transition: all 0.2s;
.agent-indicator:hover { }
background: rgba(0, 212, 255, 0.2);
border-color: var(--accent-cyan); .agent-indicator:hover {
} background: rgba(0, 212, 255, 0.2);
border-color: var(--accent-cyan);
.agent-indicator-dot { }
width: 8px;
height: 8px; .agent-indicator-dot {
border-radius: 50%; width: 8px;
background: var(--accent-green); height: 8px;
box-shadow: 0 0 6px var(--accent-green); border-radius: 50%;
} background: var(--accent-green);
box-shadow: 0 0 6px var(--accent-green);
.agent-indicator-dot.remote { }
background: var(--accent-cyan);
box-shadow: 0 0 6px var(--accent-cyan); .agent-indicator-dot.remote {
} background: var(--accent-cyan);
box-shadow: 0 0 6px var(--accent-cyan);
.agent-indicator-dot.multiple { }
background: var(--accent-orange);
box-shadow: 0 0 6px var(--accent-orange); .agent-indicator-dot.multiple {
} background: var(--accent-orange);
box-shadow: 0 0 6px var(--accent-orange);
.agent-indicator-label { }
font-size: 11px;
color: var(--text-primary); .agent-indicator-label {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
} color: var(--text-primary);
font-family: var(--font-mono);
.agent-indicator-count { }
font-size: 10px;
padding: 2px 6px; .agent-indicator-count {
background: rgba(0, 212, 255, 0.2); font-size: 10px;
border-radius: 10px; padding: 2px 6px;
color: var(--accent-cyan); background: rgba(0, 212, 255, 0.2);
} border-radius: 10px;
color: var(--accent-cyan);
/* Agent selector dropdown */ }
.agent-selector {
position: relative; /* Agent selector dropdown */
} .agent-selector {
position: relative;
.agent-selector-dropdown { }
position: absolute;
top: 100%; .agent-selector-dropdown {
right: 0; position: absolute;
margin-top: 8px; top: 100%;
min-width: 280px; right: 0;
background: var(--bg-secondary); margin-top: 8px;
border: 1px solid var(--border-color); min-width: 280px;
border-radius: 8px; background: var(--bg-secondary);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); border: 1px solid var(--border-color);
z-index: 1000; border-radius: 8px;
display: none; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
} z-index: 1000;
display: none;
.agent-selector-dropdown.show { }
display: block;
} .agent-selector-dropdown.show {
display: block;
.agent-selector-header { }
display: flex;
justify-content: space-between; .agent-selector-header {
align-items: center; display: flex;
padding: 12px 15px; justify-content: space-between;
border-bottom: 1px solid var(--border-color); align-items: center;
} padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
.agent-selector-header h4 { }
margin: 0;
font-size: 12px; .agent-selector-header h4 {
color: var(--accent-cyan); margin: 0;
text-transform: uppercase; font-size: 12px;
letter-spacing: 1px; color: var(--accent-cyan);
} text-transform: uppercase;
letter-spacing: 1px;
.agent-selector-manage { }
font-size: 11px;
color: var(--accent-cyan); .agent-selector-manage {
text-decoration: none; font-size: 11px;
} color: var(--accent-cyan);
text-decoration: none;
.agent-selector-manage:hover { }
text-decoration: underline;
} .agent-selector-manage:hover {
text-decoration: underline;
.agent-selector-list { }
max-height: 300px;
overflow-y: auto; .agent-selector-list {
} max-height: 300px;
overflow-y: auto;
.agent-selector-item { }
display: flex;
align-items: center; .agent-selector-item {
gap: 10px; display: flex;
padding: 10px 15px; align-items: center;
cursor: pointer; gap: 10px;
transition: background 0.2s; padding: 10px 15px;
border-bottom: 1px solid var(--border-color); cursor: pointer;
} transition: background 0.2s;
border-bottom: 1px solid var(--border-color);
.agent-selector-item:last-child { }
border-bottom: none;
} .agent-selector-item:last-child {
border-bottom: none;
.agent-selector-item:hover { }
background: rgba(0, 212, 255, 0.1);
} .agent-selector-item:hover {
background: rgba(0, 212, 255, 0.1);
.agent-selector-item.selected { }
background: rgba(0, 212, 255, 0.15);
border-left: 3px solid var(--accent-cyan); .agent-selector-item.selected {
} background: rgba(0, 212, 255, 0.15);
border-left: 3px solid var(--accent-cyan);
.agent-selector-item.local { }
border-left: 3px solid var(--accent-green);
} .agent-selector-item.local {
border-left: 3px solid var(--accent-green);
.agent-selector-item-status { }
width: 8px;
height: 8px; .agent-selector-item-status {
border-radius: 50%; width: 8px;
flex-shrink: 0; height: 8px;
} border-radius: 50%;
flex-shrink: 0;
.agent-selector-item-status.online { }
background: var(--accent-green);
} .agent-selector-item-status.online {
background: var(--accent-green);
.agent-selector-item-status.offline { }
background: var(--accent-red);
} .agent-selector-item-status.offline {
background: var(--accent-red);
.agent-selector-item-info { }
flex: 1;
min-width: 0; .agent-selector-item-info {
} flex: 1;
min-width: 0;
.agent-selector-item-name { }
font-size: 13px;
color: var(--text-primary); .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-url { }
font-size: 10px;
color: var(--text-secondary); .agent-selector-item-url {
font-family: 'JetBrains Mono', monospace; font-size: 10px;
white-space: nowrap; color: var(--text-secondary);
overflow: hidden; font-family: var(--font-mono);
text-overflow: ellipsis; white-space: nowrap;
} overflow: hidden;
text-overflow: ellipsis;
.agent-selector-item-check { }
color: var(--accent-green);
opacity: 0; .agent-selector-item-check {
} color: var(--accent-green);
opacity: 0;
.agent-selector-item.selected .agent-selector-item-check { }
opacity: 1;
} .agent-selector-item.selected .agent-selector-item-check {
opacity: 1;
/* Agent badge in data displays */ }
.agent-badge {
display: inline-flex; /* Agent badge in data displays */
align-items: center; .agent-badge {
gap: 4px; display: inline-flex;
padding: 2px 8px; align-items: center;
font-size: 10px; gap: 4px;
background: rgba(0, 212, 255, 0.1); padding: 2px 8px;
color: var(--accent-cyan); font-size: 10px;
border-radius: 10px; background: rgba(0, 212, 255, 0.1);
font-family: 'JetBrains Mono', monospace; color: var(--accent-cyan);
} border-radius: 10px;
font-family: var(--font-mono);
.agent-badge.local, }
.agent-badge.agent-local {
background: rgba(0, 255, 136, 0.1); .agent-badge.local,
color: var(--accent-green); .agent-badge.agent-local {
} background: rgba(0, 255, 136, 0.1);
color: var(--accent-green);
.agent-badge.agent-remote { }
background: rgba(0, 212, 255, 0.1);
color: var(--accent-cyan); .agent-badge.agent-remote {
} background: rgba(0, 212, 255, 0.1);
color: var(--accent-cyan);
/* WiFi table agent column */ }
.wifi-networks-table .col-agent {
width: 100px; /* WiFi table agent column */
text-align: center; .wifi-networks-table .col-agent {
} width: 100px;
text-align: center;
.wifi-networks-table th.col-agent { }
font-size: 10px;
} .wifi-networks-table th.col-agent {
font-size: 10px;
/* Bluetooth table agent column */ }
.bt-devices-table .col-agent {
width: 100px; /* Bluetooth table agent column */
text-align: center; .bt-devices-table .col-agent {
} width: 100px;
text-align: center;
.agent-badge-dot { }
width: 6px;
height: 6px; .agent-badge-dot {
border-radius: 50%; width: 6px;
background: currentColor; height: 6px;
} border-radius: 50%;
background: currentColor;
/* Agent column in data tables */ }
.data-table .agent-col {
width: 120px; /* Agent column in data tables */
max-width: 120px; .data-table .agent-col {
} width: 120px;
max-width: 120px;
/* Multi-agent stream indicator */ }
.multi-agent-indicator {
position: fixed; /* Multi-agent stream indicator */
bottom: 20px; .multi-agent-indicator {
left: 20px; position: fixed;
display: flex; bottom: 20px;
align-items: center; left: 20px;
gap: 8px; display: flex;
padding: 8px 12px; align-items: center;
background: var(--bg-secondary); gap: 8px;
border: 1px solid var(--border-color); padding: 8px 12px;
border-radius: 20px; background: var(--bg-secondary);
font-size: 11px; border: 1px solid var(--border-color);
color: var(--text-secondary); border-radius: 20px;
z-index: 100; font-size: 11px;
} color: var(--text-secondary);
z-index: 100;
.multi-agent-indicator.active { }
border-color: var(--accent-cyan);
color: var(--accent-cyan); .multi-agent-indicator.active {
} border-color: var(--accent-cyan);
color: var(--accent-cyan);
.multi-agent-indicator-pulse { }
width: 8px;
height: 8px; .multi-agent-indicator-pulse {
border-radius: 50%; width: 8px;
background: var(--accent-cyan); height: 8px;
animation: pulse 2s infinite; border-radius: 50%;
} background: var(--accent-cyan);
animation: pulse 2s infinite;
@keyframes pulse { }
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); } @keyframes pulse {
} 0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
/* Agent connection status toast */ }
.agent-toast {
position: fixed; /* Agent connection status toast */
top: 80px; .agent-toast {
right: 20px; position: fixed;
padding: 10px 15px; top: 80px;
background: var(--bg-secondary); right: 20px;
border: 1px solid var(--border-color); padding: 10px 15px;
border-radius: 6px; background: var(--bg-secondary);
font-size: 12px; border: 1px solid var(--border-color);
z-index: 1001; border-radius: 6px;
animation: slideInRight 0.3s ease; font-size: 12px;
} z-index: 1001;
animation: slideInRight 0.3s ease;
.agent-toast.connected { }
border-color: var(--accent-green);
color: var(--accent-green); .agent-toast.connected {
} border-color: var(--accent-green);
color: var(--accent-green);
.agent-toast.disconnected { }
border-color: var(--accent-red);
color: var(--accent-red); .agent-toast.disconnected {
} border-color: var(--accent-red);
color: var(--accent-red);
@keyframes slideInRight { }
from {
transform: translateX(100%); @keyframes slideInRight {
opacity: 0; from {
} transform: translateX(100%);
to { opacity: 0;
transform: translateX(0); }
opacity: 1; to {
} transform: translateX(0);
} opacity: 1;
}
/* Responsive adjustments */ }
@media (max-width: 768px) {
.agent-indicator { /* Responsive adjustments */
padding: 4px 8px; @media (max-width: 768px) {
} .agent-indicator {
padding: 4px 8px;
.agent-indicator-label { }
display: none;
} .agent-indicator-label {
display: none;
.agent-selector-dropdown { }
position: fixed;
top: auto; .agent-selector-dropdown {
bottom: 0; position: fixed;
left: 0; top: auto;
right: 0; bottom: 0;
margin: 0; left: 0;
border-radius: 16px 16px 0 0; right: 0;
max-height: 60vh; margin: 0;
} border-radius: 16px 16px 0 0;
max-height: 60vh;
.agents-grid { }
grid-template-columns: 1fr;
} .agents-grid {
} grid-template-columns: 1fr;
}
}
+23 -21
View File
@@ -8,6 +8,8 @@
} }
:root { :root {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains 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;
@@ -134,7 +136,7 @@ body {
display: flex; display: flex;
gap: 20px; gap: 20px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -183,7 +185,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 +289,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;
@@ -367,7 +369,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 */
@@ -518,7 +520,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 +550,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);
} }
@@ -611,13 +613,13 @@ body {
} }
.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 +689,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 +700,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 +719,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;
@@ -1004,7 +1006,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 +1081,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 +1098,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 +1122,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);
} }
@@ -1157,7 +1159,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 +1179,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 +1190,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); }
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: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', '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;
}
}
+67 -67
View File
@@ -1,67 +1,67 @@
/* Local font declarations for offline mode */ /* Local font declarations for offline mode */
/* Inter - Primary UI font */ /* Inter - Primary UI font */
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
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/Inter-Regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2'); src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2'); src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2'); src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
} }
/* JetBrains Mono - Monospace/code font */ /* JetBrains Mono - Monospace/code font */
@font-face { @font-face {
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2'); src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2'); src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2'); src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2'); src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
} }
+332
View File
@@ -0,0 +1,332 @@
/* ============================================
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);
}
.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;
}
+215 -294
View File
File diff suppressed because it is too large Load Diff
+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
+10 -8
View File
@@ -5,6 +5,8 @@
} }
:root { :root {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains 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;
} }
@@ -164,7 +166,7 @@ body {
display: flex; display: flex;
gap: 20px; gap: 20px;
align-items: center; align-items: center;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
} }
@@ -457,7 +459,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 +545,7 @@ body {
} }
.pass-time { .pass-time {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
} }
/* Bottom controls bar */ /* Bottom controls bar */
@@ -579,7 +581,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;
} }
@@ -748,4 +750,4 @@ body.embedded .panel {
body.embedded .controls-bar { body.embedded .controls-bar {
padding: 10px 15px; padding: 10px 15px;
} }
+444 -444
View File
@@ -1,444 +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;
} }
/* GPS Detection Spinner */ /* GPS Detection Spinner */
.detecting-spinner { .detecting-spinner {
display: inline-block; display: inline-block;
width: 12px; width: 12px;
height: 12px; height: 12px;
border: 2px solid currentColor; border: 2px solid currentColor;
border-top-color: transparent; border-top-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: detecting-spin 0.8s linear infinite; animation: detecting-spin 0.8s linear infinite;
vertical-align: middle; vertical-align: middle;
margin-right: 6px; margin-right: 6px;
} }
@keyframes detecting-spin { @keyframes detecting-spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* About Section */ /* About Section */
.about-info { .about-info {
font-size: 13px; font-size: 13px;
color: var(--text-muted, #888); color: var(--text-muted, #888);
line-height: 1.6; line-height: 1.6;
} }
.about-info p { .about-info p {
margin: 0 0 12px 0; margin: 0 0 12px 0;
} }
.about-info a { .about-info a {
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
text-decoration: none; text-decoration: none;
} }
.about-info a:hover { .about-info a:hover {
text-decoration: underline; text-decoration: underline;
} }
.about-version { .about-version {
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
} }
/* Donate Button */ /* Donate Button */
.donate-btn { .donate-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 10px 20px; padding: 10px 20px;
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%); background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
border: none; border: none;
border-radius: 6px; border-radius: 6px;
color: #000; color: #000;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3); box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
} }
.donate-btn:hover { .donate-btn:hover {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4); box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
filter: brightness(1.1); filter: brightness(1.1);
} }
.donate-btn:active { .donate-btn:active {
transform: translateY(0); transform: translateY(0);
} }
/* Tile Provider Custom URL */ /* Tile Provider Custom URL */
.custom-url-row { .custom-url-row {
margin-top: 8px; margin-top: 8px;
padding-top: 8px; padding-top: 8px;
} }
.custom-url-row .settings-input { .custom-url-row .settings-input {
width: 100%; width: 100%;
} }
/* Info Callout */ /* Info Callout */
.settings-info { .settings-info {
background: rgba(0, 212, 255, 0.1); background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2); border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
margin-top: 16px; margin-top: 16px;
font-size: 12px; font-size: 12px;
color: var(--text-muted, #888); color: var(--text-muted, #888);
} }
.settings-info strong { .settings-info strong {
color: var(--accent-cyan, #00d4ff); color: var(--accent-cyan, #00d4ff);
} }
/* Responsive */ /* Responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
.settings-modal.active { .settings-modal.active {
padding: 20px 10px; padding: 20px 10px;
} }
.settings-content { .settings-content {
max-width: 100%; max-width: 100%;
} }
.settings-row { .settings-row {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
} }
.settings-select, .settings-select,
.settings-input { .settings-input {
width: 100%; width: 100%;
} }
} }
+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');
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('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('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: "JetBrains 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: "JetBrains 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
+4904 -4899
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -4,8 +4,13 @@
<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/adsb_history.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
</head> </head>
<body> <body>
@@ -22,6 +27,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">
@@ -761,5 +769,6 @@
} }
}); });
</script> </script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body> </body>
</html> </html>
+592 -588
View File
File diff suppressed because it is too large Load Diff
+7 -2
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' %}
@@ -20,6 +20,7 @@
{% endif %} {% endif %}
<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') }}">
<script> <script>
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }}; window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
</script> </script>
@@ -51,6 +52,9 @@
</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">
@@ -1495,7 +1499,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 {
@@ -1549,6 +1553,7 @@
<!-- 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>
+96 -120
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' %}
@@ -290,7 +290,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 +348,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>
@@ -1087,7 +989,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>
@@ -1107,7 +1009,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>
@@ -1153,6 +1055,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
@@ -1165,6 +1071,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;">
@@ -1259,10 +1172,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"
@@ -1335,7 +1248,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>
@@ -1344,15 +1257,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>
@@ -1410,19 +1325,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>
@@ -2117,7 +2032,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);
} }
@@ -2126,6 +2041,35 @@
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 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');
@@ -2137,7 +2081,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);
} }
@@ -2456,6 +2407,9 @@
// Start SDR device status polling // Start SDR device status polling
startSdrStatusPolling(); startSdrStatusPolling();
// Apply mode from URL query (e.g., /?mode=wifi)
applyModeFromQuery();
}); });
// Toggle section collapse // Toggle section collapse
@@ -2511,8 +2465,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) {
@@ -2525,6 +2491,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') {
@@ -2757,6 +2726,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(() => {
@@ -12205,7 +12181,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=JetBrains+Mono:wght@400;500;600;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('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
+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>
+283
View File
@@ -0,0 +1,283 @@
{#
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>
<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('theme', next);
};
}
if (typeof showSettings === 'undefined') {
window.showSettings = function() {
// Navigate to main page settings
window.location.href = '/?settings=1';
};
}
if (typeof showHelp === 'undefined') {
window.showHelp = function() {
window.open('https://smittix.github.io/intercept', '_blank');
};
}
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('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
const savedAnimations = localStorage.getItem('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">JetBrains 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">JetBrains 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