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
def require_login():
# Routes that don't require login (to avoid infinite redirect loop)
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']
def require_login():
# Routes that don't require login (to avoid infinite redirect loop)
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
# 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:
pass
except Exception as e:
if "timed out" not in str(e).lower():
logger.error(f"WebSocket receive error: {e}")
except Exception as e:
msg = str(e).lower()
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
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 {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10;
--bg-panel: #0f1218;
--bg-card: #151a23;
@@ -25,7 +27,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: var(--font-sans);
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
@@ -94,7 +96,7 @@ body {
}
.logo {
font-family: 'Inter', sans-serif;
font-family: var(--font-sans);
font-size: 16px;
font-weight: 700;
letter-spacing: 2px;
@@ -132,7 +134,7 @@ body {
display: flex;
gap: 20px;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -624,7 +626,7 @@ body {
}
.telemetry-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent-cyan);
}
@@ -680,7 +682,7 @@ body {
}
.aircraft-icao {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-secondary);
background: rgba(74, 158, 255, 0.1);
@@ -700,7 +702,7 @@ body {
}
.aircraft-detail-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
color: var(--accent-cyan);
font-size: 11px;
}
@@ -790,7 +792,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
}
@@ -801,7 +803,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
}
@@ -879,7 +881,7 @@ body {
border: none;
background: var(--accent-green);
color: #fff;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
@@ -911,7 +913,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
cursor: pointer;
}
@@ -1023,7 +1025,7 @@ body {
cursor: pointer;
font-size: 11px;
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
display: flex;
align-items: center;
gap: 5px;
@@ -1057,7 +1059,7 @@ body {
}
.airband-status {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
padding: 0 8px;
color: var(--text-muted);
@@ -1407,7 +1409,7 @@ body {
}
.strip-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan);
@@ -1545,7 +1547,7 @@ body {
.report-grid span:nth-child(even) {
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
.report-highlights {
@@ -1784,7 +1786,7 @@ body {
font-size: 11px;
font-weight: 500;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
padding-left: 8px;
border-left: 1px solid rgba(74, 158, 255, 0.2);
white-space: nowrap;
@@ -1938,7 +1940,7 @@ body {
}
.squawk-code {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-weight: 700;
color: var(--accent-cyan);
font-size: 12px;
+8 -6
View File
@@ -5,6 +5,8 @@
}
: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-panel: #0f1218;
--bg-card: #141a24;
@@ -20,14 +22,14 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: var(--font-sans);
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
}
.mono {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
.radar-bg {
@@ -91,7 +93,7 @@ body {
display: flex;
align-items: center;
gap: 12px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -268,7 +270,7 @@ body {
}
.status-pill {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
padding: 8px 12px;
border-radius: 999px;
@@ -306,7 +308,7 @@ body {
}
.panel-meta {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-cyan);
}
@@ -347,7 +349,7 @@ body {
}
.mono {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
.empty-row td,
+345 -343
View File
@@ -1,343 +1,345 @@
/*
* Agents Management CSS
* Styles for the remote agent management interface
*/
/* CSS Variables (inherited from main theme) */
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--text-primary: #e0e0e0;
--text-secondary: #888;
--border-color: #1a1a2e;
--accent-cyan: #00d4ff;
--accent-green: #00ff88;
--accent-red: #ff3366;
--accent-orange: #ff9f1c;
}
/* Agent indicator in navigation */
.agent-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
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-dot {
width: 8px;
height: 8px;
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.multiple {
background: var(--accent-orange);
box-shadow: 0 0 6px var(--accent-orange);
}
.agent-indicator-label {
font-size: 11px;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
}
.agent-indicator-count {
font-size: 10px;
padding: 2px 6px;
background: rgba(0, 212, 255, 0.2);
border-radius: 10px;
color: var(--accent-cyan);
}
/* Agent selector dropdown */
.agent-selector {
position: relative;
}
.agent-selector-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
min-width: 280px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
}
.agent-selector-dropdown.show {
display: block;
}
.agent-selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.agent-selector-header h4 {
margin: 0;
font-size: 12px;
color: var(--accent-cyan);
text-transform: uppercase;
letter-spacing: 1px;
}
.agent-selector-manage {
font-size: 11px;
color: var(--accent-cyan);
text-decoration: none;
}
.agent-selector-manage:hover {
text-decoration: underline;
}
.agent-selector-list {
max-height: 300px;
overflow-y: auto;
}
.agent-selector-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 15px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--border-color);
}
.agent-selector-item:last-child {
border-bottom: none;
}
.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.local {
border-left: 3px solid var(--accent-green);
}
.agent-selector-item-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.agent-selector-item-status.online {
background: var(--accent-green);
}
.agent-selector-item-status.offline {
background: var(--accent-red);
}
.agent-selector-item-info {
flex: 1;
min-width: 0;
}
.agent-selector-item-name {
font-size: 13px;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-selector-item-url {
font-size: 10px;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-selector-item-check {
color: var(--accent-green);
opacity: 0;
}
.agent-selector-item.selected .agent-selector-item-check {
opacity: 1;
}
/* Agent badge in data displays */
.agent-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: 10px;
background: rgba(0, 212, 255, 0.1);
color: var(--accent-cyan);
border-radius: 10px;
font-family: 'JetBrains Mono', monospace;
}
.agent-badge.local,
.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);
}
/* WiFi table agent column */
.wifi-networks-table .col-agent {
width: 100px;
text-align: center;
}
.wifi-networks-table th.col-agent {
font-size: 10px;
}
/* Bluetooth table agent column */
.bt-devices-table .col-agent {
width: 100px;
text-align: center;
}
.agent-badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* Agent column in data tables */
.data-table .agent-col {
width: 120px;
max-width: 120px;
}
/* Multi-agent stream indicator */
.multi-agent-indicator {
position: fixed;
bottom: 20px;
left: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
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-pulse {
width: 8px;
height: 8px;
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); }
}
/* Agent connection status toast */
.agent-toast {
position: fixed;
top: 80px;
right: 20px;
padding: 10px 15px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
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.disconnected {
border-color: var(--accent-red);
color: var(--accent-red);
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.agent-indicator {
padding: 4px 8px;
}
.agent-indicator-label {
display: none;
}
.agent-selector-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-radius: 16px 16px 0 0;
max-height: 60vh;
}
.agents-grid {
grid-template-columns: 1fr;
}
}
/*
* Agents Management CSS
* Styles for the remote agent management interface
*/
/* CSS Variables (inherited from main theme) */
:root {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--text-primary: #e0e0e0;
--text-secondary: #888;
--border-color: #1a1a2e;
--accent-cyan: #00d4ff;
--accent-green: #00ff88;
--accent-red: #ff3366;
--accent-orange: #ff9f1c;
}
/* Agent indicator in navigation */
.agent-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
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-dot {
width: 8px;
height: 8px;
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.multiple {
background: var(--accent-orange);
box-shadow: 0 0 6px var(--accent-orange);
}
.agent-indicator-label {
font-size: 11px;
color: var(--text-primary);
font-family: var(--font-mono);
}
.agent-indicator-count {
font-size: 10px;
padding: 2px 6px;
background: rgba(0, 212, 255, 0.2);
border-radius: 10px;
color: var(--accent-cyan);
}
/* Agent selector dropdown */
.agent-selector {
position: relative;
}
.agent-selector-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
min-width: 280px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
}
.agent-selector-dropdown.show {
display: block;
}
.agent-selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.agent-selector-header h4 {
margin: 0;
font-size: 12px;
color: var(--accent-cyan);
text-transform: uppercase;
letter-spacing: 1px;
}
.agent-selector-manage {
font-size: 11px;
color: var(--accent-cyan);
text-decoration: none;
}
.agent-selector-manage:hover {
text-decoration: underline;
}
.agent-selector-list {
max-height: 300px;
overflow-y: auto;
}
.agent-selector-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 15px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--border-color);
}
.agent-selector-item:last-child {
border-bottom: none;
}
.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.local {
border-left: 3px solid var(--accent-green);
}
.agent-selector-item-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.agent-selector-item-status.online {
background: var(--accent-green);
}
.agent-selector-item-status.offline {
background: var(--accent-red);
}
.agent-selector-item-info {
flex: 1;
min-width: 0;
}
.agent-selector-item-name {
font-size: 13px;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-selector-item-url {
font-size: 10px;
color: var(--text-secondary);
font-family: var(--font-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-selector-item-check {
color: var(--accent-green);
opacity: 0;
}
.agent-selector-item.selected .agent-selector-item-check {
opacity: 1;
}
/* Agent badge in data displays */
.agent-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: 10px;
background: rgba(0, 212, 255, 0.1);
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);
color: var(--accent-green);
}
.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;
text-align: center;
}
.wifi-networks-table th.col-agent {
font-size: 10px;
}
/* Bluetooth table agent column */
.bt-devices-table .col-agent {
width: 100px;
text-align: center;
}
.agent-badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* Agent column in data tables */
.data-table .agent-col {
width: 120px;
max-width: 120px;
}
/* Multi-agent stream indicator */
.multi-agent-indicator {
position: fixed;
bottom: 20px;
left: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
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-pulse {
width: 8px;
height: 8px;
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); }
}
/* Agent connection status toast */
.agent-toast {
position: fixed;
top: 80px;
right: 20px;
padding: 10px 15px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
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.disconnected {
border-color: var(--accent-red);
color: var(--accent-red);
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.agent-indicator {
padding: 4px 8px;
}
.agent-indicator-label {
display: none;
}
.agent-selector-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-radius: 16px 16px 0 0;
max-height: 60vh;
}
.agents-grid {
grid-template-columns: 1fr;
}
}
+23 -21
View File
@@ -8,6 +8,8 @@
}
: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-panel: #0f1218;
--bg-card: #151a23;
@@ -28,7 +30,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: var(--font-sans);
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
@@ -97,7 +99,7 @@ body {
}
.logo {
font-family: 'Inter', sans-serif;
font-family: var(--font-sans);
font-size: 16px;
font-weight: 700;
letter-spacing: 2px;
@@ -134,7 +136,7 @@ body {
display: flex;
gap: 20px;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -183,7 +185,7 @@ body {
}
.strip-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan);
@@ -287,7 +289,7 @@ body {
font-size: 11px;
font-weight: 500;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
padding-left: 8px;
border-left: 1px solid rgba(74, 158, 255, 0.2);
white-space: nowrap;
@@ -367,7 +369,7 @@ body {
/* Leaflet overrides - Dark map styling */
.leaflet-container {
background: var(--bg-dark) !important;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
/* Using actual dark tiles now - no filter needed */
@@ -518,7 +520,7 @@ body {
}
.vessel-mmsi {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-secondary);
background: rgba(74, 158, 255, 0.1);
@@ -548,7 +550,7 @@ body {
}
.detail-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent-cyan);
}
@@ -611,13 +613,13 @@ body {
}
.vessel-item-type {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-secondary);
}
.vessel-item-speed {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-cyan);
text-align: right;
@@ -687,7 +689,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
}
@@ -698,7 +700,7 @@ body {
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
}
@@ -717,7 +719,7 @@ body {
border: none;
background: var(--accent-green);
color: #fff;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
@@ -1004,7 +1006,7 @@ body {
padding: 6px 12px;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(245, 158, 11, 0.1);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
}
@@ -1079,7 +1081,7 @@ body {
}
.dsc-message-category {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
@@ -1096,13 +1098,13 @@ body {
}
.dsc-message-time {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-dim);
}
.dsc-message-mmsi {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-orange);
}
@@ -1120,7 +1122,7 @@ body {
}
.dsc-message-pos {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-secondary);
}
@@ -1157,7 +1159,7 @@ body {
}
.dsc-distress-alert .dsc-alert-mmsi {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 16px;
color: var(--accent-cyan);
margin-bottom: 8px;
@@ -1177,7 +1179,7 @@ body {
}
.dsc-distress-alert .dsc-alert-position {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 14px;
color: var(--accent-cyan);
margin-bottom: 16px;
@@ -1188,7 +1190,7 @@ body {
border: none;
color: white;
padding: 10px 24px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
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 */
/* Inter - Primary UI font */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
}
/* JetBrains Mono - Monospace/code font */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
}
/* Local font declarations for offline mode */
/* Inter - Primary UI font */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
}
/* JetBrains Mono - Monospace/code font */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
}
+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 */
.landing-title {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 2.2rem;
font-weight: 700;
letter-spacing: 0.4em;
@@ -48,7 +48,7 @@
}
.landing-tagline {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
color: var(--accent-cyan);
font-size: 0.9rem;
letter-spacing: 0.15em;
@@ -71,7 +71,7 @@
/* Hacker Style Error */
.flash-error {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
color: var(--accent-red);
background: rgba(239, 68, 68, 0.1);
@@ -94,7 +94,7 @@
color: var(--accent-cyan);
padding: 12px;
margin-bottom: 15px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
outline: none;
box-sizing: border-box; /* Crucial for visibility */
@@ -106,7 +106,7 @@
border: 2px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 15px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-weight: 600;
letter-spacing: 3px;
cursor: pointer;
@@ -116,7 +116,7 @@
.landing-version {
margin-top: 25px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
color: rgba(255, 255, 255, 0.3);
letter-spacing: 2px;
+328 -328
View File
@@ -1,328 +1,328 @@
/* APRS Function Bar (Stats Strip) Styles */
.aprs-strip {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px 12px;
margin-bottom: 10px;
overflow-x: auto;
}
.aprs-strip-inner {
display: flex;
align-items: center;
gap: 8px;
min-width: max-content;
}
.aprs-strip .strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 10px;
background: rgba(74, 158, 255, 0.05);
border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px;
min-width: 55px;
}
.aprs-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3);
}
.aprs-strip .strip-value {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan);
line-height: 1.2;
}
.aprs-strip .strip-label {
font-size: 8px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 1px;
}
.aprs-strip .strip-divider {
width: 1px;
height: 28px;
background: var(--border-color);
margin: 0 4px;
}
/* Signal stat coloring */
.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.poor .strip-value { color: var(--accent-red); }
/* Controls */
.aprs-strip .strip-control {
display: flex;
align-items: center;
gap: 4px;
}
.aprs-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;
}
.aprs-strip .strip-select:hover {
border-color: var(--accent-cyan);
}
.aprs-strip .strip-input-label {
font-size: 9px;
color: var(--text-muted);
font-weight: 600;
}
.aprs-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;
}
.aprs-strip .strip-input:hover,
.aprs-strip .strip-input:focus {
border-color: var(--accent-cyan);
outline: none;
}
/* Tool Status Indicators */
.aprs-strip .strip-tools {
display: flex;
gap: 4px;
}
.aprs-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);
}
.aprs-strip .strip-tool.ok {
background: rgba(0, 255, 136, 0.1);
color: var(--accent-green);
border-color: rgba(0, 255, 136, 0.3);
}
/* Buttons */
.aprs-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;
}
.aprs-strip .strip-btn:hover:not(:disabled) {
background: rgba(74, 158, 255, 0.2);
border-color: rgba(74, 158, 255, 0.4);
}
.aprs-strip .strip-btn.primary {
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
border: none;
color: #000;
}
.aprs-strip .strip-btn.primary:hover:not(:disabled) {
filter: brightness(1.1);
}
.aprs-strip .strip-btn.stop {
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
border: none;
color: #fff;
}
.aprs-strip .strip-btn.stop:hover:not(:disabled) {
filter: brightness(1.1);
}
.aprs-strip .strip-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Status indicator */
.aprs-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);
}
.aprs-strip .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.aprs-strip .status-dot.listening {
background: var(--accent-cyan);
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
}
.aprs-strip .status-dot.tracking {
background: var(--accent-green);
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
}
.aprs-strip .status-dot.error {
background: var(--accent-red);
}
@keyframes aprs-strip-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
50% { opacity: 0.6; box-shadow: none; }
}
/* Time display */
.aprs-strip .strip-time {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-muted);
padding: 4px 8px;
background: rgba(0,0,0,0.2);
border-radius: 4px;
white-space: nowrap;
}
/* APRS Status Bar Styles (Sidebar - legacy) */
.aprs-status-bar {
margin-top: 12px;
padding: 10px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.aprs-status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.aprs-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.aprs-status-dot.standby { background: var(--text-muted); }
.aprs-status-dot.listening {
background: var(--accent-cyan);
animation: aprs-pulse 1.5s ease-in-out infinite;
}
.aprs-status-dot.tracking { background: var(--accent-green); }
.aprs-status-dot.error { background: var(--accent-red); }
@keyframes aprs-pulse {
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); }
}
.aprs-status-text {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
}
.aprs-status-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 9px;
}
.aprs-stat {
color: var(--text-secondary);
}
.aprs-stat-label {
color: var(--text-muted);
}
/* Signal Meter Styles */
.aprs-signal-meter {
margin-top: 12px;
padding: 10px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.aprs-meter-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.aprs-meter-label {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
color: var(--text-secondary);
}
.aprs-meter-value {
font-size: 12px;
font-weight: bold;
font-family: monospace;
color: var(--accent-cyan);
min-width: 24px;
}
.aprs-meter-burst {
font-size: 9px;
font-weight: bold;
color: var(--accent-yellow);
background: rgba(255, 193, 7, 0.2);
padding: 2px 6px;
border-radius: 3px;
animation: burst-flash 0.3s ease-out;
}
@keyframes burst-flash {
0% { opacity: 1; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
.aprs-meter-bar-container {
position: relative;
height: 16px;
background: rgba(0,0,0,0.4);
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.aprs-meter-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg,
var(--accent-green) 0%,
var(--accent-cyan) 50%,
var(--accent-yellow) 75%,
var(--accent-red) 100%
);
border-radius: 3px;
transition: width 0.1s ease-out;
}
.aprs-meter-bar.no-signal {
opacity: 0.3;
}
.aprs-meter-ticks {
display: flex;
justify-content: space-between;
font-size: 8px;
color: var(--text-muted);
padding: 0 2px;
}
.aprs-meter-status {
font-size: 9px;
color: var(--text-muted);
text-align: center;
margin-top: 6px;
}
.aprs-meter-status.active {
color: var(--accent-green);
}
.aprs-meter-status.no-signal {
color: var(--accent-yellow);
}
/* APRS Function Bar (Stats Strip) Styles */
.aprs-strip {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 6px 12px;
margin-bottom: 10px;
overflow-x: auto;
}
.aprs-strip-inner {
display: flex;
align-items: center;
gap: 8px;
min-width: max-content;
}
.aprs-strip .strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 10px;
background: rgba(74, 158, 255, 0.05);
border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px;
min-width: 55px;
}
.aprs-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3);
}
.aprs-strip .strip-value {
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan);
line-height: 1.2;
}
.aprs-strip .strip-label {
font-size: 8px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 1px;
}
.aprs-strip .strip-divider {
width: 1px;
height: 28px;
background: var(--border-color);
margin: 0 4px;
}
/* Signal stat coloring */
.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.poor .strip-value { color: var(--accent-red); }
/* Controls */
.aprs-strip .strip-control {
display: flex;
align-items: center;
gap: 4px;
}
.aprs-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;
}
.aprs-strip .strip-select:hover {
border-color: var(--accent-cyan);
}
.aprs-strip .strip-input-label {
font-size: 9px;
color: var(--text-muted);
font-weight: 600;
}
.aprs-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;
}
.aprs-strip .strip-input:hover,
.aprs-strip .strip-input:focus {
border-color: var(--accent-cyan);
outline: none;
}
/* Tool Status Indicators */
.aprs-strip .strip-tools {
display: flex;
gap: 4px;
}
.aprs-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);
}
.aprs-strip .strip-tool.ok {
background: rgba(0, 255, 136, 0.1);
color: var(--accent-green);
border-color: rgba(0, 255, 136, 0.3);
}
/* Buttons */
.aprs-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;
}
.aprs-strip .strip-btn:hover:not(:disabled) {
background: rgba(74, 158, 255, 0.2);
border-color: rgba(74, 158, 255, 0.4);
}
.aprs-strip .strip-btn.primary {
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
border: none;
color: #000;
}
.aprs-strip .strip-btn.primary:hover:not(:disabled) {
filter: brightness(1.1);
}
.aprs-strip .strip-btn.stop {
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
border: none;
color: #fff;
}
.aprs-strip .strip-btn.stop:hover:not(:disabled) {
filter: brightness(1.1);
}
.aprs-strip .strip-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Status indicator */
.aprs-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);
}
.aprs-strip .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.aprs-strip .status-dot.listening {
background: var(--accent-cyan);
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
}
.aprs-strip .status-dot.tracking {
background: var(--accent-green);
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
}
.aprs-strip .status-dot.error {
background: var(--accent-red);
}
@keyframes aprs-strip-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
50% { opacity: 0.6; box-shadow: none; }
}
/* Time display */
.aprs-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;
}
/* APRS Status Bar Styles (Sidebar - legacy) */
.aprs-status-bar {
margin-top: 12px;
padding: 10px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.aprs-status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.aprs-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.aprs-status-dot.standby { background: var(--text-muted); }
.aprs-status-dot.listening {
background: var(--accent-cyan);
animation: aprs-pulse 1.5s ease-in-out infinite;
}
.aprs-status-dot.tracking { background: var(--accent-green); }
.aprs-status-dot.error { background: var(--accent-red); }
@keyframes aprs-pulse {
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); }
}
.aprs-status-text {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
}
.aprs-status-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 9px;
}
.aprs-stat {
color: var(--text-secondary);
}
.aprs-stat-label {
color: var(--text-muted);
}
/* Signal Meter Styles */
.aprs-signal-meter {
margin-top: 12px;
padding: 10px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.aprs-meter-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.aprs-meter-label {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
color: var(--text-secondary);
}
.aprs-meter-value {
font-size: 12px;
font-weight: bold;
font-family: monospace;
color: var(--accent-cyan);
min-width: 24px;
}
.aprs-meter-burst {
font-size: 9px;
font-weight: bold;
color: var(--accent-yellow);
background: rgba(255, 193, 7, 0.2);
padding: 2px 6px;
border-radius: 3px;
animation: burst-flash 0.3s ease-out;
}
@keyframes burst-flash {
0% { opacity: 1; transform: scale(1.1); }
100% { opacity: 1; transform: scale(1); }
}
.aprs-meter-bar-container {
position: relative;
height: 16px;
background: rgba(0,0,0,0.4);
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.aprs-meter-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg,
var(--accent-green) 0%,
var(--accent-cyan) 50%,
var(--accent-yellow) 75%,
var(--accent-red) 100%
);
border-radius: 3px;
transition: width 0.1s ease-out;
}
.aprs-meter-bar.no-signal {
opacity: 0.3;
}
.aprs-meter-ticks {
display: flex;
justify-content: space-between;
font-size: 8px;
color: var(--text-muted);
padding: 0 2px;
}
.aprs-meter-status {
font-size: 9px;
color: var(--text-muted);
text-align: center;
margin-top: 6px;
}
.aprs-meter-status.active {
color: var(--accent-green);
}
.aprs-meter-status.no-signal {
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 {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
@@ -101,7 +101,7 @@
}
.spy-station-name {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
@@ -117,7 +117,7 @@
/* Type Badge */
.spy-station-badge {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
@@ -173,7 +173,7 @@
}
.spy-meta-mode {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
color: var(--accent-orange);
}
@@ -186,7 +186,7 @@
}
.spy-freq-list {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-cyan);
line-height: 1.6;
@@ -199,7 +199,7 @@
}
.spy-freq-item {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-cyan);
background: var(--bg-secondary);
@@ -236,7 +236,7 @@
}
.spy-freq-select {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
padding: 6px 8px;
background: var(--bg-secondary);
@@ -273,7 +273,7 @@
display: inline-flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
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 {
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--bg-dark: #0a0c10;
--bg-panel: #0f1218;
--bg-card: #151a23;
@@ -23,7 +25,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: var(--font-sans);
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
@@ -93,7 +95,7 @@ body {
}
.logo {
font-family: 'Inter', sans-serif;
font-family: var(--font-sans);
font-size: 20px;
font-weight: 700;
letter-spacing: 3px;
@@ -142,7 +144,7 @@ body {
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 4px;
padding: 4px 10px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -164,7 +166,7 @@ body {
display: flex;
gap: 20px;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -457,7 +459,7 @@ body {
}
.telemetry-value {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent-cyan);
}
@@ -543,7 +545,7 @@ body {
}
.pass-time {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
/* Bottom controls bar */
@@ -579,7 +581,7 @@ body {
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 4px;
color: var(--accent-cyan);
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
}
@@ -748,4 +750,4 @@ body.embedded .panel {
body.embedded .controls-bar {
padding: 10px 15px;
}
}
+444 -444
View File
@@ -1,444 +1,444 @@
/* Settings Modal Styles */
.settings-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 10000;
overflow-y: auto;
backdrop-filter: blur(4px);
}
.settings-modal.active {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
}
.settings-content {
background: var(--bg-dark, #0a0a0f);
border: 1px solid var(--border-color, #1a1a2e);
border-radius: 8px;
max-width: 600px;
width: 100%;
position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color, #1a1a2e);
}
.settings-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
display: flex;
align-items: center;
gap: 8px;
}
.settings-header h2 .icon {
width: 20px;
height: 20px;
color: var(--accent-cyan, #00d4ff);
}
.settings-close {
background: none;
border: none;
color: var(--text-muted, #666);
font-size: 24px;
cursor: pointer;
padding: 4px;
line-height: 1;
transition: color 0.2s;
}
.settings-close:hover {
color: var(--accent-red, #ff4444);
}
/* Settings Tabs */
.settings-tabs {
display: flex;
border-bottom: 1px solid var(--border-color, #1a1a2e);
padding: 0 20px;
gap: 4px;
}
.settings-tab {
background: none;
border: none;
padding: 12px 16px;
color: var(--text-muted, #666);
font-size: 13px;
font-weight: 500;
cursor: pointer;
position: relative;
transition: color 0.2s;
}
.settings-tab:hover {
color: var(--text-primary, #e0e0e0);
}
.settings-tab.active {
color: var(--accent-cyan, #00d4ff);
}
.settings-tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--accent-cyan, #00d4ff);
}
/* Settings Sections */
.settings-section {
display: none;
padding: 20px;
}
.settings-section.active {
display: block;
}
.settings-group {
margin-bottom: 24px;
}
.settings-group:last-child {
margin-bottom: 0;
}
.settings-group-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted, #666);
margin-bottom: 12px;
}
/* Settings Row */
.settings-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.settings-row:last-child {
border-bottom: none;
}
.settings-label {
display: flex;
flex-direction: column;
gap: 2px;
}
.settings-label-text {
font-size: 13px;
color: var(--text-primary, #e0e0e0);
}
.settings-label-desc {
font-size: 11px;
color: var(--text-muted, #666);
}
/* Toggle Switch */
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: var(--text-muted, #666);
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--accent-cyan, #00d4ff);
border-color: var(--accent-cyan, #00d4ff);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
background-color: white;
}
.toggle-switch input:focus + .toggle-slider {
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
}
/* Select Dropdown */
.settings-select {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
color: var(--text-primary, #e0e0e0);
min-width: 160px;
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='%23666' 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: 32px;
}
.settings-select:focus {
outline: none;
border-color: var(--accent-cyan, #00d4ff);
}
/* Text Input */
.settings-input {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
color: var(--text-primary, #e0e0e0);
width: 200px;
}
.settings-input:focus {
outline: none;
border-color: var(--accent-cyan, #00d4ff);
}
.settings-input::placeholder {
color: var(--text-muted, #666);
}
/* Asset Status */
.asset-status {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding: 12px;
background: var(--bg-secondary, #0f0f1a);
border-radius: 6px;
}
.asset-status-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.asset-name {
color: var(--text-muted, #888);
}
.asset-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
}
.asset-badge.available {
background: rgba(0, 255, 136, 0.15);
color: var(--accent-green, #00ff88);
}
.asset-badge.missing {
background: rgba(255, 68, 68, 0.15);
color: var(--accent-red, #ff4444);
}
.asset-badge.checking {
background: rgba(255, 170, 0, 0.15);
color: var(--accent-orange, #ffaa00);
}
/* Check Assets Button */
.check-assets-btn {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
color: var(--text-primary, #e0e0e0);
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-top: 12px;
transition: all 0.2s;
}
.check-assets-btn:hover {
border-color: var(--accent-cyan, #00d4ff);
color: var(--accent-cyan, #00d4ff);
}
.check-assets-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* GPS Detection Spinner */
.detecting-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: detecting-spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
@keyframes detecting-spin {
to { transform: rotate(360deg); }
}
/* About Section */
.about-info {
font-size: 13px;
color: var(--text-muted, #888);
line-height: 1.6;
}
.about-info p {
margin: 0 0 12px 0;
}
.about-info a {
color: var(--accent-cyan, #00d4ff);
text-decoration: none;
}
.about-info a:hover {
text-decoration: underline;
}
.about-version {
font-family: 'JetBrains Mono', monospace;
color: var(--accent-cyan, #00d4ff);
}
/* Donate Button */
.donate-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
border: none;
border-radius: 6px;
color: #000;
font-size: 13px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
}
.donate-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
filter: brightness(1.1);
}
.donate-btn:active {
transform: translateY(0);
}
/* Tile Provider Custom URL */
.custom-url-row {
margin-top: 8px;
padding-top: 8px;
}
.custom-url-row .settings-input {
width: 100%;
}
/* Info Callout */
.settings-info {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px;
padding: 12px;
margin-top: 16px;
font-size: 12px;
color: var(--text-muted, #888);
}
.settings-info strong {
color: var(--accent-cyan, #00d4ff);
}
/* Responsive */
@media (max-width: 640px) {
.settings-modal.active {
padding: 20px 10px;
}
.settings-content {
max-width: 100%;
}
.settings-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.settings-select,
.settings-input {
width: 100%;
}
}
/* Settings Modal Styles */
.settings-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 10000;
overflow-y: auto;
backdrop-filter: blur(4px);
}
.settings-modal.active {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
}
.settings-content {
background: var(--bg-dark, #0a0a0f);
border: 1px solid var(--border-color, #1a1a2e);
border-radius: 8px;
max-width: 600px;
width: 100%;
position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color, #1a1a2e);
}
.settings-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
display: flex;
align-items: center;
gap: 8px;
}
.settings-header h2 .icon {
width: 20px;
height: 20px;
color: var(--accent-cyan, #00d4ff);
}
.settings-close {
background: none;
border: none;
color: var(--text-muted, #666);
font-size: 24px;
cursor: pointer;
padding: 4px;
line-height: 1;
transition: color 0.2s;
}
.settings-close:hover {
color: var(--accent-red, #ff4444);
}
/* Settings Tabs */
.settings-tabs {
display: flex;
border-bottom: 1px solid var(--border-color, #1a1a2e);
padding: 0 20px;
gap: 4px;
}
.settings-tab {
background: none;
border: none;
padding: 12px 16px;
color: var(--text-muted, #666);
font-size: 13px;
font-weight: 500;
cursor: pointer;
position: relative;
transition: color 0.2s;
}
.settings-tab:hover {
color: var(--text-primary, #e0e0e0);
}
.settings-tab.active {
color: var(--accent-cyan, #00d4ff);
}
.settings-tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--accent-cyan, #00d4ff);
}
/* Settings Sections */
.settings-section {
display: none;
padding: 20px;
}
.settings-section.active {
display: block;
}
.settings-group {
margin-bottom: 24px;
}
.settings-group:last-child {
margin-bottom: 0;
}
.settings-group-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted, #666);
margin-bottom: 12px;
}
/* Settings Row */
.settings-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.settings-row:last-child {
border-bottom: none;
}
.settings-label {
display: flex;
flex-direction: column;
gap: 2px;
}
.settings-label-text {
font-size: 13px;
color: var(--text-primary, #e0e0e0);
}
.settings-label-desc {
font-size: 11px;
color: var(--text-muted, #666);
}
/* Toggle Switch */
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: var(--text-muted, #666);
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--accent-cyan, #00d4ff);
border-color: var(--accent-cyan, #00d4ff);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
background-color: white;
}
.toggle-switch input:focus + .toggle-slider {
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
}
/* Select Dropdown */
.settings-select {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
color: var(--text-primary, #e0e0e0);
min-width: 160px;
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='%23666' 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: 32px;
}
.settings-select:focus {
outline: none;
border-color: var(--accent-cyan, #00d4ff);
}
/* Text Input */
.settings-input {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
color: var(--text-primary, #e0e0e0);
width: 200px;
}
.settings-input:focus {
outline: none;
border-color: var(--accent-cyan, #00d4ff);
}
.settings-input::placeholder {
color: var(--text-muted, #666);
}
/* Asset Status */
.asset-status {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding: 12px;
background: var(--bg-secondary, #0f0f1a);
border-radius: 6px;
}
.asset-status-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.asset-name {
color: var(--text-muted, #888);
}
.asset-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
}
.asset-badge.available {
background: rgba(0, 255, 136, 0.15);
color: var(--accent-green, #00ff88);
}
.asset-badge.missing {
background: rgba(255, 68, 68, 0.15);
color: var(--accent-red, #ff4444);
}
.asset-badge.checking {
background: rgba(255, 170, 0, 0.15);
color: var(--accent-orange, #ffaa00);
}
/* Check Assets Button */
.check-assets-btn {
background: var(--bg-tertiary, #1a1a2e);
border: 1px solid var(--border-color, #2a2a3e);
color: var(--text-primary, #e0e0e0);
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-top: 12px;
transition: all 0.2s;
}
.check-assets-btn:hover {
border-color: var(--accent-cyan, #00d4ff);
color: var(--accent-cyan, #00d4ff);
}
.check-assets-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* GPS Detection Spinner */
.detecting-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: detecting-spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
@keyframes detecting-spin {
to { transform: rotate(360deg); }
}
/* About Section */
.about-info {
font-size: 13px;
color: var(--text-muted, #888);
line-height: 1.6;
}
.about-info p {
margin: 0 0 12px 0;
}
.about-info a {
color: var(--accent-cyan, #00d4ff);
text-decoration: none;
}
.about-info a:hover {
text-decoration: underline;
}
.about-version {
font-family: var(--font-mono);
color: var(--accent-cyan, #00d4ff);
}
/* Donate Button */
.donate-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
border: none;
border-radius: 6px;
color: #000;
font-size: 13px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
}
.donate-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
filter: brightness(1.1);
}
.donate-btn:active {
transform: translateY(0);
}
/* Tile Provider Custom URL */
.custom-url-row {
margin-top: 8px;
padding-top: 8px;
}
.custom-url-row .settings-input {
width: 100%;
}
/* Info Callout */
.settings-info {
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px;
padding: 12px;
margin-top: 16px;
font-size: 12px;
color: var(--text-muted, #888);
}
.settings-info strong {
color: var(--accent-cyan, #00d4ff);
}
/* Responsive */
@media (max-width: 640px) {
.settings-modal.active {
padding: 20px 10px;
}
.settings-content {
max-width: 100%;
}
.settings-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.settings-select,
.settings-input {
width: 100%;
}
}
+494 -494
View File
@@ -1,36 +1,36 @@
/**
* Intercept - Core Application Logic
* Global state, mode switching, and shared functionality
*/
// ============== GLOBAL STATE ==============
// Mode state flags
let eventSource = null;
let isRunning = false;
let isSensorRunning = false;
let isAdsbRunning = false;
let isWifiRunning = false;
let isBtRunning = false;
let currentMode = 'pager';
// Message counters
let msgCount = 0;
let pocsagCount = 0;
let flexCount = 0;
let sensorCount = 0;
let filteredCount = 0;
// Device list (populated from server via Jinja2)
let deviceList = [];
// Auto-scroll setting
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
// Mute setting
let muted = localStorage.getItem('audioMuted') === 'true';
// Observer location (load from localStorage or default to London)
/**
* Intercept - Core Application Logic
* Global state, mode switching, and shared functionality
*/
// ============== GLOBAL STATE ==============
// Mode state flags
let eventSource = null;
let isRunning = false;
let isSensorRunning = false;
let isAdsbRunning = false;
let isWifiRunning = false;
let isBtRunning = false;
let currentMode = 'pager';
// Message counters
let msgCount = 0;
let pocsagCount = 0;
let flexCount = 0;
let sensorCount = 0;
let filteredCount = 0;
// Device list (populated from server via Jinja2)
let deviceList = [];
// Auto-scroll setting
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
// Mute setting
let muted = localStorage.getItem('audioMuted') === 'true';
// Observer location (load from localStorage or default to London)
let observerLocation = (function() {
if (window.ObserverLocation && ObserverLocation.getForModule) {
return ObserverLocation.getForModule('observerLocation');
@@ -44,464 +44,464 @@ let observerLocation = (function() {
}
return { lat: 51.5074, lon: -0.1278 };
})();
// Message storage for export
let allMessages = [];
// Track unique sensor devices
let uniqueDevices = new Set();
// SDR device usage tracking
let sdrDeviceUsage = {};
// ============== DISCLAIMER HANDLING ==============
function checkDisclaimer() {
const accepted = localStorage.getItem('disclaimerAccepted');
if (accepted === 'true') {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
}
}
function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
}
function declineDisclaimer() {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
}
// ============== HEADER CLOCK ==============
function updateHeaderClock() {
const now = new Date();
const utc = now.toISOString().substring(11, 19);
document.getElementById('headerUtcTime').textContent = utc;
}
// ============== MODE SWITCHING ==============
function switchMode(mode) {
// Stop any running scans when switching modes
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
currentMode = mode;
// Remove active from all nav buttons, then add to the correct one
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
const modeMap = {
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
'listening': 'listening', 'meshtastic': 'meshtastic'
};
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
const label = btn.querySelector('.nav-label');
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
btn.classList.add('active');
}
});
// Toggle mode content visibility
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
// Toggle stats visibility
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
// Hide signal meter - individual panels show signal strength where needed
document.getElementById('signalMeter').style.display = 'none';
// Show/hide dashboard buttons in nav bar
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
// Update active mode indicator
const modeNames = {
'pager': 'PAGER',
'sensor': '433MHZ',
'aircraft': 'AIRCRAFT',
'satellite': 'SATELLITE',
'wifi': 'WIFI',
'bluetooth': 'BLUETOOTH',
'listening': 'LISTENING POST',
'tscm': 'TSCM',
'aprs': 'APRS',
'meshtastic': 'MESHTASTIC'
};
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
// Update mobile nav buttons
updateMobileNavButtons(mode);
// Close mobile drawer when mode is switched (on mobile)
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
window.closeMobileDrawer();
}
// Toggle layout containers
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
// Respect the "Show Radar Display" checkbox for aircraft mode
const showRadar = document.getElementById('adsbEnableMap')?.checked;
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
// Update output panel title based on mode
const titles = {
'pager': 'Pager Decoder',
'sensor': '433MHz Sensor Monitor',
'aircraft': 'ADS-B Aircraft Tracker',
'satellite': 'Satellite Monitor',
'wifi': 'WiFi Scanner',
'bluetooth': 'Bluetooth Scanner',
'listening': 'Listening Post',
'meshtastic': 'Meshtastic Mesh Monitor'
};
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
// Show/hide Device Intelligence for modes that use it
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
document.getElementById('reconPanel').style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
} else {
if (reconBtn) reconBtn.style.display = 'inline-block';
if (intelBtn) intelBtn.style.display = 'inline-block';
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
document.getElementById('reconPanel').style.display = 'block';
}
}
// Show RTL-SDR device section for modes that use it
document.getElementById('rtlDeviceSection').style.display =
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
// Toggle mode-specific tool status displays
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
// Hide waterfall and output console for modes with their own visualizations
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';
document.getElementById('output').style.display =
(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';
// Load interfaces and initialize visualizations when switching modes
if (mode === 'wifi') {
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
if (typeof initRadar === 'function') initRadar();
if (typeof initWatchList === 'function') initWatchList();
} else if (mode === 'bluetooth') {
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
if (typeof initBtRadar === 'function') initBtRadar();
} else if (mode === 'aircraft') {
if (typeof checkAdsbTools === 'function') checkAdsbTools();
if (typeof initAircraftRadar === 'function') initAircraftRadar();
} else if (mode === 'satellite') {
if (typeof initPolarPlot === 'function') initPolarPlot();
if (typeof initSatelliteList === 'function') initSatelliteList();
} else if (mode === 'listening') {
if (typeof checkScannerTools === 'function') checkScannerTools();
if (typeof checkAudioTools === 'function') checkAudioTools();
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
} else if (mode === 'meshtastic') {
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
}
}
// ============== SECTION COLLAPSE ==============
function toggleSection(el) {
el.closest('.section').classList.toggle('collapsed');
}
// ============== THEME MANAGEMENT ==============
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update button text
const btn = document.getElementById('themeToggle');
if (btn) {
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
}
}
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
const btn = document.getElementById('themeToggle');
if (btn) {
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
}
}
// ============== AUTO-SCROLL ==============
function toggleAutoScroll() {
autoScroll = !autoScroll;
localStorage.setItem('autoScroll', autoScroll);
updateAutoScrollButton();
}
function updateAutoScrollButton() {
const btn = document.getElementById('autoScrollBtn');
if (btn) {
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
btn.classList.toggle('active', autoScroll);
}
}
// ============== SDR DEVICE MANAGEMENT ==============
function getSelectedDevice() {
return document.getElementById('deviceSelect').value;
}
function getSelectedSDRType() {
return document.getElementById('sdrTypeSelect').value;
}
function reserveDevice(deviceIndex, modeId) {
sdrDeviceUsage[modeId] = deviceIndex;
}
function releaseDevice(modeId) {
delete sdrDeviceUsage[modeId];
}
function checkDeviceAvailability(requestingMode) {
const selectedDevice = parseInt(getSelectedDevice());
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
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.`);
return false;
}
}
return true;
}
// ============== BIAS-T SETTINGS ==============
function saveBiasTSetting() {
const enabled = document.getElementById('biasT')?.checked || false;
localStorage.setItem('biasTEnabled', enabled);
}
function getBiasTEnabled() {
return document.getElementById('biasT')?.checked || false;
}
function loadBiasTSetting() {
const saved = localStorage.getItem('biasTEnabled');
if (saved === 'true') {
const checkbox = document.getElementById('biasT');
if (checkbox) checkbox.checked = true;
}
}
// ============== REMOTE SDR ==============
function toggleRemoteSDR() {
const useRemote = document.getElementById('useRemoteSDR').checked;
const configDiv = document.getElementById('remoteSDRConfig');
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
if (useRemote) {
configDiv.style.display = 'block';
localControls.forEach(el => el.disabled = true);
} else {
configDiv.style.display = 'none';
localControls.forEach(el => el.disabled = false);
}
}
function getRemoteSDRConfig() {
const useRemote = document.getElementById('useRemoteSDR')?.checked;
if (!useRemote) return null;
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
if (!host || isNaN(port)) {
alert('Please enter valid rtl_tcp host and port');
return false;
}
return { host, port };
}
// ============== OUTPUT DISPLAY ==============
function showInfo(text) {
const output = document.getElementById('output');
if (!output) return;
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove();
const infoEl = document.createElement('div');
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.textContent = text;
output.insertBefore(infoEl, output.firstChild);
}
function showError(text) {
const output = document.getElementById('output');
if (!output) return;
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove();
const errorEl = document.createElement('div');
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.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild);
}
// ============== INITIALIZATION ==============
// ============== MOBILE NAVIGATION ==============
function initMobileNav() {
const hamburgerBtn = document.getElementById('hamburgerBtn');
const sidebar = document.getElementById('mainSidebar');
const overlay = document.getElementById('drawerOverlay');
if (!hamburgerBtn || !sidebar || !overlay) return;
function openDrawer() {
sidebar.classList.add('open');
overlay.classList.add('visible');
hamburgerBtn.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
hamburgerBtn.classList.remove('active');
document.body.style.overflow = '';
}
function toggleDrawer() {
if (sidebar.classList.contains('open')) {
closeDrawer();
} else {
openDrawer();
}
}
hamburgerBtn.addEventListener('click', toggleDrawer);
overlay.addEventListener('click', closeDrawer);
// Close drawer when resizing to desktop
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) {
closeDrawer();
}
});
// Expose for external use
window.toggleMobileDrawer = toggleDrawer;
window.closeMobileDrawer = closeDrawer;
}
function setViewportHeight() {
// Fix for iOS Safari address bar height
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
function updateMobileNavButtons(mode) {
// Update mobile nav bar buttons
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
const btnMode = btn.getAttribute('data-mode');
btn.classList.toggle('active', btnMode === mode);
});
}
function initApp() {
// Check disclaimer
checkDisclaimer();
// Load theme
loadTheme();
// Start clock
updateHeaderClock();
setInterval(updateHeaderClock, 1000);
// Load bias-T setting
loadBiasTSetting();
// Initialize observer location inputs
const adsbLatInput = document.getElementById('adsbObsLat');
const adsbLonInput = document.getElementById('adsbObsLon');
const obsLatInput = document.getElementById('obsLat');
const obsLonInput = document.getElementById('obsLon');
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
// Update UI state
updateAutoScrollButton();
// Make sections collapsible
document.querySelectorAll('.section h3').forEach(h3 => {
h3.addEventListener('click', function() {
this.parentElement.classList.toggle('collapsed');
});
});
// Collapse all sections by default (except SDR Device which is first)
document.querySelectorAll('.section').forEach((section, index) => {
if (index > 0) {
section.classList.add('collapsed');
}
});
// Initialize mobile navigation
initMobileNav();
// Set viewport height for mobile browsers
setViewportHeight();
window.addEventListener('resize', setViewportHeight);
}
// Run initialization when DOM is ready
document.addEventListener('DOMContentLoaded', initApp);
// Message storage for export
let allMessages = [];
// Track unique sensor devices
let uniqueDevices = new Set();
// SDR device usage tracking
let sdrDeviceUsage = {};
// ============== DISCLAIMER HANDLING ==============
function checkDisclaimer() {
const accepted = localStorage.getItem('disclaimerAccepted');
if (accepted === 'true') {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
}
}
function acceptDisclaimer() {
localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
}
function declineDisclaimer() {
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
}
// ============== HEADER CLOCK ==============
function updateHeaderClock() {
const now = new Date();
const utc = now.toISOString().substring(11, 19);
document.getElementById('headerUtcTime').textContent = utc;
}
// ============== MODE SWITCHING ==============
function switchMode(mode) {
// Stop any running scans when switching modes
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
currentMode = mode;
// Remove active from all nav buttons, then add to the correct one
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
const modeMap = {
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
'listening': 'listening', 'meshtastic': 'meshtastic'
};
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
const label = btn.querySelector('.nav-label');
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
btn.classList.add('active');
}
});
// Toggle mode content visibility
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
// Toggle stats visibility
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
// Hide signal meter - individual panels show signal strength where needed
document.getElementById('signalMeter').style.display = 'none';
// Show/hide dashboard buttons in nav bar
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
// Update active mode indicator
const modeNames = {
'pager': 'PAGER',
'sensor': '433MHZ',
'aircraft': 'AIRCRAFT',
'satellite': 'SATELLITE',
'wifi': 'WIFI',
'bluetooth': 'BLUETOOTH',
'listening': 'LISTENING POST',
'tscm': 'TSCM',
'aprs': 'APRS',
'meshtastic': 'MESHTASTIC'
};
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
// Update mobile nav buttons
updateMobileNavButtons(mode);
// Close mobile drawer when mode is switched (on mobile)
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
window.closeMobileDrawer();
}
// Toggle layout containers
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
// Respect the "Show Radar Display" checkbox for aircraft mode
const showRadar = document.getElementById('adsbEnableMap')?.checked;
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
// Update output panel title based on mode
const titles = {
'pager': 'Pager Decoder',
'sensor': '433MHz Sensor Monitor',
'aircraft': 'ADS-B Aircraft Tracker',
'satellite': 'Satellite Monitor',
'wifi': 'WiFi Scanner',
'bluetooth': 'Bluetooth Scanner',
'listening': 'Listening Post',
'meshtastic': 'Meshtastic Mesh Monitor'
};
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
// Show/hide Device Intelligence for modes that use it
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
document.getElementById('reconPanel').style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
} else {
if (reconBtn) reconBtn.style.display = 'inline-block';
if (intelBtn) intelBtn.style.display = 'inline-block';
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
document.getElementById('reconPanel').style.display = 'block';
}
}
// Show RTL-SDR device section for modes that use it
document.getElementById('rtlDeviceSection').style.display =
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
// Toggle mode-specific tool status displays
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
// Hide waterfall and output console for modes with their own visualizations
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';
document.getElementById('output').style.display =
(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';
// Load interfaces and initialize visualizations when switching modes
if (mode === 'wifi') {
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
if (typeof initRadar === 'function') initRadar();
if (typeof initWatchList === 'function') initWatchList();
} else if (mode === 'bluetooth') {
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
if (typeof initBtRadar === 'function') initBtRadar();
} else if (mode === 'aircraft') {
if (typeof checkAdsbTools === 'function') checkAdsbTools();
if (typeof initAircraftRadar === 'function') initAircraftRadar();
} else if (mode === 'satellite') {
if (typeof initPolarPlot === 'function') initPolarPlot();
if (typeof initSatelliteList === 'function') initSatelliteList();
} else if (mode === 'listening') {
if (typeof checkScannerTools === 'function') checkScannerTools();
if (typeof checkAudioTools === 'function') checkAudioTools();
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
} else if (mode === 'meshtastic') {
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
}
}
// ============== SECTION COLLAPSE ==============
function toggleSection(el) {
el.closest('.section').classList.toggle('collapsed');
}
// ============== THEME MANAGEMENT ==============
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update button text
const btn = document.getElementById('themeToggle');
if (btn) {
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
}
}
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
const btn = document.getElementById('themeToggle');
if (btn) {
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
}
}
// ============== AUTO-SCROLL ==============
function toggleAutoScroll() {
autoScroll = !autoScroll;
localStorage.setItem('autoScroll', autoScroll);
updateAutoScrollButton();
}
function updateAutoScrollButton() {
const btn = document.getElementById('autoScrollBtn');
if (btn) {
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
btn.classList.toggle('active', autoScroll);
}
}
// ============== SDR DEVICE MANAGEMENT ==============
function getSelectedDevice() {
return document.getElementById('deviceSelect').value;
}
function getSelectedSDRType() {
return document.getElementById('sdrTypeSelect').value;
}
function reserveDevice(deviceIndex, modeId) {
sdrDeviceUsage[modeId] = deviceIndex;
}
function releaseDevice(modeId) {
delete sdrDeviceUsage[modeId];
}
function checkDeviceAvailability(requestingMode) {
const selectedDevice = parseInt(getSelectedDevice());
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
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.`);
return false;
}
}
return true;
}
// ============== BIAS-T SETTINGS ==============
function saveBiasTSetting() {
const enabled = document.getElementById('biasT')?.checked || false;
localStorage.setItem('biasTEnabled', enabled);
}
function getBiasTEnabled() {
return document.getElementById('biasT')?.checked || false;
}
function loadBiasTSetting() {
const saved = localStorage.getItem('biasTEnabled');
if (saved === 'true') {
const checkbox = document.getElementById('biasT');
if (checkbox) checkbox.checked = true;
}
}
// ============== REMOTE SDR ==============
function toggleRemoteSDR() {
const useRemote = document.getElementById('useRemoteSDR').checked;
const configDiv = document.getElementById('remoteSDRConfig');
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
if (useRemote) {
configDiv.style.display = 'block';
localControls.forEach(el => el.disabled = true);
} else {
configDiv.style.display = 'none';
localControls.forEach(el => el.disabled = false);
}
}
function getRemoteSDRConfig() {
const useRemote = document.getElementById('useRemoteSDR')?.checked;
if (!useRemote) return null;
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
if (!host || isNaN(port)) {
alert('Please enter valid rtl_tcp host and port');
return false;
}
return { host, port };
}
// ============== OUTPUT DISPLAY ==============
function showInfo(text) {
const output = document.getElementById('output');
if (!output) return;
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove();
const infoEl = document.createElement('div');
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.textContent = text;
output.insertBefore(infoEl, output.firstChild);
}
function showError(text) {
const output = document.getElementById('output');
if (!output) return;
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.remove();
const errorEl = document.createElement('div');
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.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild);
}
// ============== INITIALIZATION ==============
// ============== MOBILE NAVIGATION ==============
function initMobileNav() {
const hamburgerBtn = document.getElementById('hamburgerBtn');
const sidebar = document.getElementById('mainSidebar');
const overlay = document.getElementById('drawerOverlay');
if (!hamburgerBtn || !sidebar || !overlay) return;
function openDrawer() {
sidebar.classList.add('open');
overlay.classList.add('visible');
hamburgerBtn.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
hamburgerBtn.classList.remove('active');
document.body.style.overflow = '';
}
function toggleDrawer() {
if (sidebar.classList.contains('open')) {
closeDrawer();
} else {
openDrawer();
}
}
hamburgerBtn.addEventListener('click', toggleDrawer);
overlay.addEventListener('click', closeDrawer);
// Close drawer when resizing to desktop
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024) {
closeDrawer();
}
});
// Expose for external use
window.toggleMobileDrawer = toggleDrawer;
window.closeMobileDrawer = closeDrawer;
}
function setViewportHeight() {
// Fix for iOS Safari address bar height
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
function updateMobileNavButtons(mode) {
// Update mobile nav bar buttons
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
const btnMode = btn.getAttribute('data-mode');
btn.classList.toggle('active', btnMode === mode);
});
}
function initApp() {
// Check disclaimer
checkDisclaimer();
// Load theme
loadTheme();
// Start clock
updateHeaderClock();
setInterval(updateHeaderClock, 1000);
// Load bias-T setting
loadBiasTSetting();
// Initialize observer location inputs
const adsbLatInput = document.getElementById('adsbObsLat');
const adsbLonInput = document.getElementById('adsbObsLon');
const obsLatInput = document.getElementById('obsLat');
const obsLonInput = document.getElementById('obsLon');
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
// Update UI state
updateAutoScrollButton();
// Make sections collapsible
document.querySelectorAll('.section h3').forEach(h3 => {
h3.addEventListener('click', function() {
this.parentElement.classList.toggle('collapsed');
});
});
// Collapse all sections by default (except SDR Device which is first)
document.querySelectorAll('.section').forEach((section, index) => {
if (index > 0) {
section.classList.add('collapsed');
}
});
// Initialize mobile navigation
initMobileNav();
// Set viewport height for mobile browsers
setViewportHeight();
window.addEventListener('resize', setViewportHeight);
}
// Run initialization when DOM is ready
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 name="viewport" content="width=device-width, initial-scale=1.0">
<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/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
</head>
<body>
@@ -22,6 +27,9 @@
</div>
</header>
{% set active_mode = 'adsb' %}
{% include 'partials/nav.html' with context %}
<main class="history-shell">
<section class="summary-strip">
<div class="summary-card">
@@ -761,5 +769,6 @@
}
});
</script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</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' %}
<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&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 %}
<!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
@@ -20,6 +20,7 @@
{% endif %}
<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/global-nav.css') }}">
<script>
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
</script>
@@ -51,6 +52,9 @@
</div>
</header>
{% set active_mode = 'ais' %}
{% include 'partials/nav.html' with context %}
<div class="stats-strip">
<div class="stats-strip-inner">
<div class="strip-stat">
@@ -1495,7 +1499,7 @@
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
cursor: pointer;
}
.agent-select-sm:focus {
@@ -1549,6 +1553,7 @@
<!-- Agent Manager -->
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
<script>
// AIS-specific agent integration
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' %}
<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">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
@@ -290,7 +290,7 @@
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
<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
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
<span style="color: #666;">[sudo] password for user: ********</span><br>
@@ -348,107 +348,9 @@
</header>
<!-- Mode Navigation Bar -->
<nav class="mode-nav">
<div class="mode-nav-dropdown" data-group="sdr">
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('sdr')">
<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>
{% set is_index_page = true %}
{% set active_mode = 'pager' %}
{% include 'partials/nav.html' with context %}
<!-- Mobile Drawer Overlay -->
<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;">
PACKET LOG</h5>
<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>
</div>
@@ -1107,7 +1009,7 @@
STOPPED</div>
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
<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>
<span class="freq-unit"
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
@@ -1153,6 +1055,10 @@
Audio Output</div>
</div>
<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;">
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
<div
@@ -1165,6 +1071,13 @@
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
dB</span>
</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 -->
<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;">
@@ -1259,10 +1172,10 @@
<!-- SQL, Gain, Vol Knobs -->
<div style="display: flex; gap: 15px;">
<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>
<div class="knob-label">SQL</div>
<div class="knob-value" id="radioSquelchValue">30</div>
<div class="knob-value" id="radioSquelchValue">0</div>
</div>
<div class="knob-container">
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
@@ -1335,7 +1248,7 @@
START</div>
<input type="number" id="radioScanStart" value="118" step="0.1"
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>
<span
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
@@ -1344,15 +1257,17 @@
END</div>
<input type="number" id="radioScanEnd" value="137" step="0.1"
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>
<!-- Action Buttons -->
<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>
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()"
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 scan" id="radioScanBtn" onclick="toggleScanner()">
<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>
<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>
@@ -1410,19 +1325,19 @@
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</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>
</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</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>
</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</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>
</div>
</div>
@@ -2117,7 +2032,7 @@
// After fade out, hide welcome and switch to mode
setTimeout(() => {
welcome.style.display = 'none';
switchMode(mode);
switchMode(mode, { updateUrl: true });
}, 400);
}
@@ -2126,6 +2041,35 @@
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() {
localStorage.setItem('disclaimerAccepted', 'true');
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
@@ -2137,7 +2081,14 @@
const gateStyle = document.getElementById('disclaimer-gate');
if (gateStyle) gateStyle.remove();
// 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);
}
@@ -2456,6 +2407,9 @@
// Start SDR device status polling
startSdrStatusPolling();
// Apply mode from URL query (e.g., /?mode=wifi)
applyModeFromQuery();
});
// 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
function switchMode(mode) {
function switchMode(mode, options = {}) {
const { updateUrl = true } = options;
// Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
if (!isAgentMode) {
@@ -2525,6 +2491,9 @@
}
currentMode = mode;
if (updateUrl) {
updateModeUrl(mode);
}
// Sync mode state with current agent/local after switching
if (isAgentMode && typeof syncAgentModeStates === 'function') {
@@ -2757,6 +2726,13 @@
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
window.addEventListener('orientationchange', function () {
setTimeout(() => {
@@ -12205,7 +12181,7 @@
<textarea id="tleInput" placeholder="ISS (ZARYA)
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"
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
Satellite</button>
</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 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 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 style="display: flex; justify-content: space-between; align-items: center;">
<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 -->
<div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()">
<div class="settings-content">
<div class="settings-header">
<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>
Settings
</h2>
<button class="settings-close" onclick="hideSettings()">&times;</button>
</div>
<div class="settings-tabs">
<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="display" onclick="switchSettingsTab('display')">Display</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="about" onclick="switchSettingsTab('about')">About</button>
</div>
<!-- Offline Section -->
<div id="settings-offline" class="settings-section active">
<div class="settings-group">
<div class="settings-group-title">Offline Mode</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Enable Offline Mode</span>
<span class="settings-label-desc">Use local assets instead of CDN</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Asset Sources</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">JavaScript/CSS Libraries</span>
<span class="settings-label-desc">Leaflet, Chart.js</span>
</div>
<select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)">
<option value="cdn">CDN (Online)</option>
<option value="local">Local</option>
</select>
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Web Fonts</span>
<span class="settings-label-desc">Inter, JetBrains Mono</span>
</div>
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
<option value="cdn">Google Fonts (Online)</option>
<option value="local">Local</option>
</select>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Map Tiles</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Tile Provider</span>
<span class="settings-label-desc">Map background imagery</span>
</div>
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_dark">CartoDB Dark</option>
<option value="cartodb_light">CartoDB Positron</option>
<option value="esri_world">ESRI World Imagery</option>
<option value="custom">Custom URL</option>
</select>
</div>
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
<div class="settings-label" style="width: 100%;">
<span class="settings-label-text">Custom Tile URL</span>
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
<input type="text" id="customTileUrl" class="settings-input"
placeholder="http://tile-server/{z}/{x}/{y}.png"
onchange="Settings.setCustomTileUrl(this.value)">
</div>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Local Asset Status</div>
<div class="asset-status" id="assetStatus">
<div class="asset-status-row">
<span class="asset-name">Leaflet JS/CSS</span>
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">Chart.js</span>
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">Inter Font</span>
<span class="asset-badge checking" id="statusInter">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">JetBrains Mono</span>
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
</div>
</div>
<button class="check-assets-btn" onclick="Settings.checkAssets()">
Check Assets
</button>
</div>
<div class="settings-info">
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
Local assets must be available in <code>/static/vendor/</code>.
</div>
</div>
<!-- Location Section -->
<div id="settings-location" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Observer Location</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Set your geographic coordinates for satellite pass predictions and ISS tracking.
</p>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Latitude</span>
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
</div>
<input type="number" id="observerLatInput" class="settings-input"
step="0.0001" min="-90" max="90" placeholder="51.5074"
style="width: 120px; text-align: right;">
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Longitude</span>
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
</div>
<input type="number" id="observerLonInput" class="settings-input"
step="0.0001" min="-180" max="180" placeholder="-0.1278"
style="width: 120px; text-align: right;">
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<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;">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3"/>
<line x1="12" y1="2" x2="12" y2="6"/>
<line x1="12" y1="18" x2="12" y2="22"/>
<line x1="2" y1="12" x2="6" y2="12"/>
<line x1="18" y1="12" x2="22" y2="12"/>
</svg>
Use GPS
</button>
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
Save Location
</button>
</div>
</div>
<div class="settings-group">
<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 style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: var(--text-dim);">Latitude</span>
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Longitude</span>
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
</div>
</div>
<div class="settings-info">
<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.
</div>
</div>
<!-- Display Section -->
<div id="settings-display" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Visual Preferences</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Theme</span>
<span class="settings-label-desc">Color scheme preference</span>
</div>
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Animations</span>
<span class="settings-label-desc">Enable visual effects and animations</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Updates Section -->
<div id="settings-updates" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Update Status</div>
<div id="updateStatusContent" style="padding: 10px 0;">
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
Loading update status...
</div>
</div>
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
Check Now
</button>
</div>
<div class="settings-group">
<div class="settings-group-title">Update Settings</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Auto-Check for Updates</span>
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-info">
<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.
</div>
</div>
<!-- Tools Section -->
<div id="settings-tools" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Tool Dependencies</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Check which external tools are installed for each mode.
<span style="color: var(--accent-green);"></span> = Installed,
<span style="color: var(--accent-red);"></span> = Missing
</p>
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
Loading dependencies...
</div>
</div>
</div>
<div class="settings-group" style="margin-top: 15px;">
<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>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>
<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.
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>
<!-- About Section -->
<div id="settings-about" class="settings-section">
<div class="settings-group">
<div class="about-info">
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
<p>Version: <span class="about-version">{{ version }}</span></p>
<p>
A unified web interface for software-defined radio (SDR) tools,
supporting pager decoding, sensor monitoring, aircraft tracking,
WiFi/Bluetooth scanning, and more.
</p>
<p>
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
</p>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Support the Project</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
If you find iNTERCEPT useful, consider supporting its development.
</p>
<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;">
<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>
Buy Me a Coffee
</a>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()">
<div class="settings-content">
<div class="settings-header">
<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>
Settings
</h2>
<button class="settings-close" onclick="hideSettings()">&times;</button>
</div>
<div class="settings-tabs">
<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="display" onclick="switchSettingsTab('display')">Display</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="about" onclick="switchSettingsTab('about')">About</button>
</div>
<!-- Offline Section -->
<div id="settings-offline" class="settings-section active">
<div class="settings-group">
<div class="settings-group-title">Offline Mode</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Enable Offline Mode</span>
<span class="settings-label-desc">Use local assets instead of CDN</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Asset Sources</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">JavaScript/CSS Libraries</span>
<span class="settings-label-desc">Leaflet, Chart.js</span>
</div>
<select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)">
<option value="cdn">CDN (Online)</option>
<option value="local">Local</option>
</select>
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Web Fonts</span>
<span class="settings-label-desc">JetBrains Mono</span>
</div>
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
<option value="cdn">Google Fonts (Online)</option>
<option value="local">Local</option>
</select>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Map Tiles</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Tile Provider</span>
<span class="settings-label-desc">Map background imagery</span>
</div>
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_dark">CartoDB Dark</option>
<option value="cartodb_light">CartoDB Positron</option>
<option value="esri_world">ESRI World Imagery</option>
<option value="custom">Custom URL</option>
</select>
</div>
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
<div class="settings-label" style="width: 100%;">
<span class="settings-label-text">Custom Tile URL</span>
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
<input type="text" id="customTileUrl" class="settings-input"
placeholder="http://tile-server/{z}/{x}/{y}.png"
onchange="Settings.setCustomTileUrl(this.value)">
</div>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Local Asset Status</div>
<div class="asset-status" id="assetStatus">
<div class="asset-status-row">
<span class="asset-name">Leaflet JS/CSS</span>
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">Chart.js</span>
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">Inter Font</span>
<span class="asset-badge checking" id="statusInter">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">JetBrains Mono</span>
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
</div>
</div>
<button class="check-assets-btn" onclick="Settings.checkAssets()">
Check Assets
</button>
</div>
<div class="settings-info">
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
Local assets must be available in <code>/static/vendor/</code>.
</div>
</div>
<!-- Location Section -->
<div id="settings-location" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Observer Location</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Set your geographic coordinates for satellite pass predictions and ISS tracking.
</p>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Latitude</span>
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
</div>
<input type="number" id="observerLatInput" class="settings-input"
step="0.0001" min="-90" max="90" placeholder="51.5074"
style="width: 120px; text-align: right;">
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Longitude</span>
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
</div>
<input type="number" id="observerLonInput" class="settings-input"
step="0.0001" min="-180" max="180" placeholder="-0.1278"
style="width: 120px; text-align: right;">
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<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;">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="3"/>
<line x1="12" y1="2" x2="12" y2="6"/>
<line x1="12" y1="18" x2="12" y2="22"/>
<line x1="2" y1="12" x2="6" y2="12"/>
<line x1="18" y1="12" x2="22" y2="12"/>
</svg>
Use GPS
</button>
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
Save Location
</button>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Current Location</div>
<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;">
<span style="color: var(--text-dim);">Latitude</span>
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Longitude</span>
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
</div>
</div>
</div>
<div class="settings-info">
<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.
</div>
</div>
<!-- Display Section -->
<div id="settings-display" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Visual Preferences</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Theme</span>
<span class="settings-label-desc">Color scheme preference</span>
</div>
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Animations</span>
<span class="settings-label-desc">Enable visual effects and animations</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Updates Section -->
<div id="settings-updates" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Update Status</div>
<div id="updateStatusContent" style="padding: 10px 0;">
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
Loading update status...
</div>
</div>
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
Check Now
</button>
</div>
<div class="settings-group">
<div class="settings-group-title">Update Settings</div>
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Auto-Check for Updates</span>
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="settings-info">
<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.
</div>
</div>
<!-- Tools Section -->
<div id="settings-tools" class="settings-section">
<div class="settings-group">
<div class="settings-group-title">Tool Dependencies</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
Check which external tools are installed for each mode.
<span style="color: var(--accent-green);"></span> = Installed,
<span style="color: var(--accent-red);"></span> = Missing
</p>
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
Loading dependencies...
</div>
</div>
</div>
<div class="settings-group" style="margin-top: 15px;">
<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>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>
<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.
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>
<!-- About Section -->
<div id="settings-about" class="settings-section">
<div class="settings-group">
<div class="about-info">
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
<p>Version: <span class="about-version">{{ version }}</span></p>
<p>
A unified web interface for software-defined radio (SDR) tools,
supporting pager decoding, sensor monitoring, aircraft tracking,
WiFi/Bluetooth scanning, and more.
</p>
<p>
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
</p>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Support the Project</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
If you find iNTERCEPT useful, consider supporting its development.
</p>
<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;">
<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>
Buy Me a Coffee
</a>
</div>
</div>
</div>
</div>
File diff suppressed because it is too large Load Diff