mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 00:03:33 -07:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24332a4e23 | |||
| ebc5754684 | |||
| 340b300aa4 | |||
| bf7026cc9f | |||
| 1b04b52509 | |||
| fca334f472 | |||
| d81d644319 | |||
| 400cf1114f | |||
| fec38adc78 | |||
| 993a7d2626 | |||
| dbe09411ac | |||
| 0afc47fcdd | |||
| 4862b285a8 | |||
| 41dd1555d7 | |||
| 0cf3a25ac6 | |||
| 3674b6e2d6 | |||
| 4c9bcb00c3 | |||
| 2067d0bf84 | |||
| c0fa59d10e | |||
| 37add84d59 | |||
| c23019b8c0 | |||
| b4edd35f5f | |||
| 812f85b9a9 | |||
| 77888b7d88 | |||
| 4a38d7512d | |||
| 5d0df18dac | |||
| d18e38800e | |||
| 76e595aaec | |||
| dfb9897fa1 | |||
| 82ad784fcb | |||
| 4bd7077d64 | |||
| 3f6b9cc5ef | |||
| 0742647571 | |||
| 33090419df | |||
| 4042d0e5f1 | |||
| d3a0b41fba | |||
| 2fefea5618 | |||
| d75f7c794f | |||
| 503b91ea87 | |||
| 43db7c309d | |||
| 6e57927409 | |||
| a404f5ded9 | |||
| f6a6aab623 | |||
| 2cfbc0addc | |||
| 07d6ef984e | |||
| 50227ccae6 | |||
| 8f3c636c61 | |||
| 42761bbdbc | |||
| 0f2eba302c | |||
| 83dd58721f | |||
| d658d0b81e | |||
| e04113628a | |||
| b1e92326b6 | |||
| 9ac63bd75f |
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026-01-11_fae1348c",
|
||||
"downloaded": "2026-01-12T15:55:42.769654Z"
|
||||
"version": "2026-02-01_ba81b697",
|
||||
"downloaded": "2026-02-04T17:06:54.806043Z"
|
||||
}
|
||||
@@ -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
|
||||
@@ -645,19 +649,21 @@ def health_check() -> Response:
|
||||
|
||||
@app.route('/killall', methods=['POST'])
|
||||
def kill_all() -> Response:
|
||||
"""Kill all decoder and WiFi processes."""
|
||||
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
||||
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process
|
||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
||||
|
||||
# Import adsb and ais modules to reset their state
|
||||
from routes import adsb as adsb_module
|
||||
from routes import ais as ais_module
|
||||
from utils.bluetooth import reset_bluetooth_scanner
|
||||
|
||||
killed = []
|
||||
processes_to_kill = [
|
||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher'
|
||||
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
|
||||
'hcitool', 'bluetoothctl'
|
||||
]
|
||||
|
||||
for proc in processes_to_kill:
|
||||
@@ -701,6 +707,26 @@ def kill_all() -> Response:
|
||||
dsc_process = None
|
||||
dsc_rtl_process = None
|
||||
|
||||
# Reset Bluetooth state (legacy)
|
||||
with bt_lock:
|
||||
if bt_process:
|
||||
try:
|
||||
bt_process.terminate()
|
||||
bt_process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
bt_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
bt_process = None
|
||||
|
||||
# Reset Bluetooth v2 scanner
|
||||
try:
|
||||
reset_bluetooth_scanner()
|
||||
killed.append('bluetooth_scanner')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clear SDR device registry
|
||||
with sdr_device_registry_lock:
|
||||
sdr_device_registry.clear()
|
||||
|
||||
@@ -7,10 +7,29 @@ import os
|
||||
import sys
|
||||
|
||||
# Application version
|
||||
VERSION = "2.12.1"
|
||||
VERSION = "2.13.1"
|
||||
|
||||
# Changelog - latest release notes (shown on welcome screen)
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "2.13.1",
|
||||
"date": "February 2026",
|
||||
"highlights": [
|
||||
"Help modal system with keyboard shortcuts reference",
|
||||
"Main Dashboard button in navigation bar",
|
||||
"Settings modal accessible from all dashboards",
|
||||
"Dashboard CSS improvements and consistency fixes",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.13.0",
|
||||
"date": "February 2026",
|
||||
"highlights": [
|
||||
"WiFi client display in AP detail drawer",
|
||||
"Real-time client updates via SSE streaming",
|
||||
"Probed SSID badges for connected clients",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.12.1",
|
||||
"date": "February 2026",
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
# iNTERCEPT UI Guide
|
||||
|
||||
This guide documents the UI design system, components, and patterns used in iNTERCEPT.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Design Tokens](#design-tokens)
|
||||
2. [Base Templates](#base-templates)
|
||||
3. [Navigation](#navigation)
|
||||
4. [Components](#components)
|
||||
5. [Adding a New Module Page](#adding-a-new-module-page)
|
||||
6. [Adding a New Dashboard](#adding-a-new-dashboard)
|
||||
|
||||
---
|
||||
|
||||
## Design Tokens
|
||||
|
||||
All design tokens are defined in `static/css/core/variables.css`. Import this file first in any stylesheet.
|
||||
|
||||
### Colors
|
||||
|
||||
```css
|
||||
/* Backgrounds (layered depth) */
|
||||
--bg-primary: #0a0c10; /* Darkest - page background */
|
||||
--bg-secondary: #0f1218; /* Panels, sidebars */
|
||||
--bg-tertiary: #151a23; /* Cards, elevated elements */
|
||||
--bg-card: #121620; /* Card backgrounds */
|
||||
--bg-elevated: #1a202c; /* Hover states, modals */
|
||||
|
||||
/* Accent Colors */
|
||||
--accent-cyan: #4a9eff; /* Primary action color */
|
||||
--accent-green: #22c55e; /* Success, online status */
|
||||
--accent-red: #ef4444; /* Error, danger, stop */
|
||||
--accent-orange: #f59e0b; /* Warning */
|
||||
--accent-amber: #d4a853; /* Secondary highlight */
|
||||
|
||||
/* Text Hierarchy */
|
||||
--text-primary: #e8eaed; /* Main content */
|
||||
--text-secondary: #9ca3af; /* Secondary content */
|
||||
--text-dim: #4b5563; /* Disabled, placeholder */
|
||||
--text-muted: #374151; /* Barely visible */
|
||||
|
||||
/* Status Colors */
|
||||
--status-online: #22c55e;
|
||||
--status-warning: #f59e0b;
|
||||
--status-error: #ef4444;
|
||||
--status-offline: #6b7280;
|
||||
```
|
||||
|
||||
### Spacing Scale
|
||||
|
||||
```css
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
```css
|
||||
/* Font Families */
|
||||
--font-sans: 'Inter', -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* Font Sizes */
|
||||
--text-xs: 10px;
|
||||
--text-sm: 12px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 18px;
|
||||
--text-2xl: 20px;
|
||||
--text-3xl: 24px;
|
||||
--text-4xl: 30px;
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
|
||||
```css
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-full: 9999px;
|
||||
```
|
||||
|
||||
### Light Theme
|
||||
|
||||
The design system supports light/dark themes via `data-theme` attribute:
|
||||
|
||||
```html
|
||||
<html data-theme="dark"> <!-- or "light" -->
|
||||
```
|
||||
|
||||
Toggle with JavaScript:
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Base Templates
|
||||
|
||||
### `templates/layout/base.html`
|
||||
|
||||
The main base template for standard pages. Use for pages with sidebar + content layout.
|
||||
|
||||
```html
|
||||
{% extends 'layout/base.html' %}
|
||||
|
||||
{% block title %}My Page Title{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/my-page.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block navigation %}
|
||||
{% set active_mode = 'mymode' %}
|
||||
{% include 'partials/nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div class="app-sidebar">
|
||||
<!-- Sidebar content -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-container">
|
||||
<h1>Page Title</h1>
|
||||
<!-- Page content -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Page-specific JavaScript
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### `templates/layout/base_dashboard.html`
|
||||
|
||||
Extended base for full-screen dashboards (maps, visualizations).
|
||||
|
||||
```html
|
||||
{% extends 'layout/base_dashboard.html' %}
|
||||
|
||||
{% set active_mode = 'mydashboard' %}
|
||||
|
||||
{% block dashboard_title %}MY DASHBOARD{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_dashboard.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block stats_strip %}
|
||||
<div class="stats-strip">
|
||||
<!-- Stats bar content -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="dashboard-map-container">
|
||||
<!-- Main visualization -->
|
||||
</div>
|
||||
<div class="dashboard-sidebar">
|
||||
<!-- Sidebar panels -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
### Including Navigation
|
||||
|
||||
```html
|
||||
{% set active_mode = 'pager' %}
|
||||
{% include 'partials/nav.html' %}
|
||||
```
|
||||
|
||||
### Valid `active_mode` Values
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `pager` | Pager decoding |
|
||||
| `sensor` | 433MHz sensors |
|
||||
| `rtlamr` | Utility meters |
|
||||
| `adsb` | Aircraft tracking |
|
||||
| `ais` | Vessel tracking |
|
||||
| `aprs` | Amateur radio |
|
||||
| `wifi` | WiFi scanning |
|
||||
| `bluetooth` | Bluetooth scanning |
|
||||
| `tscm` | Counter-surveillance |
|
||||
| `satellite` | Satellite tracking |
|
||||
| `sstv` | ISS SSTV |
|
||||
| `listening` | Listening post |
|
||||
| `spystations` | Spy stations |
|
||||
| `meshtastic` | Mesh networking |
|
||||
|
||||
### Navigation Groups
|
||||
|
||||
The navigation is organized into groups:
|
||||
- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic
|
||||
- **Wireless**: WiFi, Bluetooth
|
||||
- **Security**: TSCM
|
||||
- **Space**: Satellite, ISS SSTV
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Card / Panel
|
||||
|
||||
```html
|
||||
{% call card(title='PANEL TITLE', indicator=true, indicator_active=false) %}
|
||||
<p>Panel content here</p>
|
||||
{% endcall %}
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```html
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>PANEL TITLE</span>
|
||||
<div class="panel-indicator active"></div>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<p>Content here</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
```html
|
||||
{% include 'components/empty_state.html' with context %}
|
||||
{# Or with variables: #}
|
||||
{% with title='No data yet', description='Start scanning to see results', action_text='Start Scan', action_onclick='startScan()' %}
|
||||
{% include 'components/empty_state.html' %}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
### Loading State
|
||||
|
||||
```html
|
||||
{# Inline spinner #}
|
||||
{% include 'components/loading.html' %}
|
||||
|
||||
{# With text #}
|
||||
{% with text='Loading data...', size='lg' %}
|
||||
{% include 'components/loading.html' %}
|
||||
{% endwith %}
|
||||
|
||||
{# Full overlay #}
|
||||
{% with overlay=true, text='Please wait...' %}
|
||||
{% include 'components/loading.html' %}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
### Status Badge
|
||||
|
||||
```html
|
||||
{% with status='online', text='Connected', id='connectionStatus' %}
|
||||
{% include 'components/status_badge.html' %}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
Status values: `online`, `offline`, `warning`, `error`, `inactive`
|
||||
|
||||
### Buttons
|
||||
|
||||
```html
|
||||
<!-- Primary action -->
|
||||
<button class="btn btn-primary">Start Tracking</button>
|
||||
|
||||
<!-- Secondary action -->
|
||||
<button class="btn btn-secondary">Cancel</button>
|
||||
|
||||
<!-- Danger action -->
|
||||
<button class="btn btn-danger">Stop</button>
|
||||
|
||||
<!-- Ghost/subtle -->
|
||||
<button class="btn btn-ghost">Settings</button>
|
||||
|
||||
<!-- Sizes -->
|
||||
<button class="btn btn-primary btn-sm">Small</button>
|
||||
<button class="btn btn-primary btn-lg">Large</button>
|
||||
|
||||
<!-- Icon button -->
|
||||
<button class="btn btn-icon btn-secondary">
|
||||
<span class="icon">...</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
### Badges
|
||||
|
||||
```html
|
||||
<span class="badge">Default</span>
|
||||
<span class="badge badge-primary">Primary</span>
|
||||
<span class="badge badge-success">Online</span>
|
||||
<span class="badge badge-warning">Warning</span>
|
||||
<span class="badge badge-danger">Error</span>
|
||||
```
|
||||
|
||||
### Form Groups
|
||||
|
||||
```html
|
||||
<div class="form-group">
|
||||
<label for="frequency">Frequency (MHz)</label>
|
||||
<input type="text" id="frequency" value="153.350">
|
||||
<span class="form-help">Enter frequency in MHz</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gain">Gain</label>
|
||||
<select id="gain">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="30">30 dB</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="form-check">
|
||||
<input type="checkbox" id="alerts">
|
||||
<span>Enable alerts</span>
|
||||
</label>
|
||||
```
|
||||
|
||||
### Stats Strip
|
||||
|
||||
Used in dashboards for horizontal statistics display:
|
||||
|
||||
```html
|
||||
<div class="stats-strip">
|
||||
<div class="stats-strip-inner">
|
||||
<div class="strip-stat">
|
||||
<span class="strip-value" id="count">0</span>
|
||||
<span class="strip-label">COUNT</span>
|
||||
</div>
|
||||
<div class="strip-divider"></div>
|
||||
<div class="strip-status">
|
||||
<div class="status-dot active" id="statusDot"></div>
|
||||
<span id="statusText">TRACKING</span>
|
||||
</div>
|
||||
<div class="strip-time" id="utcTime">--:--:-- UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Module Page
|
||||
|
||||
### 1. Create the Route
|
||||
|
||||
In `routes/mymodule.py`:
|
||||
|
||||
```python
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
mymodule_bp = Blueprint('mymodule', __name__, url_prefix='/mymodule')
|
||||
|
||||
@mymodule_bp.route('/dashboard')
|
||||
def dashboard():
|
||||
return render_template('mymodule_dashboard.html',
|
||||
offline_settings=get_offline_settings())
|
||||
```
|
||||
|
||||
### 2. Register the Blueprint
|
||||
|
||||
In `routes/__init__.py`:
|
||||
|
||||
```python
|
||||
from routes.mymodule import mymodule_bp
|
||||
app.register_blueprint(mymodule_bp)
|
||||
```
|
||||
|
||||
### 3. Create the Template
|
||||
|
||||
Option A: Simple page extending base.html
|
||||
```html
|
||||
{% extends 'layout/base.html' %}
|
||||
{% set active_mode = 'mymodule' %}
|
||||
|
||||
{% block title %}My Module{% endblock %}
|
||||
|
||||
{% block navigation %}
|
||||
{% include 'partials/nav.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Your content -->
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Option B: Full-screen dashboard
|
||||
```html
|
||||
{% extends 'layout/base_dashboard.html' %}
|
||||
{% set active_mode = 'mymodule' %}
|
||||
|
||||
{% block dashboard_title %}MY MODULE{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<!-- Your dashboard content -->
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### 4. Add to Navigation
|
||||
|
||||
In `templates/partials/nav.html`, add your module to the appropriate group:
|
||||
|
||||
```html
|
||||
<button class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
|
||||
onclick="switchMode('mymodule')">
|
||||
<span class="nav-icon icon"><!-- SVG icon --></span>
|
||||
<span class="nav-label">My Module</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
Or if it's a dashboard link:
|
||||
```html
|
||||
<a href="/mymodule/dashboard"
|
||||
class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
|
||||
style="text-decoration: none;">
|
||||
<span class="nav-icon icon"><!-- SVG icon --></span>
|
||||
<span class="nav-label">My Module</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
### 5. Create Stylesheet
|
||||
|
||||
In `static/css/mymodule.css`:
|
||||
|
||||
```css
|
||||
/**
|
||||
* My Module Styles
|
||||
*/
|
||||
@import url('./core/variables.css');
|
||||
|
||||
/* Your styles using design tokens */
|
||||
.mymodule-container {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Dashboard
|
||||
|
||||
For full-screen dashboards like ADSB, AIS, or Satellite:
|
||||
|
||||
### 1. Create the Template
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MY DASHBOARD // iNTERCEPT</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||
|
||||
<!-- Design tokens (required) -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
|
||||
<!-- Fonts -->
|
||||
{% if offline_settings.fonts_source == 'local' %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||
{% else %}
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
{% endif %}
|
||||
|
||||
<!-- External libraries if needed -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<!-- Dashboard styles -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/mydashboard.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Background effects -->
|
||||
<div class="radar-bg"></div>
|
||||
<div class="scanline"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<a href="/" style="color: inherit; text-decoration: none;">
|
||||
MY DASHBOARD
|
||||
<span>// iNTERCEPT</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
||||
<a href="/" class="back-link">Main Dashboard</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Unified Navigation -->
|
||||
{% set active_mode = 'mydashboard' %}
|
||||
{% include 'partials/nav.html' %}
|
||||
|
||||
<!-- Stats Strip -->
|
||||
<div class="stats-strip">
|
||||
<!-- Stats content -->
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard Content -->
|
||||
<main class="dashboard">
|
||||
<!-- Your dashboard layout -->
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Dashboard JavaScript
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 2. Create the Stylesheet
|
||||
|
||||
```css
|
||||
/**
|
||||
* My Dashboard Styles
|
||||
*/
|
||||
@import url('./core/variables.css');
|
||||
|
||||
:root {
|
||||
/* Dashboard-specific aliases */
|
||||
--bg-dark: var(--bg-primary);
|
||||
--bg-panel: var(--bg-secondary);
|
||||
--bg-card: var(--bg-tertiary);
|
||||
--grid-line: rgba(74, 158, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Your dashboard styles */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use design tokens for all colors, spacing, and typography
|
||||
- Include the nav partial on all pages for consistent navigation
|
||||
- Set `active_mode` before including the nav partial
|
||||
- Use semantic component classes (`btn`, `panel`, `badge`, etc.)
|
||||
- Support both light and dark themes
|
||||
- Test on mobile viewports
|
||||
|
||||
### DON'T
|
||||
|
||||
- Hardcode color values - use CSS variables
|
||||
- Create new color variations without adding to tokens
|
||||
- Duplicate navigation markup - use the partial
|
||||
- Skip the favicon and design tokens imports
|
||||
- Use inline styles for layout (use utility classes)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
templates/
|
||||
├── layout/
|
||||
│ ├── base.html # Standard page base
|
||||
│ └── base_dashboard.html # Dashboard page base
|
||||
├── partials/
|
||||
│ ├── nav.html # Unified navigation
|
||||
│ ├── page_header.html # Page title component
|
||||
│ └── settings-modal.html # Settings modal
|
||||
├── components/
|
||||
│ ├── card.html # Panel/card component
|
||||
│ ├── empty_state.html # Empty state placeholder
|
||||
│ ├── loading.html # Loading spinner
|
||||
│ ├── stats_strip.html # Stats bar component
|
||||
│ └── status_badge.html # Status indicator
|
||||
├── index.html # Main dashboard
|
||||
├── adsb_dashboard.html # Aircraft tracking
|
||||
├── ais_dashboard.html # Vessel tracking
|
||||
└── satellite_dashboard.html # Satellite tracking
|
||||
|
||||
static/css/
|
||||
├── core/
|
||||
│ ├── variables.css # Design tokens
|
||||
│ ├── base.css # Reset & typography
|
||||
│ ├── components.css # Component styles
|
||||
│ └── layout.css # Layout styles
|
||||
├── index.css # Main dashboard styles
|
||||
├── adsb_dashboard.css # Aircraft dashboard
|
||||
├── ais_dashboard.css # Vessel dashboard
|
||||
├── satellite_dashboard.css # Satellite dashboard
|
||||
└── responsive.css # Responsive breakpoints
|
||||
```
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "intercept"
|
||||
version = "2.12.1"
|
||||
version = "2.13.1"
|
||||
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
+36
-9
@@ -732,16 +732,43 @@ def start_adsb():
|
||||
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
|
||||
except Exception:
|
||||
pass
|
||||
if sdr_type == SDRType.RTL_SDR:
|
||||
error_msg = 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.'
|
||||
if stderr_output:
|
||||
error_msg += f' Error: {stderr_output[:200]}'
|
||||
return jsonify({'status': 'error', 'message': error_msg})
|
||||
|
||||
# Parse stderr to provide specific guidance
|
||||
error_type = 'START_FAILED'
|
||||
stderr_lower = stderr_output.lower()
|
||||
|
||||
if 'usb_claim_interface' in stderr_lower or 'libusb_error_busy' in stderr_lower or 'device or resource busy' in stderr_lower:
|
||||
error_msg = 'SDR device is busy. Another process may be using it.'
|
||||
suggestion = 'Try: 1) Stop other SDR applications, 2) Run "pkill -f rtl_" to kill stale processes, or 3) Remove and reinsert the SDR device.'
|
||||
error_type = 'DEVICE_BUSY'
|
||||
elif 'no supported devices' in stderr_lower or 'no rtl-sdr' in stderr_lower or 'failed to open' in stderr_lower:
|
||||
error_msg = 'RTL-SDR device not found.'
|
||||
suggestion = 'Ensure the device is connected. Try removing and reinserting the SDR.'
|
||||
error_type = 'DEVICE_NOT_FOUND'
|
||||
elif 'kernel driver is active' in stderr_lower or 'dvb' in stderr_lower:
|
||||
error_msg = 'Kernel DVB-T driver is blocking the device.'
|
||||
suggestion = 'Blacklist the DVB drivers: Go to Settings > Hardware > "Blacklist DVB Drivers" or run "sudo rmmod dvb_usb_rtl28xxu".'
|
||||
error_type = 'KERNEL_DRIVER'
|
||||
elif 'permission' in stderr_lower or 'access' in stderr_lower:
|
||||
error_msg = 'Permission denied accessing RTL-SDR device.'
|
||||
suggestion = 'Run Intercept with sudo, or add udev rules for RTL-SDR devices.'
|
||||
error_type = 'PERMISSION_DENIED'
|
||||
elif sdr_type == SDRType.RTL_SDR:
|
||||
error_msg = 'dump1090 failed to start.'
|
||||
suggestion = 'Try removing and reinserting the SDR device, or check if another application is using it.'
|
||||
else:
|
||||
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.'
|
||||
if stderr_output:
|
||||
error_msg += f' Error: {stderr_output[:200]}'
|
||||
return jsonify({'status': 'error', 'message': error_msg})
|
||||
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}.'
|
||||
suggestion = 'Ensure readsb is installed with SoapySDR support and the device is connected.'
|
||||
|
||||
full_msg = f'{error_msg} {suggestion}'
|
||||
if stderr_output and len(stderr_output) < 300:
|
||||
full_msg += f' (Details: {stderr_output})'
|
||||
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'error_type': error_type,
|
||||
'message': full_msg
|
||||
})
|
||||
|
||||
adsb_using_service = True
|
||||
adsb_active_device = device # Track which device is being used
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+42
-2
@@ -2,14 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, jsonify, request, Response
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.logging import get_logger
|
||||
from utils.updater import (
|
||||
check_for_updates,
|
||||
get_update_status,
|
||||
dismiss_update,
|
||||
get_update_status,
|
||||
perform_update,
|
||||
restart_application,
|
||||
)
|
||||
|
||||
logger = get_logger('intercept.routes.updater')
|
||||
@@ -137,3 +138,42 @@ def dismiss_notification() -> Response:
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@updater_bp.route('/restart', methods=['POST'])
|
||||
def restart_app() -> Response:
|
||||
"""
|
||||
Restart the application.
|
||||
|
||||
This endpoint triggers a graceful restart of the application:
|
||||
1. Stops all running decoder processes
|
||||
2. Cleans up global state
|
||||
3. Replaces the current process with a fresh instance
|
||||
|
||||
The response may not be received by the client since the process
|
||||
is replaced immediately. Clients should poll /health until the
|
||||
server responds again.
|
||||
|
||||
Returns:
|
||||
JSON with restart status (may not be delivered)
|
||||
"""
|
||||
import threading
|
||||
|
||||
logger.info("Restart requested via API")
|
||||
|
||||
# Send response before restarting
|
||||
# Use a short delay to allow the response to be sent
|
||||
def delayed_restart():
|
||||
import time
|
||||
time.sleep(0.5) # Allow response to be sent
|
||||
restart_application()
|
||||
|
||||
# Start restart in a background thread so we can return a response
|
||||
restart_thread = threading.Thread(target=delayed_restart, daemon=False)
|
||||
restart_thread.start()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Application is restarting. Please wait...',
|
||||
'action': 'restart'
|
||||
})
|
||||
|
||||
@@ -1092,4 +1092,7 @@ main() {
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
# Clear traps before exiting to prevent spurious errors during cleanup
|
||||
trap - ERR EXIT
|
||||
exit 0
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-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;
|
||||
@@ -71,7 +73,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Header - Mobile first */
|
||||
/* Header */
|
||||
.header {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@@ -81,20 +83,19 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.header {
|
||||
padding: 12px 20px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
@@ -126,14 +127,52 @@ body {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.agent-selector-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-select-sm {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-status-dot.offline {
|
||||
background: var(--accent-red);
|
||||
box-shadow: 0 0 6px var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-selector-compact .show-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -172,15 +211,15 @@ body {
|
||||
}
|
||||
|
||||
/* Main dashboard grid - Mobile first */
|
||||
/* Header ~55px + Stats strip ~55px = ~110px, using 115px for safety */
|
||||
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
height: calc(100dvh - 115px);
|
||||
height: calc(100vh - 115px); /* Fallback */
|
||||
height: calc(100dvh - 160px);
|
||||
height: calc(100vh - 160px); /* Fallback */
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@@ -216,7 +255,7 @@ body {
|
||||
@media (min-width: 1024px) {
|
||||
.acars-sidebar {
|
||||
display: flex;
|
||||
max-height: calc(100dvh - 115px);
|
||||
max-height: calc(100dvh - 160px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +663,7 @@ body {
|
||||
}
|
||||
|
||||
.telemetry-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
@@ -680,7 +719,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 +739,7 @@ body {
|
||||
}
|
||||
|
||||
.aircraft-detail-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-cyan);
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -790,7 +829,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 +840,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 +918,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 +950,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 +1062,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 +1096,7 @@ body {
|
||||
}
|
||||
|
||||
.airband-status {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
padding: 0 8px;
|
||||
color: var(--text-muted);
|
||||
@@ -1217,7 +1256,7 @@ body {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: auto !important;
|
||||
min-height: calc(100dvh - 115px);
|
||||
min-height: calc(100dvh - 160px);
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -1286,12 +1325,6 @@ body {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
/* Status bar - compact on mobile */
|
||||
.status-bar {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Strip time smaller on mobile */
|
||||
.strip-time {
|
||||
font-size: 10px;
|
||||
@@ -1407,7 +1440,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 +1578,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 +1817,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 +1971,7 @@ body {
|
||||
}
|
||||
|
||||
.squawk-code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
color: var(--accent-cyan);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-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,
|
||||
|
||||
+331
-343
@@ -1,343 +1,331 @@
|
||||
/*
|
||||
* 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
|
||||
* Inherits CSS variables from core/variables.css
|
||||
*/
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-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;
|
||||
@@ -132,10 +134,49 @@ body {
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.agent-selector-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-select-sm {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.agent-selector-compact .agent-status-dot.offline {
|
||||
background: var(--accent-red);
|
||||
box-shadow: 0 0 6px var(--accent-red);
|
||||
}
|
||||
|
||||
.agent-selector-compact .show-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
@@ -183,7 +224,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 +328,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;
|
||||
@@ -320,14 +361,15 @@ body {
|
||||
}
|
||||
|
||||
/* Main dashboard grid - Mobile first */
|
||||
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
height: calc(100dvh - 95px);
|
||||
height: calc(100vh - 95px);
|
||||
height: calc(100dvh - 160px);
|
||||
height: calc(100vh - 160px);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@@ -367,7 +409,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 */
|
||||
@@ -438,7 +480,7 @@ body {
|
||||
padding: 10px 15px;
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
border-bottom: 1px solid rgba(74, 158, 255, 0.1);
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
@@ -510,7 +552,7 @@ body {
|
||||
}
|
||||
|
||||
.vessel-name {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-cyan);
|
||||
@@ -518,7 +560,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 +590,7 @@ body {
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
@@ -604,20 +646,20 @@ body {
|
||||
}
|
||||
|
||||
.vessel-item-name {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.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 +729,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 +740,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 +759,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;
|
||||
@@ -834,7 +876,7 @@ body {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: auto !important;
|
||||
min-height: calc(100dvh - 95px);
|
||||
min-height: calc(100dvh - 160px);
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -1004,7 +1046,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 +1121,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 +1138,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 +1162,7 @@ body {
|
||||
}
|
||||
|
||||
.dsc-message-pos {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -1148,7 +1190,7 @@ body {
|
||||
}
|
||||
|
||||
.dsc-distress-alert .dsc-alert-header {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-red);
|
||||
@@ -1157,7 +1199,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 +1219,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 +1230,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
@@ -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); }
|
||||
@@ -14,10 +14,18 @@
|
||||
|
||||
.radar-device {
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radar-device:hover {
|
||||
transform: scale(1.3);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Invisible larger hit area to prevent hover flicker */
|
||||
.radar-device-hitarea {
|
||||
fill: transparent;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.radar-dot-pulse circle:first-child {
|
||||
|
||||
+1934
-1924
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+626
-626
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* INTERCEPT Design Tokens
|
||||
* Single source of truth for colors, spacing, typography, and effects
|
||||
* Import this file FIRST in any stylesheet that needs design tokens
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ============================================
|
||||
COLOR PALETTE - Dark Theme (Default)
|
||||
============================================ */
|
||||
|
||||
/* Backgrounds - layered depth system */
|
||||
--bg-primary: #0a0c10;
|
||||
--bg-secondary: #0f1218;
|
||||
--bg-tertiary: #151a23;
|
||||
--bg-card: #121620;
|
||||
--bg-elevated: #1a202c;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
/* Background aliases for components */
|
||||
--bg-dark: var(--bg-primary);
|
||||
--bg-panel: var(--bg-secondary);
|
||||
|
||||
/* Accent colors */
|
||||
--accent-cyan: #4a9eff;
|
||||
--accent-cyan-dim: rgba(74, 158, 255, 0.15);
|
||||
--accent-cyan-hover: #6bb3ff;
|
||||
--accent-green: #22c55e;
|
||||
--accent-green-dim: rgba(34, 197, 94, 0.15);
|
||||
--accent-red: #ef4444;
|
||||
--accent-red-dim: rgba(239, 68, 68, 0.15);
|
||||
--accent-orange: #f59e0b;
|
||||
--accent-orange-dim: rgba(245, 158, 11, 0.15);
|
||||
--accent-amber: #d4a853;
|
||||
--accent-amber-dim: rgba(212, 168, 83, 0.15);
|
||||
--accent-yellow: #eab308;
|
||||
--accent-purple: #a855f7;
|
||||
|
||||
/* Text hierarchy */
|
||||
--text-primary: #e8eaed;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-dim: #4b5563;
|
||||
--text-muted: #374151;
|
||||
--text-inverse: #0a0c10;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #1f2937;
|
||||
--border-light: #374151;
|
||||
--border-glow: rgba(74, 158, 255, 0.2);
|
||||
--border-focus: var(--accent-cyan);
|
||||
|
||||
/* Status colors */
|
||||
--status-online: #22c55e;
|
||||
--status-warning: #f59e0b;
|
||||
--status-error: #ef4444;
|
||||
--status-offline: #6b7280;
|
||||
--status-info: #3b82f6;
|
||||
|
||||
/* ============================================
|
||||
SPACING SCALE
|
||||
============================================ */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
--font-sans: 'Space Mono', ui-monospace, 'SF Mono', monospace;
|
||||
--font-mono: 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', monospace;
|
||||
|
||||
/* Font sizes */
|
||||
--text-xs: 10px;
|
||||
--text-sm: 12px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 18px;
|
||||
--text-2xl: 20px;
|
||||
--text-3xl: 24px;
|
||||
--text-4xl: 30px;
|
||||
|
||||
/* Font weights */
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Line heights */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.75;
|
||||
|
||||
/* ============================================
|
||||
BORDERS & RADIUS
|
||||
============================================ */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* ============================================
|
||||
SHADOWS
|
||||
============================================ */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glow: 0 0 20px rgba(74, 158, 255, 0.15);
|
||||
|
||||
/* ============================================
|
||||
TRANSITIONS
|
||||
============================================ */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
|
||||
/* ============================================
|
||||
Z-INDEX SCALE
|
||||
============================================ */
|
||||
--z-base: 0;
|
||||
--z-dropdown: 100;
|
||||
--z-sticky: 200;
|
||||
--z-fixed: 300;
|
||||
--z-modal-backdrop: 400;
|
||||
--z-modal: 500;
|
||||
--z-toast: 600;
|
||||
--z-tooltip: 700;
|
||||
|
||||
/* ============================================
|
||||
LAYOUT
|
||||
============================================ */
|
||||
--header-height: 60px;
|
||||
--nav-height: 44px;
|
||||
--sidebar-width: 280px;
|
||||
--stats-strip-height: 36px;
|
||||
--content-max-width: 1400px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LIGHT THEME OVERRIDES
|
||||
============================================ */
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f8fafc;
|
||||
--bg-secondary: #f1f5f9;
|
||||
--bg-tertiary: #e2e8f0;
|
||||
--bg-card: #ffffff;
|
||||
--bg-elevated: #f8fafc;
|
||||
--bg-overlay: rgba(255, 255, 255, 0.9);
|
||||
|
||||
/* Background aliases for components */
|
||||
--bg-dark: var(--bg-primary);
|
||||
--bg-panel: var(--bg-secondary);
|
||||
|
||||
--accent-cyan: #2563eb;
|
||||
--accent-cyan-dim: rgba(37, 99, 235, 0.1);
|
||||
--accent-cyan-hover: #1d4ed8;
|
||||
--accent-green: #16a34a;
|
||||
--accent-green-dim: rgba(22, 163, 74, 0.1);
|
||||
--accent-red: #dc2626;
|
||||
--accent-red-dim: rgba(220, 38, 38, 0.1);
|
||||
--accent-orange: #d97706;
|
||||
--accent-orange-dim: rgba(217, 119, 6, 0.1);
|
||||
--accent-amber: #b45309;
|
||||
--accent-amber-dim: rgba(180, 83, 9, 0.1);
|
||||
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-dim: #94a3b8;
|
||||
--text-muted: #cbd5e1;
|
||||
--text-inverse: #f8fafc;
|
||||
|
||||
--border-color: #e2e8f0;
|
||||
--border-light: #cbd5e1;
|
||||
--border-glow: rgba(37, 99, 235, 0.15);
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
|
||||
--shadow-glow: 0 0 20px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
REDUCED MOTION
|
||||
============================================ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--transition-fast: 0ms;
|
||||
--transition-base: 0ms;
|
||||
--transition-slow: 0ms;
|
||||
}
|
||||
}
|
||||
+18
-67
@@ -1,67 +1,18 @@
|
||||
/* 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 */
|
||||
|
||||
/* Space Mono - Console font */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/SpaceMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/SpaceMono-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
/* ============================================
|
||||
Global Navigation Styles
|
||||
Shared across all pages using nav.html
|
||||
============================================ */
|
||||
|
||||
/* Icon base (kept lightweight for nav usage) */
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon--sm {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Mode Navigation Bar */
|
||||
.mode-nav {
|
||||
display: none;
|
||||
background: linear-gradient(180deg, rgba(17, 22, 32, 0.92), rgba(15, 20, 28, 0.88));
|
||||
border-bottom: 1px solid var(--border-color, #202833);
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.mode-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-nav-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-secondary, #b7c1cf);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.mode-nav-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border-color, #202833);
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mode-nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #b7c1cf);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mode-nav-btn .nav-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mode-nav-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.8);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
border-color: var(--border-color, #202833);
|
||||
}
|
||||
|
||||
.mode-nav-btn.active {
|
||||
background: rgba(27, 36, 51, 0.9);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
border-color: var(--accent-cyan, #4d7dbf);
|
||||
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
.mode-nav-btn.active .nav-icon {
|
||||
color: var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
.mode-nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(24, 31, 44, 0.85);
|
||||
border: 1px solid var(--border-light, #2b3645);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-action-btn .nav-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.nav-action-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.95);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
box-shadow: 0 8px 16px rgba(5, 9, 15, 0.35);
|
||||
border-color: var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
/* Dropdown Navigation */
|
||||
.mode-nav-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #b7c1cf);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-btn .nav-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-btn .dropdown-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-btn .dropdown-arrow svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.8);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
border-color: var(--border-color, #202833);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
|
||||
background: rgba(27, 36, 51, 0.9);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
border-color: var(--border-color, #202833);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown.open .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
|
||||
background: rgba(27, 36, 51, 0.9);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
border-color: var(--accent-cyan, #4d7dbf);
|
||||
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
|
||||
color: var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 180px;
|
||||
background: rgba(16, 22, 32, 0.98);
|
||||
border: 1px solid var(--border-color, #202833);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 16px 36px rgba(5, 9, 15, 0.55);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.15s ease;
|
||||
z-index: 1000;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-menu .mode-nav-btn {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-menu .mode-nav-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.85);
|
||||
}
|
||||
|
||||
.mode-nav-dropdown-menu .mode-nav-btn.active {
|
||||
background: rgba(27, 36, 51, 0.95);
|
||||
color: var(--text-primary, #e7ebf2);
|
||||
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||
}
|
||||
|
||||
/* Nav Bar Utilities */
|
||||
.nav-utilities {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.nav-utilities {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-clock .utc-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim, #8a97a8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nav-clock .utc-time {
|
||||
color: var(--accent-cyan, #4d7dbf);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border-color, #202833);
|
||||
}
|
||||
|
||||
.nav-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-tool-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
border-radius: 6px;
|
||||
background: rgba(20, 33, 53, 0.6);
|
||||
border: 1px solid rgba(77, 125, 191, 0.12);
|
||||
color: var(--text-secondary, #b7c1cf);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-tool-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.9);
|
||||
border-color: var(--accent-cyan, #4d7dbf);
|
||||
color: var(--accent-cyan, #4d7dbf);
|
||||
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
|
||||
}
|
||||
|
||||
/* Position relative needed for absolute positioned icon children */
|
||||
.nav-tool-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-nav-btn:focus-visible,
|
||||
.mode-nav-dropdown-btn:focus-visible,
|
||||
.nav-action-btn:focus-visible,
|
||||
.nav-tool-btn:focus-visible {
|
||||
outline: 2px solid var(--accent-cyan, #4d7dbf);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Nav tool button SVG sizing and styling */
|
||||
.nav-tool-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* Theme toggle icon states */
|
||||
.nav-tool-btn .icon-sun,
|
||||
.nav-tool-btn .icon-moon {
|
||||
position: absolute;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon-sun {
|
||||
opacity: 0;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon-moon {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .nav-tool-btn .icon-sun {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .nav-tool-btn .icon-moon {
|
||||
opacity: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Effects/animations toggle icon states */
|
||||
.nav-tool-btn .icon-effects-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-animations="off"] .nav-tool-btn .icon-effects-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-animations="off"] .nav-tool-btn .icon-effects-off {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Main Dashboard Button in Nav */
|
||||
a.nav-dashboard-btn,
|
||||
a.nav-dashboard-btn:link,
|
||||
a.nav-dashboard-btn:visited {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(20, 33, 53, 0.6) !important;
|
||||
border: 1px solid rgba(77, 125, 191, 0.12) !important;
|
||||
color: #b7c1cf !important;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
a.nav-dashboard-btn:hover {
|
||||
background: rgba(27, 36, 51, 0.9) !important;
|
||||
border-color: #4d7dbf !important;
|
||||
color: #4d7dbf !important;
|
||||
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
|
||||
}
|
||||
|
||||
.nav-dashboard-btn .icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.nav-dashboard-btn .icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
.nav-dashboard-btn .nav-label {
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Help Modal Styles
|
||||
* Shared across all pages that include the help modal partial
|
||||
*/
|
||||
|
||||
.help-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 10000;
|
||||
overflow-y: auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.help-modal.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: var(--bg-card, var(--bg-secondary, #0f1218));
|
||||
border: 1px solid var(--border-color, #1f2937);
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-content h2 {
|
||||
color: var(--accent-cyan, #4a9eff);
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.help-content h3 {
|
||||
color: var(--text-primary, #e8eaed);
|
||||
margin: 25px 0 15px 0;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid var(--border-color, #1f2937);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.help-close {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-dim, #4b5563);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.help-close:hover {
|
||||
color: var(--accent-red, #ef4444);
|
||||
}
|
||||
|
||||
.help-modal .icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.help-modal .icon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: var(--bg-primary, #0a0c10);
|
||||
border: 1px solid var(--border-color, #1f2937);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.help-modal .icon-item .icon {
|
||||
font-size: 18px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-modal .icon-item .desc {
|
||||
color: var(--text-secondary, #9ca3af);
|
||||
}
|
||||
|
||||
.help-modal .tip-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.help-modal .tip-list li {
|
||||
padding: 8px 0;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
color: var(--text-secondary, #9ca3af);
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid var(--border-color, #1f2937);
|
||||
}
|
||||
|
||||
.help-modal .tip-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.help-modal .tip-list li::before {
|
||||
content: '\203A';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent-cyan, #4a9eff);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.help-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color, #1f2937);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.help-tab {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: var(--bg-primary, #0a0c10);
|
||||
border: none;
|
||||
color: var(--text-secondary, #9ca3af);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-tab:not(:last-child) {
|
||||
border-right: 1px solid var(--border-color, #1f2937);
|
||||
}
|
||||
|
||||
.help-tab:hover {
|
||||
background: var(--bg-tertiary, #151a23);
|
||||
color: var(--text-primary, #e8eaed);
|
||||
}
|
||||
|
||||
.help-tab.active {
|
||||
background: var(--bg-tertiary, #151a23);
|
||||
color: var(--accent-cyan, #4a9eff);
|
||||
}
|
||||
|
||||
.help-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--accent-cyan, #4a9eff);
|
||||
}
|
||||
|
||||
.help-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.help-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Ensure code tags are styled */
|
||||
.help-modal code {
|
||||
background: var(--bg-tertiary, #151a23);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
font-size: 11px;
|
||||
color: var(--accent-cyan, #4a9eff);
|
||||
}
|
||||
+197
-70
@@ -1,5 +1,3 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -7,6 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
/* Tactical dark palette */
|
||||
--bg-primary: #0a0c10;
|
||||
--bg-secondary: #0f1218;
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
@@ -259,7 +259,7 @@ body {
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
@@ -269,7 +269,7 @@ body {
|
||||
}
|
||||
|
||||
.welcome-tagline {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: var(--accent-cyan);
|
||||
letter-spacing: 0.15em;
|
||||
@@ -278,7 +278,7 @@ body {
|
||||
|
||||
.welcome-version {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.65rem;
|
||||
color: var(--bg-primary);
|
||||
background: var(--accent-cyan);
|
||||
@@ -297,7 +297,7 @@ body {
|
||||
}
|
||||
|
||||
.welcome-content h2 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
@@ -333,14 +333,14 @@ body {
|
||||
}
|
||||
|
||||
.changelog-version {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.changelog-date {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
@@ -352,7 +352,7 @@ body {
|
||||
}
|
||||
|
||||
.changelog-list li {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
@@ -364,7 +364,7 @@ body {
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
color: var(--accent-green);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Mode Selection Grid */
|
||||
@@ -435,7 +435,7 @@ body {
|
||||
}
|
||||
|
||||
.mode-card .mode-name {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
@@ -444,7 +444,7 @@ body {
|
||||
}
|
||||
|
||||
.mode-card .mode-desc {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
@@ -463,7 +463,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
@@ -517,7 +517,7 @@ body {
|
||||
}
|
||||
|
||||
.welcome-footer p {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.1em;
|
||||
@@ -731,7 +731,7 @@ header h1 {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -778,7 +778,7 @@ header h1 {
|
||||
border: 1px solid var(--accent-cyan);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-cyan);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
@@ -814,7 +814,7 @@ header h1 {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -922,7 +922,7 @@ header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
@@ -1030,7 +1030,7 @@ header h1 {
|
||||
.version-badge {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 500;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
@@ -1089,7 +1089,7 @@ header h1 .tagline {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -1578,7 +1578,7 @@ header h1 .tagline {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
@@ -1590,6 +1590,11 @@ header h1 .tagline {
|
||||
box-shadow: 0 0 0 2px var(--accent-cyan-dim);
|
||||
}
|
||||
|
||||
/* Ensure device select is wide enough for device name + serial */
|
||||
#deviceSelect {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1632,7 +1637,7 @@ header h1 .tagline {
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -1652,7 +1657,7 @@ header h1 .tagline {
|
||||
background: var(--accent-green);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -1689,7 +1694,7 @@ header h1 .tagline {
|
||||
background: var(--accent-red);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -1752,7 +1757,7 @@ header h1 .tagline {
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.stats>div {
|
||||
@@ -1778,7 +1783,7 @@ header h1 .tagline {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
background: var(--bg-primary);
|
||||
min-height: 0; /* Allow shrinking in flex context */
|
||||
@@ -1850,7 +1855,7 @@ header h1 .tagline {
|
||||
|
||||
.message .address {
|
||||
color: var(--accent-green);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -1863,7 +1868,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.message .content.numeric {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 15px;
|
||||
letter-spacing: 2px;
|
||||
color: var(--accent-cyan);
|
||||
@@ -2084,7 +2089,7 @@ header h1 .tagline {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
@@ -2352,7 +2357,7 @@ header h1 .tagline {
|
||||
/* Dark theme for Leaflet */
|
||||
.leaflet-container {
|
||||
background: #0a0a0a;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Using actual dark tiles now - no filter needed */
|
||||
@@ -2389,7 +2394,7 @@ header h1 .tagline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
z-index: 1000;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--accent-cyan);
|
||||
text-shadow: 0 0 5px var(--accent-cyan);
|
||||
@@ -2406,7 +2411,7 @@ header h1 .tagline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
z-index: 1000;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--accent-cyan);
|
||||
text-shadow: 0 0 5px var(--accent-cyan);
|
||||
@@ -2427,7 +2432,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.aircraft-popup {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -2471,7 +2476,7 @@ header h1 .tagline {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
border: 1px solid var(--accent-cyan) !important;
|
||||
color: var(--accent-cyan) !important;
|
||||
font-family: 'JetBrains Mono', monospace !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: 10px !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 2px !important;
|
||||
@@ -2499,7 +2504,7 @@ header h1 .tagline {
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.2s ease;
|
||||
@@ -2715,7 +2720,7 @@ header h1 .tagline {
|
||||
color: var(--accent-cyan);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
text-shadow: 0 0 15px var(--accent-cyan-dim);
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -3109,7 +3114,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.sensor-card .data-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
@@ -3159,7 +3164,7 @@ header h1 .tagline {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.recon-stats span {
|
||||
@@ -3209,14 +3214,14 @@ header h1 .tagline {
|
||||
|
||||
.device-id {
|
||||
color: var(--text-dim);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.device-meta {
|
||||
text-align: right;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.device-meta.encrypted {
|
||||
@@ -3292,7 +3297,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.hex-dump {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-primary);
|
||||
@@ -3383,7 +3388,7 @@ header h1 .tagline {
|
||||
/* WiFi Main Content - 3 columns */
|
||||
.wifi-main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(240px, 280px) minmax(240px, 280px);
|
||||
grid-template-columns: minmax(300px, 1fr) minmax(240px, 280px) minmax(240px, 280px);
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -3398,6 +3403,7 @@ header h1 .tagline {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
min-width: 0; /* Prevent content from forcing panel wider */
|
||||
}
|
||||
|
||||
.wifi-networks-header {
|
||||
@@ -3565,6 +3571,8 @@ header h1 .tagline {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
min-width: 0; /* Prevent content from forcing panel wider */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wifi-radar-panel h5 {
|
||||
@@ -3800,10 +3808,90 @@ header h1 .tagline {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wifi-client-count-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
background: var(--accent-cyan);
|
||||
color: var(--bg-primary);
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wifi-client-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wifi-client-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wifi-client-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wifi-client-mac {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.wifi-client-vendor {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.wifi-client-probes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.wifi-client-probe-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.wifi-client-signal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wifi-client-rssi {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.wifi-client-lastseen {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* WiFi Responsive */
|
||||
@media (max-width: 1400px) {
|
||||
.wifi-main-content {
|
||||
grid-template-columns: 1fr 240px 240px;
|
||||
grid-template-columns: minmax(280px, 1fr) 240px 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3961,7 +4049,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.bt-detail-address {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: #00d4ff;
|
||||
}
|
||||
@@ -3975,7 +4063,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.bt-detail-rssi-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -4070,7 +4158,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.bt-detail-services-list {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
@@ -4104,10 +4192,37 @@ header h1 .tagline {
|
||||
|
||||
.bt-device-list {
|
||||
border-left-color: var(--accent-purple) !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
max-height: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bt-device-list .wifi-device-list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.bt-device-list .wifi-device-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-device-list .wifi-device-list-header h5 {
|
||||
color: var(--accent-purple);
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Bluetooth Device Filters */
|
||||
@@ -4117,6 +4232,7 @@ header h1 .tagline {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-filter-btn {
|
||||
@@ -4289,7 +4405,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.bt-rssi-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
min-width: 28px;
|
||||
@@ -4648,7 +4764,7 @@ header h1 .tagline {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.security-legend-item {
|
||||
@@ -4695,7 +4811,7 @@ header h1 .tagline {
|
||||
}
|
||||
|
||||
.signal-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 28px;
|
||||
color: var(--accent-cyan);
|
||||
text-shadow: 0 0 10px var(--accent-cyan-dim);
|
||||
@@ -4848,7 +4964,7 @@ body::before {
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 12px 40px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
@@ -5211,7 +5327,7 @@ body::before {
|
||||
|
||||
.meter-value {
|
||||
font-size: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
@@ -5368,7 +5484,7 @@ body::before {
|
||||
}
|
||||
|
||||
.freq-digits {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-cyan);
|
||||
@@ -5389,7 +5505,7 @@ body::before {
|
||||
}
|
||||
|
||||
.freq-unit {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 20px;
|
||||
color: var(--text-secondary);
|
||||
margin-left: 8px;
|
||||
@@ -5533,7 +5649,7 @@ body::before {
|
||||
}
|
||||
|
||||
.knob-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
@@ -5658,7 +5774,7 @@ body::before {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
@@ -5720,13 +5836,13 @@ body::before {
|
||||
}
|
||||
|
||||
.signal-arc-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
fill: var(--text-muted);
|
||||
}
|
||||
|
||||
.signal-arc-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
fill: var(--accent-cyan);
|
||||
@@ -5758,7 +5874,7 @@ body::before {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-cyan);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
@@ -5894,7 +6010,7 @@ body::before {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 15px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -5983,7 +6099,7 @@ body::before {
|
||||
}
|
||||
|
||||
.module-header {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
@@ -6008,7 +6124,7 @@ body::before {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
@@ -6047,16 +6163,27 @@ body::before {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.radio-action-btn.scan {
|
||||
.radio-action-btn.scan,
|
||||
.radio-action-btn.listen {
|
||||
background: var(--accent-green);
|
||||
border-color: var(--accent-green);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.radio-action-btn.scan:hover:not(:disabled) {
|
||||
.radio-action-btn.scan:hover:not(:disabled),
|
||||
.radio-action-btn.listen:hover:not(:disabled) {
|
||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
||||
}
|
||||
|
||||
.radio-action-btn.listen.active {
|
||||
background: var(--accent-red);
|
||||
border-color: var(--accent-red);
|
||||
}
|
||||
|
||||
.radio-action-btn.listen.active:hover:not(:disabled) {
|
||||
box-shadow: 0 0 20px var(--accent-red-dim);
|
||||
}
|
||||
|
||||
/* Statistics Box */
|
||||
.stat-box {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
@@ -6066,7 +6193,7 @@ body::before {
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -6114,7 +6241,7 @@ body::before {
|
||||
.tune-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
@@ -6144,13 +6271,13 @@ body::before {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Listening Mode Selector Buttons */
|
||||
.radio-mode-btn {
|
||||
padding: 12px 24px;
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -6191,7 +6318,7 @@ body::before {
|
||||
/* Frequency Preset Buttons */
|
||||
.preset-freq-btn {
|
||||
padding: 8px 14px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -6255,4 +6382,4 @@ body::before {
|
||||
[data-animations="off"] .logo-dot,
|
||||
[data-animations="off"] .welcome-logo {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
+1610
-1610
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+1463
-1463
File diff suppressed because it is too large
Load Diff
+660
-660
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-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;
|
||||
}
|
||||
|
||||
@@ -162,10 +164,45 @@ body {
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.location-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.location-selector .location-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.location-selector .location-select {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.location-selector .location-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 6px var(--accent-green);
|
||||
}
|
||||
|
||||
.location-selector .location-status-dot.offline {
|
||||
background: var(--accent-red);
|
||||
box-shadow: 0 0 6px var(--accent-red);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
@@ -211,6 +248,7 @@ body {
|
||||
}
|
||||
|
||||
/* Main dashboard grid */
|
||||
/* Header ~52px + Nav 44px = ~96px, using 100px for safety */
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@@ -218,7 +256,7 @@ body {
|
||||
grid-template-columns: 1fr 1fr 340px;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 0;
|
||||
height: calc(100vh - 60px);
|
||||
height: calc(100vh - 100px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@@ -457,7 +495,7 @@ body {
|
||||
}
|
||||
|
||||
.telemetry-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
@@ -543,7 +581,7 @@ body {
|
||||
}
|
||||
|
||||
.pass-time {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Bottom controls bar */
|
||||
@@ -579,7 +617,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;
|
||||
}
|
||||
|
||||
@@ -696,7 +734,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: calc(100vh - 60px);
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.polar-container,
|
||||
@@ -748,4 +786,4 @@ body.embedded .panel {
|
||||
|
||||
body.embedded .controls-bar {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
+444
-427
@@ -1,427 +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;
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,9 +207,14 @@ const ProximityRadar = (function() {
|
||||
const pulseClass = isNew ? 'radar-dot-pulse' : '';
|
||||
const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey;
|
||||
|
||||
// Hit area size (prevents hover flicker when scaling)
|
||||
const hitAreaSize = Math.max(dotSize * 2, 15);
|
||||
|
||||
return `
|
||||
<g class="radar-device ${pulseClass}${isSelected ? ' selected' : ''}" data-device-key="${escapeAttr(device.device_key)}"
|
||||
transform="translate(${x}, ${y})" style="cursor: pointer;">
|
||||
<!-- Invisible hit area to prevent hover flicker -->
|
||||
<circle class="radar-device-hitarea" r="${hitAreaSize}" fill="transparent" />
|
||||
${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
|
||||
<animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/>
|
||||
<animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/>
|
||||
|
||||
+494
-494
@@ -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') || 'dark';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('intercept-theme', newTheme);
|
||||
|
||||
// Update button text
|
||||
const btn = document.getElementById('themeToggle');
|
||||
if (btn) {
|
||||
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
||||
}
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('intercept-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: "Space 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: "Space 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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
+878
-843
File diff suppressed because it is too large
Load Diff
+2951
-2599
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ const Meshtastic = (function() {
|
||||
/**
|
||||
* Initialize the Leaflet map
|
||||
*/
|
||||
function initMap() {
|
||||
async function initMap() {
|
||||
if (meshMap) return;
|
||||
|
||||
const mapContainer = document.getElementById('meshMap');
|
||||
@@ -111,7 +111,9 @@ const Meshtastic = (function() {
|
||||
window.meshMap = meshMap;
|
||||
|
||||
// Use settings manager for tile layer (allows runtime changes)
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
if (typeof Settings !== 'undefined') {
|
||||
// Wait for settings to load from server before applying tiles
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(meshMap);
|
||||
Settings.registerMap(meshMap);
|
||||
} else {
|
||||
|
||||
@@ -84,7 +84,7 @@ const SpyStations = (function() {
|
||||
modeContainer.innerHTML = modes.map(m => `
|
||||
<label class="inline-checkbox">
|
||||
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
|
||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 10px;">${m}</span>
|
||||
<span style="font-family: 'Space Mono', monospace; font-size: 10px;">${m}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
+39
-35
@@ -37,20 +37,20 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Load location into input fields
|
||||
*/
|
||||
function loadLocationInputs() {
|
||||
const latInput = document.getElementById('sstvObsLat');
|
||||
const lonInput = document.getElementById('sstvObsLon');
|
||||
|
||||
let storedLat = localStorage.getItem('observerLat');
|
||||
let storedLon = localStorage.getItem('observerLon');
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
storedLat = shared.lat.toString();
|
||||
storedLon = shared.lon.toString();
|
||||
}
|
||||
|
||||
if (latInput && storedLat) latInput.value = storedLat;
|
||||
if (lonInput && storedLon) lonInput.value = storedLon;
|
||||
function loadLocationInputs() {
|
||||
const latInput = document.getElementById('sstvObsLat');
|
||||
const lonInput = document.getElementById('sstvObsLon');
|
||||
|
||||
let storedLat = localStorage.getItem('observerLat');
|
||||
let storedLon = localStorage.getItem('observerLon');
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
storedLat = shared.lat.toString();
|
||||
storedLon = shared.lon.toString();
|
||||
}
|
||||
|
||||
if (latInput && storedLat) latInput.value = storedLat;
|
||||
if (lonInput && storedLon) lonInput.value = storedLon;
|
||||
|
||||
// Add change handlers to save and refresh
|
||||
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
||||
@@ -60,23 +60,23 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Save location from input fields
|
||||
*/
|
||||
function saveLocationFromInputs() {
|
||||
const latInput = document.getElementById('sstvObsLat');
|
||||
const lonInput = document.getElementById('sstvObsLon');
|
||||
function saveLocationFromInputs() {
|
||||
const latInput = document.getElementById('sstvObsLat');
|
||||
const lonInput = document.getElementById('sstvObsLon');
|
||||
|
||||
const lat = parseFloat(latInput?.value);
|
||||
const lon = parseFloat(lonInput?.value);
|
||||
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 &&
|
||||
!isNaN(lon) && lon >= -180 && lon <= 180) {
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
ObserverLocation.setShared({ lat, lon });
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat.toString());
|
||||
localStorage.setItem('observerLon', lon.toString());
|
||||
}
|
||||
loadIssSchedule(); // Refresh pass predictions
|
||||
}
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 &&
|
||||
!isNaN(lon) && lon >= -180 && lon <= 180) {
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
ObserverLocation.setShared({ lat, lon });
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat.toString());
|
||||
localStorage.setItem('observerLon', lon.toString());
|
||||
}
|
||||
loadIssSchedule(); // Refresh pass predictions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,12 +103,12 @@ const SSTV = (function() {
|
||||
if (latInput) latInput.value = lat;
|
||||
if (lonInput) lonInput.value = lon;
|
||||
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) });
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat);
|
||||
localStorage.setItem('observerLon', lon);
|
||||
}
|
||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||
ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) });
|
||||
} else {
|
||||
localStorage.setItem('observerLat', lat);
|
||||
localStorage.setItem('observerLon', lon);
|
||||
}
|
||||
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
@@ -159,7 +159,7 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize Leaflet map for ISS tracking
|
||||
*/
|
||||
function initMap() {
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
|
||||
@@ -173,10 +173,14 @@ const SSTV = (function() {
|
||||
attributionControl: false,
|
||||
worldCopyJump: true
|
||||
});
|
||||
window.issMap = issMap;
|
||||
|
||||
// Add tile layer using settings manager if available
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
if (typeof Settings !== 'undefined') {
|
||||
// Wait for settings to load from server before applying tiles
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(issMap);
|
||||
Settings.registerMap(issMap);
|
||||
} else {
|
||||
// Fallback to dark theme tiles
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
|
||||
@@ -887,6 +887,9 @@ const WiFiMode = (function() {
|
||||
clients.set(client.mac, client);
|
||||
updateStats();
|
||||
|
||||
// Update client display if this client belongs to the selected network
|
||||
updateClientInList(client);
|
||||
|
||||
if (onClientUpdate) onClientUpdate(client);
|
||||
}
|
||||
|
||||
@@ -1135,6 +1138,9 @@ const WiFiMode = (function() {
|
||||
|
||||
// Show the drawer
|
||||
elements.detailDrawer.classList.add('open');
|
||||
|
||||
// Fetch and display clients for this network
|
||||
fetchClientsForNetwork(network.bssid);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
@@ -1147,6 +1153,130 @@ const WiFiMode = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Client Display
|
||||
// ==========================================================================
|
||||
|
||||
async function fetchClientsForNetwork(bssid) {
|
||||
if (!elements.detailClientList) return;
|
||||
|
||||
try {
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
let response;
|
||||
|
||||
if (isAgentMode) {
|
||||
// Route through agent proxy
|
||||
response = await fetch(`/controller/agents/${currentAgent}/wifi/v2/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||
} else {
|
||||
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// Hide client list on error
|
||||
elements.detailClientList.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Handle agent response format (may be nested in 'result')
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
const clientList = result.clients || [];
|
||||
|
||||
if (clientList.length > 0) {
|
||||
renderClientList(clientList, bssid);
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderClientList(clientList, bssid) {
|
||||
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
// Update count badge
|
||||
if (countBadge) {
|
||||
countBadge.textContent = clientList.length;
|
||||
}
|
||||
|
||||
// Render client cards
|
||||
container.innerHTML = clientList.map(client => {
|
||||
const rssi = client.rssi_current;
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
|
||||
// Format last seen time
|
||||
const lastSeen = client.last_seen ? formatTime(client.last_seen) : '--';
|
||||
|
||||
// Build probed SSIDs badges
|
||||
let probesHtml = '';
|
||||
if (client.probed_ssids && client.probed_ssids.length > 0) {
|
||||
const probes = client.probed_ssids.slice(0, 5); // Show max 5
|
||||
probesHtml = `
|
||||
<div class="wifi-client-probes">
|
||||
${probes.map(ssid => `<span class="wifi-client-probe-badge">${escapeHtml(ssid)}</span>`).join('')}
|
||||
${client.probed_ssids.length > 5 ? `<span class="wifi-client-probe-badge">+${client.probed_ssids.length - 5}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="wifi-client-card" data-mac="${escapeHtml(client.mac)}">
|
||||
<div class="wifi-client-identity">
|
||||
<span class="wifi-client-mac">${escapeHtml(client.mac)}</span>
|
||||
<span class="wifi-client-vendor">${escapeHtml(client.vendor || 'Unknown vendor')}</span>
|
||||
${probesHtml}
|
||||
</div>
|
||||
<div class="wifi-client-signal">
|
||||
<span class="wifi-client-rssi ${signalClass}">${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'}</span>
|
||||
<span class="wifi-client-lastseen">${lastSeen}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateClientInList(client) {
|
||||
// Check if this client belongs to the currently selected network
|
||||
if (!selectedNetwork || client.associated_bssid !== selectedNetwork) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||
if (!container) return;
|
||||
|
||||
const existingCard = container.querySelector(`[data-mac="${client.mac}"]`);
|
||||
|
||||
if (existingCard) {
|
||||
// Update existing card's RSSI and last seen
|
||||
const rssiEl = existingCard.querySelector('.wifi-client-rssi');
|
||||
const lastSeenEl = existingCard.querySelector('.wifi-client-lastseen');
|
||||
|
||||
if (rssiEl && client.rssi_current !== null && client.rssi_current !== undefined) {
|
||||
const rssi = client.rssi_current;
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
rssiEl.textContent = rssi + ' dBm';
|
||||
rssiEl.className = 'wifi-client-rssi ' + signalClass;
|
||||
}
|
||||
|
||||
if (lastSeenEl && client.last_seen) {
|
||||
lastSeenEl.textContent = formatTime(client.last_seen);
|
||||
}
|
||||
} else {
|
||||
// New client for this network - re-fetch the full list
|
||||
fetchClientsForNetwork(selectedNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Statistics
|
||||
// ==========================================================================
|
||||
|
||||
+4916
-4899
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,15 @@
|
||||
<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/settings.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
@@ -22,6 +29,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">
|
||||
@@ -462,7 +472,7 @@
|
||||
|
||||
if (!points.length) {
|
||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||
ctx.font = '12px "JetBrains Mono", monospace';
|
||||
ctx.font = '12px "Space Mono", monospace';
|
||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||
return;
|
||||
}
|
||||
@@ -470,7 +480,7 @@
|
||||
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
|
||||
if (!series.length) {
|
||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||
ctx.font = '12px "JetBrains Mono", monospace';
|
||||
ctx.font = '12px "Space Mono", monospace';
|
||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||
return;
|
||||
}
|
||||
@@ -511,7 +521,7 @@
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
|
||||
ctx.font = '11px "JetBrains Mono", monospace';
|
||||
ctx.font = '11px "Space Mono", monospace';
|
||||
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
|
||||
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
|
||||
}
|
||||
@@ -761,5 +771,14 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
{% include 'partials/settings-modal.html' %}
|
||||
|
||||
<!-- Help Modal -->
|
||||
{% include 'partials/help-modal.html' %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+568
-588
File diff suppressed because it is too large
Load Diff
@@ -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' %}
|
||||
@@ -18,8 +18,13 @@
|
||||
<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>
|
||||
{% endif %}
|
||||
<!-- Core CSS variables -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
|
||||
<script>
|
||||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||||
</script>
|
||||
@@ -46,11 +51,12 @@
|
||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||
</label>
|
||||
</div>
|
||||
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
||||
<a href="/" class="back-link">Main Dashboard</a>
|
||||
</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">
|
||||
@@ -384,7 +390,7 @@
|
||||
};
|
||||
|
||||
// Initialize map
|
||||
function initMap() {
|
||||
async function initMap() {
|
||||
if (observerLocation) {
|
||||
document.getElementById('obsLat').value = observerLocation.lat;
|
||||
document.getElementById('obsLon').value = observerLocation.lon;
|
||||
@@ -398,7 +404,9 @@
|
||||
|
||||
// Use settings manager for tile layer (allows runtime changes)
|
||||
window.vesselMap = vesselMap;
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
if (typeof Settings !== 'undefined') {
|
||||
// Wait for settings to load from server before applying tiles
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(vesselMap);
|
||||
Settings.registerMap(vesselMap);
|
||||
} else {
|
||||
@@ -1495,7 +1503,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 {
|
||||
@@ -1547,8 +1555,17 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
{% include 'partials/settings-modal.html' %}
|
||||
|
||||
<!-- Help Modal -->
|
||||
{% include 'partials/help-modal.html' %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
|
||||
|
||||
<!-- Agent Manager -->
|
||||
<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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,27 @@
|
||||
{#
|
||||
Status Badge Component
|
||||
Compact status indicator with dot and text
|
||||
|
||||
Variables:
|
||||
- status: 'online', 'offline', 'warning', 'error' (default: 'offline')
|
||||
- text: Status text to display
|
||||
- id: Optional ID for the text element (for JS updates)
|
||||
- dot_id: Optional ID for the dot element (for JS updates)
|
||||
- pulse: If true, adds pulse animation to dot
|
||||
#}
|
||||
|
||||
{% set status_class = {
|
||||
'online': 'online',
|
||||
'active': 'online',
|
||||
'offline': 'offline',
|
||||
'warning': 'warning',
|
||||
'error': 'error',
|
||||
'inactive': 'inactive'
|
||||
}.get(status|default('offline'), 'inactive') %}
|
||||
|
||||
<div class="status-badge flex items-center gap-2">
|
||||
<div class="status-dot {{ status_class }}{% if pulse %} pulse{% endif %}"
|
||||
{% if dot_id %}id="{{ dot_id }}"{% endif %}></div>
|
||||
<span class="text-sm"
|
||||
{% if id %}id="{{ id }}"{% endif %}>{{ text|default('Unknown') }}</span>
|
||||
</div>
|
||||
+143
-151
@@ -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' %}
|
||||
@@ -39,6 +39,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
{% 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/index.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
|
||||
@@ -290,7 +291,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 +349,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>
|
||||
@@ -508,8 +411,7 @@
|
||||
{% if devices %}
|
||||
{% for device in devices %}
|
||||
<option value="{{ device.index }}"
|
||||
data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{
|
||||
device.name }}</option>
|
||||
data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}{% if device.serial and device.serial != 'N/A' and device.serial != 'Unknown' %} (SN: {{ device.serial }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="0">No devices found</option>
|
||||
@@ -806,7 +708,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
|
||||
<h6>Connected Clients</h6>
|
||||
<h6>Connected Clients <span class="wifi-client-count-badge" id="wifiClientCountBadge"></span></h6>
|
||||
<div class="wifi-client-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1088,7 +990,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>
|
||||
@@ -1108,7 +1010,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>
|
||||
@@ -1154,6 +1056,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
|
||||
@@ -1166,6 +1072,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;">
|
||||
@@ -1260,10 +1173,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"
|
||||
@@ -1336,7 +1249,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>
|
||||
@@ -1345,15 +1258,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>
|
||||
@@ -1411,19 +1326,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>
|
||||
@@ -2118,7 +2033,7 @@
|
||||
// After fade out, hide welcome and switch to mode
|
||||
setTimeout(() => {
|
||||
welcome.style.display = 'none';
|
||||
switchMode(mode);
|
||||
switchMode(mode, { updateUrl: true });
|
||||
}, 400);
|
||||
}
|
||||
|
||||
@@ -2127,6 +2042,53 @@
|
||||
document.getElementById('disclaimerModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Mode from query string (e.g., /?mode=wifi)
|
||||
let pendingStartMode = null;
|
||||
const validModes = new Set([
|
||||
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
||||
'spystations', 'meshtastic', 'wifi', 'bluetooth',
|
||||
'tscm', 'satellite', 'sstv'
|
||||
]);
|
||||
|
||||
function getModeFromQuery() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mode = params.get('mode');
|
||||
if (!mode || !validModes.has(mode)) return null;
|
||||
return mode;
|
||||
}
|
||||
|
||||
function applyModeFromQuery() {
|
||||
const mode = getModeFromQuery();
|
||||
if (!mode) return;
|
||||
const accepted = localStorage.getItem('disclaimerAccepted') === 'true';
|
||||
if (accepted) {
|
||||
const welcome = document.getElementById('welcomePage');
|
||||
if (welcome) welcome.style.display = 'none';
|
||||
switchMode(mode, { updateUrl: false });
|
||||
updateModeUrl(mode, true);
|
||||
} else {
|
||||
pendingStartMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
function applySettingsFromQuery() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('settings') === '1') {
|
||||
// Remove settings param from URL to avoid reopening on refresh
|
||||
params.delete('settings');
|
||||
const newUrl = params.toString()
|
||||
? window.location.pathname + '?' + params.toString()
|
||||
: window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
// Open settings modal after a brief delay to ensure page is ready
|
||||
setTimeout(() => {
|
||||
if (typeof showSettings === 'function') {
|
||||
showSettings();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function acceptDisclaimer() {
|
||||
localStorage.setItem('disclaimerAccepted', 'true');
|
||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||
@@ -2138,7 +2100,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);
|
||||
}
|
||||
|
||||
@@ -2457,6 +2426,12 @@
|
||||
|
||||
// Start SDR device status polling
|
||||
startSdrStatusPolling();
|
||||
|
||||
// Apply mode from URL query (e.g., /?mode=wifi)
|
||||
applyModeFromQuery();
|
||||
|
||||
// Check for settings=1 query param (from dashboard settings button)
|
||||
applySettingsFromQuery();
|
||||
});
|
||||
|
||||
// Toggle section collapse
|
||||
@@ -2512,8 +2487,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
function updateModeUrl(mode, replace = false) {
|
||||
if (!validModes.has(mode)) return;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('mode', mode);
|
||||
if (replace) {
|
||||
window.history.replaceState({ mode }, '', url);
|
||||
} else {
|
||||
window.history.pushState({ mode }, '', url);
|
||||
}
|
||||
}
|
||||
|
||||
// Mode switching
|
||||
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) {
|
||||
@@ -2526,6 +2513,9 @@
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
if (updateUrl) {
|
||||
updateModeUrl(mode);
|
||||
}
|
||||
|
||||
// Sync mode state with current agent/local after switching
|
||||
if (isAgentMode && typeof syncAgentModeStates === 'function') {
|
||||
@@ -2758,6 +2748,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(() => {
|
||||
@@ -3696,9 +3693,10 @@
|
||||
if (filteredDevices.length === 0) {
|
||||
select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`;
|
||||
} else {
|
||||
select.innerHTML = filteredDevices.map(d =>
|
||||
`<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}</option>`
|
||||
).join('');
|
||||
select.innerHTML = filteredDevices.map(d => {
|
||||
const serialSuffix = d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (SN: ${d.serial})` : '';
|
||||
return `<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}${serialSuffix}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update capabilities display
|
||||
@@ -3764,7 +3762,7 @@
|
||||
return `<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border-color);">
|
||||
<div style="display: flex; align-items: center;">
|
||||
${statusDot}
|
||||
<span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}</span>
|
||||
<span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}${d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (${d.serial})` : ''}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span style="font-size: 10px; color: ${modeColor}; font-weight: bold;">${modeName}</span>
|
||||
@@ -4283,7 +4281,7 @@
|
||||
|
||||
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.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
||||
infoEl.textContent = text;
|
||||
output.insertBefore(infoEl, output.firstChild);
|
||||
}
|
||||
@@ -4299,7 +4297,7 @@
|
||||
|
||||
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.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
||||
errorEl.textContent = '⚠ ' + text;
|
||||
output.insertBefore(errorEl, output.firstChild);
|
||||
}
|
||||
@@ -6915,7 +6913,7 @@
|
||||
|
||||
// Draw total in center
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 16px JetBrains Mono';
|
||||
ctx.font = 'bold 16px Space Mono';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(total, cx, cy);
|
||||
@@ -7947,7 +7945,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initAprsMap() {
|
||||
async function initAprsMap() {
|
||||
if (aprsMap) return;
|
||||
|
||||
const mapContainer = document.getElementById('aprsMap');
|
||||
@@ -7962,7 +7960,9 @@
|
||||
window.aprsMap = aprsMap;
|
||||
|
||||
// Use settings manager for tile layer (allows runtime changes)
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
if (typeof Settings !== 'undefined') {
|
||||
// Wait for settings to load from server before applying tiles
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(aprsMap);
|
||||
Settings.registerMap(aprsMap);
|
||||
} else {
|
||||
@@ -8685,7 +8685,7 @@
|
||||
// Label
|
||||
if (el > 0) {
|
||||
ctx.fillStyle = '#444';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(el + '°', cx, cy - r + 12);
|
||||
}
|
||||
@@ -8758,7 +8758,7 @@
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '11px JetBrains Mono';
|
||||
ctx.font = '11px Space Mono';
|
||||
ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
|
||||
}
|
||||
}
|
||||
@@ -8870,7 +8870,7 @@
|
||||
let observerMarker = null;
|
||||
let satPositionInterval = null;
|
||||
|
||||
function initGroundTrackMap() {
|
||||
async function initGroundTrackMap() {
|
||||
const mapContainer = document.getElementById('groundTrackMap');
|
||||
if (!mapContainer || groundTrackMap) return;
|
||||
|
||||
@@ -8883,7 +8883,9 @@
|
||||
window.groundTrackMap = groundTrackMap;
|
||||
|
||||
// Use settings manager for tile layer (allows runtime changes)
|
||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
||||
if (typeof Settings !== 'undefined') {
|
||||
// Wait for settings to load from server before applying tiles
|
||||
await Settings.init();
|
||||
Settings.createTileLayer().addTo(groundTrackMap);
|
||||
Settings.registerMap(groundTrackMap);
|
||||
} else {
|
||||
@@ -9343,14 +9345,10 @@
|
||||
// Theme toggle functions
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-theme');
|
||||
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
html.removeAttribute('data-theme');
|
||||
} else {
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
|
||||
// Save to localStorage for instant load on next visit
|
||||
localStorage.setItem('intercept-theme', newTheme);
|
||||
@@ -9382,10 +9380,8 @@
|
||||
// Load saved theme and animations on page load
|
||||
(function () {
|
||||
// First apply localStorage theme for instant load (no flash)
|
||||
const localTheme = localStorage.getItem('intercept-theme');
|
||||
if (localTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
const localTheme = localStorage.getItem('intercept-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', localTheme);
|
||||
|
||||
// Apply animations preference
|
||||
const localAnimations = localStorage.getItem('intercept-animations');
|
||||
@@ -9401,11 +9397,7 @@
|
||||
const serverTheme = data.value;
|
||||
if (serverTheme !== localTheme) {
|
||||
// Server has different theme, apply it
|
||||
if (serverTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', serverTheme);
|
||||
localStorage.setItem('intercept-theme', serverTheme);
|
||||
}
|
||||
}
|
||||
@@ -12205,7 +12197,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>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="{{ theme|default('dark') }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}iNTERCEPT{% endblock %} // iNTERCEPT</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||
|
||||
{# Fonts - Conditional CDN/Local loading #}
|
||||
{% if offline_settings and offline_settings.fonts_source == 'local' %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||
{% else %}
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
{% endif %}
|
||||
|
||||
{# Core CSS (Design System) #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/base.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/components.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||||
|
||||
{# Responsive styles #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
|
||||
{# Page-specific CSS #}
|
||||
{% block styles %}{% endblock %}
|
||||
|
||||
{# Page-specific head content #}
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
{# Global Header #}
|
||||
{% block header %}
|
||||
<header class="app-header">
|
||||
<div class="app-header-left">
|
||||
<button class="hamburger-btn" id="hamburgerBtn" aria-label="Toggle navigation menu">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<a href="/" class="app-logo">
|
||||
<svg class="app-logo-icon" width="40" height="40" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 30 Q5 50, 15 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M22 35 Q14 50, 22 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M29 40 Q23 50, 29 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<path d="M85 30 Q95 50, 85 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M78 35 Q86 50, 78 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M71 40 Q77 50, 71 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<circle cx="50" cy="22" r="6" fill="var(--accent-green)"/>
|
||||
<rect x="44" y="35" width="12" height="45" rx="2" fill="var(--accent-cyan)"/>
|
||||
<rect x="38" y="35" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
|
||||
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
|
||||
</svg>
|
||||
<span class="app-logo-text">
|
||||
<span class="app-logo-title">iNTERCEPT</span>
|
||||
<span class="app-logo-tagline">// See the Invisible</span>
|
||||
</span>
|
||||
</a>
|
||||
{% if version %}
|
||||
<span class="badge badge-primary">v{{ version }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="app-header-right">
|
||||
{% block header_right %}
|
||||
<div class="header-clock">
|
||||
<span class="header-clock-label">UTC</span>
|
||||
<span id="headerUtcTime">--:--:--</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{# Global Navigation - opt-in for pages that need it #}
|
||||
{# Override this block and include 'partials/nav.html' in child templates #}
|
||||
{% block navigation %}{% endblock %}
|
||||
|
||||
{# Main Content Area #}
|
||||
<main class="app-main">
|
||||
{% block main %}
|
||||
<div class="content-wrapper">
|
||||
{# Optional Sidebar #}
|
||||
{% block sidebar %}{% endblock %}
|
||||
|
||||
{# Page Content #}
|
||||
<div class="app-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{# Toast/Notification Container #}
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
</div>
|
||||
|
||||
{# Core JavaScript #}
|
||||
<script>
|
||||
// UTC Clock
|
||||
function updateUtcClock() {
|
||||
const now = new Date();
|
||||
const utc = now.toISOString().slice(11, 19);
|
||||
const clockEl = document.getElementById('headerUtcTime');
|
||||
if (clockEl) clockEl.textContent = utc;
|
||||
}
|
||||
setInterval(updateUtcClock, 1000);
|
||||
updateUtcClock();
|
||||
|
||||
// Mobile menu toggle
|
||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||
const drawerOverlay = document.getElementById('drawerOverlay');
|
||||
|
||||
if (hamburgerBtn) {
|
||||
hamburgerBtn.addEventListener('click', function() {
|
||||
this.classList.toggle('open');
|
||||
document.querySelector('.app-sidebar')?.classList.toggle('open');
|
||||
drawerOverlay?.classList.toggle('visible');
|
||||
});
|
||||
}
|
||||
|
||||
if (drawerOverlay) {
|
||||
drawerOverlay.addEventListener('click', function() {
|
||||
hamburgerBtn?.classList.remove('open');
|
||||
document.querySelector('.app-sidebar')?.classList.remove('open');
|
||||
this.classList.remove('visible');
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
}
|
||||
|
||||
// Apply saved theme
|
||||
const savedTheme = localStorage.getItem('intercept-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
// Nav dropdown handling
|
||||
function toggleNavDropdown(groupName) {
|
||||
const group = document.querySelector(`.nav-group[data-group="${groupName}"]`);
|
||||
if (!group) return;
|
||||
|
||||
// Close other dropdowns
|
||||
document.querySelectorAll('.nav-group.open').forEach(g => {
|
||||
if (g !== group) g.classList.remove('open');
|
||||
});
|
||||
|
||||
group.classList.toggle('open');
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.nav-group')) {
|
||||
document.querySelectorAll('.nav-group.open').forEach(g => g.classList.remove('open'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{# Page-specific JavaScript #}
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 %}
|
||||
+1123
-1106
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,318 @@
|
||||
{#
|
||||
Help Modal Partial
|
||||
Provides consistent help modal across all pages
|
||||
#}
|
||||
|
||||
<!-- Help Modal -->
|
||||
<div id="helpModal" class="help-modal" onclick="if(event.target === this) hideHelp()">
|
||||
<div class="help-content">
|
||||
<button class="help-close" onclick="hideHelp()">×</button>
|
||||
<h2>iNTERCEPT Help</h2>
|
||||
|
||||
<div class="help-tabs">
|
||||
<button class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')">Icons</button>
|
||||
<button class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')">Modes</button>
|
||||
<button class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')">WiFi</button>
|
||||
<button class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')">Tips</button>
|
||||
</div>
|
||||
|
||||
<!-- Icons Section -->
|
||||
<div id="help-icons" class="help-section active">
|
||||
<h3>Stats Bar Icons</h3>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-item"><span class="icon">📟</span><span class="desc">POCSAG messages decoded</span></div>
|
||||
<div class="icon-item"><span class="icon">📠</span><span class="desc">FLEX messages decoded</span></div>
|
||||
<div class="icon-item"><span class="icon">📨</span><span class="desc">Total messages received</span></div>
|
||||
<div class="icon-item"><span class="icon">🌡️</span><span class="desc">Unique sensors detected</span></div>
|
||||
<div class="icon-item"><span class="icon">📊</span><span class="desc">Device types found</span></div>
|
||||
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellites monitored</span></div>
|
||||
<div class="icon-item"><span class="icon">📡</span><span class="desc">WiFi Access Points</span></div>
|
||||
<div class="icon-item"><span class="icon">👤</span><span class="desc">Connected WiFi clients</span></div>
|
||||
<div class="icon-item"><span class="icon">🤝</span><span class="desc">Captured handshakes</span></div>
|
||||
<div class="icon-item"><span class="icon">🚁</span><span class="desc">Detected drones (click for details)</span></div>
|
||||
<div class="icon-item"><span class="icon">⚠️</span><span class="desc">Rogue APs (click for details)</span></div>
|
||||
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth devices</span></div>
|
||||
<div class="icon-item"><span class="icon">📍</span><span class="desc">BLE beacons / APRS stations</span></div>
|
||||
</div>
|
||||
|
||||
<h3>Mode Tab Icons</h3>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-item"><span class="icon">📟</span><span class="desc">Pager - POCSAG/FLEX decoder</span></div>
|
||||
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span></div>
|
||||
<div class="icon-item"><span class="icon">⚡</span><span class="desc">Meters - Utility meter decoder</span></div>
|
||||
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - ADS-B tracking & history</span></div>
|
||||
<div class="icon-item"><span class="icon">🚢</span><span class="desc">Vessels - AIS & VHF DSC distress</span></div>
|
||||
<div class="icon-item"><span class="icon">📻</span><span class="desc">Spy Stations - Number stations database</span></div>
|
||||
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio tracking</span></div>
|
||||
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellite - Pass prediction</span></div>
|
||||
<div class="icon-item"><span class="icon">📶</span><span class="desc">WiFi - Network scanner</span></div>
|
||||
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
|
||||
<div class="icon-item"><span class="icon">📻</span><span class="desc">Listening Post - SDR scanner</span></div>
|
||||
<div class="icon-item"><span class="icon">🔍</span><span class="desc">TSCM - Counter-surveillance</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modes Section -->
|
||||
<div id="help-modes" class="help-section">
|
||||
<h3>Pager Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes POCSAG and FLEX pager signals using RTL-SDR</li>
|
||||
<li>Set frequency to local pager frequencies (common: 152-158 MHz)</li>
|
||||
<li>Messages are displayed in real-time as they're decoded</li>
|
||||
<li>Use presets for common pager frequencies</li>
|
||||
</ul>
|
||||
|
||||
<h3>433MHz Sensor Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes wireless sensors on 433.92 MHz ISM band</li>
|
||||
<li>Detects temperature, humidity, weather stations, tire pressure monitors</li>
|
||||
<li>Supports many common protocols (Acurite, LaCrosse, Oregon Scientific, etc.)</li>
|
||||
<li>Device intelligence builds profiles of recurring devices</li>
|
||||
</ul>
|
||||
|
||||
<h3>Utility Meter Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes utility meter transmissions (water, gas, electric) using rtlamr</li>
|
||||
<li>Supports ERT protocol on 912 MHz (North America) or 868 MHz (Europe)</li>
|
||||
<li>Displays meter IDs and consumption data in real-time</li>
|
||||
<li>Supports SCM, SCM+, IDM, NetIDM, and R900 message types</li>
|
||||
</ul>
|
||||
|
||||
<h3>Aircraft (Dashboard)</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
|
||||
<li>Features radar scope, map view, airband audio, and ACARS decoding</li>
|
||||
<li>Optional history mode persists data to Postgres for long-term analysis</li>
|
||||
<li>Access history dashboard at <code>/adsb/history</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Vessels (Dashboard)</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Opens the AIS Dashboard for maritime vessel tracking</li>
|
||||
<li>Displays vessel name, MMSI, callsign, destination, and navigation data</li>
|
||||
<li><strong>VHF DSC Channel 70:</strong> Monitors maritime distress frequency (156.525 MHz)</li>
|
||||
<li>Decodes DSC messages: Distress, Urgency, Safety, and Routine calls</li>
|
||||
<li>MMSI country identification via Maritime Identification Digits (MID)</li>
|
||||
<li>Visual alerts for DISTRESS and URGENCY messages with map markers</li>
|
||||
</ul>
|
||||
|
||||
<h3>Spy Stations</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Database of number stations and diplomatic HF networks</li>
|
||||
<li>Browse stations from priyom.org with frequencies and schedules</li>
|
||||
<li>Filter by type (number/diplomatic), country, and mode</li>
|
||||
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
|
||||
<li>Click "Tune" to listen via Listening Post mode</li>
|
||||
</ul>
|
||||
|
||||
<h3>APRS Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes APRS (Automatic Packet Reporting System) on VHF</li>
|
||||
<li>Tracks amateur radio operators transmitting position data</li>
|
||||
<li>Regional frequencies: 144.390 MHz (N. America), 144.800 MHz (Europe)</li>
|
||||
<li>Uses Direwolf or multimon-ng for packet decoding</li>
|
||||
<li>Interactive map shows station positions in real-time</li>
|
||||
</ul>
|
||||
|
||||
<h3>Satellite Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Track satellites using TLE (Two-Line Element) data</li>
|
||||
<li>Add satellites manually or fetch from Celestrak by category</li>
|
||||
<li>Categories: Amateur, Weather, ISS, Starlink, GPS, and more</li>
|
||||
<li>View next pass predictions with elevation and duration</li>
|
||||
</ul>
|
||||
|
||||
<h3>WiFi Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Requires a WiFi adapter capable of monitor mode</li>
|
||||
<li>Click "Enable Monitor" to put adapter in monitor mode</li>
|
||||
<li>Scans all channels or lock to a specific channel</li>
|
||||
<li>Detects drones by SSID patterns and manufacturer OUI</li>
|
||||
<li>Rogue AP detection flags same SSID on multiple BSSIDs</li>
|
||||
<li>Click network rows to target for deauth or handshake capture</li>
|
||||
</ul>
|
||||
|
||||
<h3>Bluetooth Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Scans for classic Bluetooth and BLE devices</li>
|
||||
<li>Shows device names, addresses, and signal strength</li>
|
||||
<li>Manufacturer lookup from MAC address OUI</li>
|
||||
<li>Radar visualization shows device proximity</li>
|
||||
</ul>
|
||||
|
||||
<h3>Listening Post Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Wideband SDR scanner with spectrum visualization</li>
|
||||
<li>Tune to any frequency supported by your SDR hardware</li>
|
||||
<li>AM/FM/USB/LSB demodulation modes</li>
|
||||
<li>Bookmark frequencies for quick recall</li>
|
||||
<li>Quick tune presets for emergency and marine channels</li>
|
||||
</ul>
|
||||
|
||||
<h3>TSCM Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Technical Surveillance Countermeasures sweep</li>
|
||||
<li>Scans for unknown RF transmitters, WiFi devices, Bluetooth</li>
|
||||
<li>Baseline comparison to detect new/anomalous devices</li>
|
||||
<li>Threat classification: Critical, High, Medium, Low</li>
|
||||
<li>Useful for security audits and bug sweeps</li>
|
||||
<li><em style="color: var(--text-muted);">Note: This feature is in early development</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Meshtastic Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Integrates with Meshtastic LoRa mesh network devices</li>
|
||||
<li>Connect Heltec, T-Beam, RAK, or other compatible devices via USB</li>
|
||||
<li>Real-time message streaming with RSSI and SNR metrics</li>
|
||||
<li>Configure channels with encryption keys</li>
|
||||
<li>View connected nodes and message history</li>
|
||||
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Network Monitor</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Aggregates data from multiple remote INTERCEPT agents</li>
|
||||
<li>View all WiFi, Bluetooth, ADS-B, AIS data in one unified view</li>
|
||||
<li>Real-time streaming via Server-Sent Events (SSE)</li>
|
||||
<li>Location estimation using multi-agent trilateration</li>
|
||||
<li>Manage agents at <code>/controller/manage</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Section -->
|
||||
<div id="help-wifi" class="help-section">
|
||||
<h3>Monitor Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>Enable Monitor:</strong> Puts WiFi adapter in monitor mode for passive scanning</li>
|
||||
<li><strong>Kill Processes:</strong> Optional - stops NetworkManager/wpa_supplicant (may drop other connections)</li>
|
||||
<li>Some adapters rename when entering monitor mode (e.g., wlan0 → wlan0mon)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Handshake Capture</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Click "Capture" on a network to start targeted handshake capture</li>
|
||||
<li>Status panel shows capture progress and file location</li>
|
||||
<li>Use deauth to force clients to reconnect (only on authorized networks!)</li>
|
||||
<li>Handshake files saved to /tmp/intercept_handshake_*.cap</li>
|
||||
</ul>
|
||||
|
||||
<h3>Drone Detection</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Drones detected by SSID patterns (DJI, Parrot, Autel, etc.)</li>
|
||||
<li>Also detected by manufacturer OUI in MAC address</li>
|
||||
<li>Distance estimated from signal strength (approximate)</li>
|
||||
<li>Click drone count in stats bar to see all detected drones</li>
|
||||
</ul>
|
||||
|
||||
<h3>Rogue AP Detection</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Flags networks where same SSID appears on multiple BSSIDs</li>
|
||||
<li>Could indicate evil twin attack or legitimate multi-AP setup</li>
|
||||
<li>Click rogue count to see which SSIDs are flagged</li>
|
||||
</ul>
|
||||
|
||||
<h3>Proximity Alerts</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Add MAC addresses to watch list for alerts when detected</li>
|
||||
<li>Watch list persists in browser localStorage</li>
|
||||
<li>Useful for tracking specific devices</li>
|
||||
</ul>
|
||||
|
||||
<h3>Client Probe Analysis</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Shows what networks client devices are looking for</li>
|
||||
<li>Orange highlights indicate sensitive/private network names</li>
|
||||
<li>Reveals user location history (home, work, hotels, airports)</li>
|
||||
<li>Useful for security awareness and pen test reports</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Tips Section -->
|
||||
<div id="help-tips" class="help-section">
|
||||
<h3>General Tips</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>Collapsible sections:</strong> Click any section header (∇) to collapse/expand</li>
|
||||
<li><strong>Sound alerts:</strong> Toggle sound on/off in settings for each mode</li>
|
||||
<li><strong>Export data:</strong> Use export buttons to save captured data as JSON</li>
|
||||
<li><strong>Device Intelligence:</strong> Tracks device patterns over time</li>
|
||||
<li><strong>Theme toggle:</strong> Click the theme button in header to switch dark/light mode</li>
|
||||
<li><strong>Settings:</strong> Click the gear icon in the header to access settings</li>
|
||||
<li><strong>Offline mode:</strong> Enable in Settings to use local assets without internet</li>
|
||||
</ul>
|
||||
|
||||
<h3>Keyboard Shortcuts</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>F1</strong> - Open this help page</li>
|
||||
<li><strong>?</strong> - Open help (when not typing in a field)</li>
|
||||
<li><strong>Escape</strong> - Close help and modal dialogs</li>
|
||||
</ul>
|
||||
|
||||
<h3>Requirements</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
|
||||
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
|
||||
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
|
||||
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
|
||||
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
||||
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
|
||||
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
||||
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
||||
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
||||
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
|
||||
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
|
||||
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
|
||||
<li>Run as root/sudo for full hardware access</li>
|
||||
</ul>
|
||||
|
||||
<h3>Legal Notice</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Only use on networks and devices you own or have authorization to test</li>
|
||||
<li>Passive monitoring may be legal; active attacks require authorization</li>
|
||||
<li>Check local laws regarding radio frequency monitoring</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Help modal functions - defined here so all pages have them
|
||||
(function() {
|
||||
// Only define if not already defined (index.html defines its own)
|
||||
if (typeof window.showHelp === 'undefined') {
|
||||
window.showHelp = function() {
|
||||
document.getElementById('helpModal').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.hideHelp === 'undefined') {
|
||||
window.hideHelp = function() {
|
||||
document.getElementById('helpModal').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.switchHelpTab === 'undefined') {
|
||||
window.switchHelpTab = function(tab) {
|
||||
document.querySelectorAll('.help-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.help-section').forEach(s => s.classList.remove('active'));
|
||||
document.querySelector('.help-tab[data-tab="' + tab + '"]').classList.add('active');
|
||||
document.getElementById('help-' + tab).classList.add('active');
|
||||
};
|
||||
}
|
||||
|
||||
// Keyboard shortcuts for help (only add once)
|
||||
if (!window._helpKeyboardSetup) {
|
||||
window._helpKeyboardSetup = true;
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') hideHelp();
|
||||
// Open help with F1 or ? key (when not typing in an input)
|
||||
var helpModal = document.getElementById('helpModal');
|
||||
if (helpModal && (e.key === 'F1' || (e.key === '?' && !e.target.matches('input, textarea, select'))) && !helpModal.classList.contains('active')) {
|
||||
e.preventDefault();
|
||||
showHelp();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
{#
|
||||
Global Navigation Partial
|
||||
Single source of truth for app navigation
|
||||
|
||||
Compatible with:
|
||||
- index.html (uses switchMode() for mode panels)
|
||||
- Dashboard pages (uses navigation links)
|
||||
|
||||
Variables:
|
||||
- active_mode: Current active mode (e.g., 'pager', 'adsb', 'wifi')
|
||||
- is_index_page: If true, Satellite/SSTV use switchMode (panel mode)
|
||||
If false (default), Satellite links to dashboard
|
||||
#}
|
||||
|
||||
{% set is_index_page = is_index_page|default(false) %}
|
||||
|
||||
{% macro mode_item(mode, label, icon_svg, href=None) -%}
|
||||
{%- set is_active = 'active' if active_mode == mode else '' -%}
|
||||
{%- if href %}
|
||||
<a href="{{ href }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||
<span class="nav-label">{{ label }}</span>
|
||||
</a>
|
||||
{%- elif is_index_page %}
|
||||
<button class="mode-nav-btn {{ is_active }}" onclick="switchMode('{{ mode }}')">
|
||||
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||
<span class="nav-label">{{ label }}</span>
|
||||
</button>
|
||||
{%- else %}
|
||||
<a href="/?mode={{ mode }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||
<span class="nav-label">{{ label }}</span>
|
||||
</a>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro mobile_item(mode, label, icon_svg, href=None) -%}
|
||||
{%- set is_active = 'active' if active_mode == mode else '' -%}
|
||||
{%- if href %}
|
||||
<a href="{{ href }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||
</a>
|
||||
{%- elif is_index_page %}
|
||||
<button class="mobile-nav-btn {{ is_active }}" data-mode="{{ mode }}" onclick="switchMode('{{ mode }}')">
|
||||
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||
</button>
|
||||
{%- else %}
|
||||
<a href="/?mode={{ mode }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||
</a>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{# Desktop Navigation - uses existing CSS class names for compatibility #}
|
||||
<nav class="mode-nav" id="mainNav">
|
||||
{# SDR / RF Group #}
|
||||
<div class="mode-nav-dropdown" data-group="sdr">
|
||||
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('sdr')"{% endif %}>
|
||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
|
||||
<span class="nav-label">SDR / RF</span>
|
||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
|
||||
<div class="mode-nav-dropdown-menu">
|
||||
{{ mode_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
|
||||
{{ mode_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||
{{ mode_item('meshtastic', 'Meshtastic', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Wireless Group #}
|
||||
<div class="mode-nav-dropdown" data-group="wireless">
|
||||
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('wireless')"{% endif %}>
|
||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
||||
<span class="nav-label">Wireless</span>
|
||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
|
||||
<div class="mode-nav-dropdown-menu">
|
||||
{{ mode_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg>') }}
|
||||
{{ mode_item('bluetooth', 'Bluetooth', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Security Group #}
|
||||
<div class="mode-nav-dropdown" data-group="security">
|
||||
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('security')"{% endif %}>
|
||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
||||
<span class="nav-label">Security</span>
|
||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
|
||||
<div class="mode-nav-dropdown-menu">
|
||||
{{ mode_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Space Group #}
|
||||
<div class="mode-nav-dropdown" data-group="space">
|
||||
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('space')"{% endif %}>
|
||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
|
||||
<span class="nav-label">Space</span>
|
||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
|
||||
<div class="mode-nav-dropdown-menu">
|
||||
{% if is_index_page %}
|
||||
{{ mode_item('satellite', 'Satellite', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>') }}
|
||||
{% else %}
|
||||
{{ mode_item('satellite', 'Satellite', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>', '/satellite/dashboard') }}
|
||||
{% endif %}
|
||||
{{ mode_item('sstv', 'ISS SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Dynamic dashboard button (shown when in satellite mode) #}
|
||||
<div class="mode-nav-actions">
|
||||
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;">
|
||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span>
|
||||
<span class="nav-label">Full Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Nav Utilities (clock, theme, tools) #}
|
||||
<div class="nav-utilities">
|
||||
<div class="nav-clock">
|
||||
<span class="utc-label">UTC</span>
|
||||
<span class="utc-time" id="headerUtcTime">--:--:--</span>
|
||||
</div>
|
||||
<div class="nav-divider"></div>
|
||||
<a href="/" class="nav-dashboard-btn" title="Return to Main Dashboard" style="text-decoration: none;">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>
|
||||
<span class="nav-label">Main Dashboard</span>
|
||||
</a>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="nav-tools">
|
||||
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
|
||||
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
|
||||
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
|
||||
</button>
|
||||
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
|
||||
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
|
||||
<span class="icon-sun icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
|
||||
</button>
|
||||
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
||||
</a>
|
||||
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span>
|
||||
</a>
|
||||
<button class="nav-tool-btn" onclick="showSettings()" title="Settings">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||
</button>
|
||||
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
||||
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
|
||||
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# Mobile Navigation Bar #}
|
||||
<nav class="mobile-nav-bar" id="mobileNavBar">
|
||||
{{ mobile_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
|
||||
{{ mobile_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
|
||||
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||
{{ mobile_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||
{% if is_index_page %}
|
||||
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>') }}
|
||||
{% else %}
|
||||
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>', '/satellite/dashboard') }}
|
||||
{% endif %}
|
||||
{{ mobile_item('sstv', 'SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
|
||||
{{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||
{{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||
</nav>
|
||||
|
||||
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||
<script>
|
||||
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
|
||||
if (typeof switchMode === 'undefined') {
|
||||
window.switchMode = function(mode) {
|
||||
// On dashboard pages, navigate to main page with mode param
|
||||
window.location.href = '/?mode=' + mode;
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof toggleNavDropdown === 'undefined') {
|
||||
window.toggleNavDropdown = function(groupName) {
|
||||
const dropdown = document.querySelector(`.mode-nav-dropdown[data-group="${groupName}"]`);
|
||||
if (!dropdown) return;
|
||||
|
||||
// Close other dropdowns
|
||||
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => {
|
||||
if (d !== dropdown) d.classList.remove('open');
|
||||
});
|
||||
|
||||
dropdown.classList.toggle('open');
|
||||
};
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.mode-nav-dropdown')) {
|
||||
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => d.classList.remove('open'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof toggleAnimations === 'undefined') {
|
||||
window.toggleAnimations = function() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-animations') || 'on';
|
||||
const next = current === 'on' ? 'off' : 'on';
|
||||
html.setAttribute('data-animations', next);
|
||||
localStorage.setItem('animations', next);
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof toggleTheme === 'undefined') {
|
||||
window.toggleTheme = function() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('intercept-theme', next);
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof showSettings === 'undefined') {
|
||||
window.showSettings = function() {
|
||||
// Try to open settings modal if it exists on this page
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
if (typeof Settings !== 'undefined' && Settings.init) {
|
||||
Settings.init().then(() => {
|
||||
if (Settings.checkAssets) Settings.checkAssets();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fall back to navigating to main page settings
|
||||
window.location.href = '/?settings=1';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof hideSettings === 'undefined') {
|
||||
window.hideSettings = function() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// showHelp is defined by the help-modal.html partial
|
||||
|
||||
if (typeof logout === 'undefined') {
|
||||
window.logout = function(e) {
|
||||
if (e) e.preventDefault();
|
||||
if (confirm('Are you sure you want to logout?')) {
|
||||
window.location.href = '/logout';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Apply saved preferences and start clock
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('intercept-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
const savedAnimations = localStorage.getItem('intercept-animations');
|
||||
if (savedAnimations) {
|
||||
document.documentElement.setAttribute('data-animations', savedAnimations);
|
||||
}
|
||||
|
||||
// UTC Clock update (if not already defined by parent page)
|
||||
if (typeof window._navClockStarted === 'undefined') {
|
||||
window._navClockStarted = true;
|
||||
function updateNavUtcClock() {
|
||||
const now = new Date();
|
||||
const utc = now.toISOString().slice(11, 19);
|
||||
const el = document.getElementById('headerUtcTime');
|
||||
if (el) el.textContent = utc;
|
||||
}
|
||||
setInterval(updateNavUtcClock, 1000);
|
||||
updateNavUtcClock();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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()">×</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()">×</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">Space 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">Space 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>
|
||||
|
||||
+1021
-1003
File diff suppressed because it is too large
Load Diff
+84
-2
@@ -9,11 +9,12 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from urllib.request import urlopen, Request
|
||||
from urllib.error import URLError, HTTPError
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import config
|
||||
from utils.database import get_setting, set_setting
|
||||
@@ -509,6 +510,7 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
||||
'success': True,
|
||||
'updated': True,
|
||||
'message': 'Update successful! Please restart the application.',
|
||||
'restart_required': True,
|
||||
'requirements_changed': requirements_changed,
|
||||
'stashed': stashed,
|
||||
'stash_restored': stashed,
|
||||
@@ -527,3 +529,83 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
|
||||
def restart_application() -> dict[str, Any]:
|
||||
"""
|
||||
Restart the application using os.execv to replace the current process.
|
||||
|
||||
This function:
|
||||
1. Cleans up all running decoder processes
|
||||
2. Stops the cleanup manager
|
||||
3. Replaces the current process with a fresh Python interpreter
|
||||
|
||||
Returns:
|
||||
Dict with status (though this is typically not reached due to execv)
|
||||
"""
|
||||
import app as app_module
|
||||
from utils.cleanup import cleanup_manager
|
||||
from utils.process import cleanup_all_processes
|
||||
|
||||
logger.info("Application restart requested")
|
||||
|
||||
try:
|
||||
# Step 1: Kill all decoder processes
|
||||
logger.info("Stopping all decoder processes...")
|
||||
cleanup_all_processes()
|
||||
|
||||
# Step 2: Clear global process state
|
||||
with app_module.process_lock:
|
||||
app_module.current_process = None
|
||||
with app_module.sensor_lock:
|
||||
app_module.sensor_process = None
|
||||
with app_module.wifi_lock:
|
||||
app_module.wifi_process = None
|
||||
with app_module.adsb_lock:
|
||||
app_module.adsb_process = None
|
||||
with app_module.ais_lock:
|
||||
app_module.ais_process = None
|
||||
with app_module.acars_lock:
|
||||
app_module.acars_process = None
|
||||
with app_module.aprs_lock:
|
||||
app_module.aprs_process = None
|
||||
app_module.aprs_rtl_process = None
|
||||
with app_module.dsc_lock:
|
||||
app_module.dsc_process = None
|
||||
app_module.dsc_rtl_process = None
|
||||
|
||||
# Step 3: Clear SDR device registry
|
||||
with app_module.sdr_device_registry_lock:
|
||||
app_module.sdr_device_registry.clear()
|
||||
|
||||
# Step 4: Stop cleanup manager
|
||||
logger.info("Stopping cleanup manager...")
|
||||
cleanup_manager.stop()
|
||||
|
||||
# Step 5: Prepare for restart using os.execv
|
||||
# Get the Python executable and script path
|
||||
python_executable = sys.executable
|
||||
script_path = os.path.abspath(sys.argv[0])
|
||||
|
||||
# Build argument list (preserve original command-line args)
|
||||
args = [python_executable, script_path] + sys.argv[1:]
|
||||
|
||||
logger.info(f"Restarting with: {' '.join(args)}")
|
||||
|
||||
# Flush any pending log output
|
||||
logging.shutdown()
|
||||
|
||||
# Use os.execv to replace the current process
|
||||
# This will not return - the process is replaced entirely
|
||||
os.execv(python_executable, args)
|
||||
|
||||
# This code is never reached
|
||||
return {'success': True, 'message': 'Restarting...'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Restart failed: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'message': 'Failed to restart application. Please restart manually.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user