mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 08:13:32 -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",
|
"version": "2026-02-01_ba81b697",
|
||||||
"downloaded": "2026-01-12T15:55:42.769654Z"
|
"downloaded": "2026-02-04T17:06:54.806043Z"
|
||||||
}
|
}
|
||||||
@@ -278,9 +278,13 @@ def get_sdr_device_status() -> dict[int, str]:
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def require_login():
|
def require_login():
|
||||||
# Routes that don't require login (to avoid infinite redirect loop)
|
# Routes that don't require login (to avoid infinite redirect loop)
|
||||||
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']
|
allowed_routes = ['login', 'static', 'favicon', 'health', 'health_check']
|
||||||
|
|
||||||
|
# Allow audio streaming endpoints without session auth
|
||||||
|
if request.path.startswith('/listening/audio/'):
|
||||||
|
return None
|
||||||
|
|
||||||
# Controller API endpoints use API key auth, not session auth
|
# Controller API endpoints use API key auth, not session auth
|
||||||
# Allow agent push/pull endpoints without session login
|
# Allow agent push/pull endpoints without session login
|
||||||
@@ -645,19 +649,21 @@ def health_check() -> Response:
|
|||||||
|
|
||||||
@app.route('/killall', methods=['POST'])
|
@app.route('/killall', methods=['POST'])
|
||||||
def kill_all() -> Response:
|
def kill_all() -> Response:
|
||||||
"""Kill all decoder and WiFi processes."""
|
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
||||||
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process
|
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
||||||
|
|
||||||
# Import adsb and ais modules to reset their state
|
# Import adsb and ais modules to reset their state
|
||||||
from routes import adsb as adsb_module
|
from routes import adsb as adsb_module
|
||||||
from routes import ais as ais_module
|
from routes import ais as ais_module
|
||||||
|
from utils.bluetooth import reset_bluetooth_scanner
|
||||||
|
|
||||||
killed = []
|
killed = []
|
||||||
processes_to_kill = [
|
processes_to_kill = [
|
||||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||||
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher'
|
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
|
||||||
|
'hcitool', 'bluetoothctl'
|
||||||
]
|
]
|
||||||
|
|
||||||
for proc in processes_to_kill:
|
for proc in processes_to_kill:
|
||||||
@@ -701,6 +707,26 @@ def kill_all() -> Response:
|
|||||||
dsc_process = None
|
dsc_process = None
|
||||||
dsc_rtl_process = None
|
dsc_rtl_process = None
|
||||||
|
|
||||||
|
# Reset Bluetooth state (legacy)
|
||||||
|
with bt_lock:
|
||||||
|
if bt_process:
|
||||||
|
try:
|
||||||
|
bt_process.terminate()
|
||||||
|
bt_process.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
bt_process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
bt_process = None
|
||||||
|
|
||||||
|
# Reset Bluetooth v2 scanner
|
||||||
|
try:
|
||||||
|
reset_bluetooth_scanner()
|
||||||
|
killed.append('bluetooth_scanner')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Clear SDR device registry
|
# Clear SDR device registry
|
||||||
with sdr_device_registry_lock:
|
with sdr_device_registry_lock:
|
||||||
sdr_device_registry.clear()
|
sdr_device_registry.clear()
|
||||||
|
|||||||
@@ -7,10 +7,29 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Application version
|
# Application version
|
||||||
VERSION = "2.12.1"
|
VERSION = "2.13.1"
|
||||||
|
|
||||||
# Changelog - latest release notes (shown on welcome screen)
|
# Changelog - latest release notes (shown on welcome screen)
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "2.13.1",
|
||||||
|
"date": "February 2026",
|
||||||
|
"highlights": [
|
||||||
|
"Help modal system with keyboard shortcuts reference",
|
||||||
|
"Main Dashboard button in navigation bar",
|
||||||
|
"Settings modal accessible from all dashboards",
|
||||||
|
"Dashboard CSS improvements and consistency fixes",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2.13.0",
|
||||||
|
"date": "February 2026",
|
||||||
|
"highlights": [
|
||||||
|
"WiFi client display in AP detail drawer",
|
||||||
|
"Real-time client updates via SSE streaming",
|
||||||
|
"Probed SSID badges for connected clients",
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "2.12.1",
|
"version": "2.12.1",
|
||||||
"date": "February 2026",
|
"date": "February 2026",
|
||||||
|
|||||||
@@ -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]
|
[project]
|
||||||
name = "intercept"
|
name = "intercept"
|
||||||
version = "2.12.1"
|
version = "2.13.1"
|
||||||
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
|
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
+36
-9
@@ -732,16 +732,43 @@ def start_adsb():
|
|||||||
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
|
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if sdr_type == SDRType.RTL_SDR:
|
|
||||||
error_msg = 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.'
|
# Parse stderr to provide specific guidance
|
||||||
if stderr_output:
|
error_type = 'START_FAILED'
|
||||||
error_msg += f' Error: {stderr_output[:200]}'
|
stderr_lower = stderr_output.lower()
|
||||||
return jsonify({'status': 'error', 'message': error_msg})
|
|
||||||
|
if 'usb_claim_interface' in stderr_lower or 'libusb_error_busy' in stderr_lower or 'device or resource busy' in stderr_lower:
|
||||||
|
error_msg = 'SDR device is busy. Another process may be using it.'
|
||||||
|
suggestion = 'Try: 1) Stop other SDR applications, 2) Run "pkill -f rtl_" to kill stale processes, or 3) Remove and reinsert the SDR device.'
|
||||||
|
error_type = 'DEVICE_BUSY'
|
||||||
|
elif 'no supported devices' in stderr_lower or 'no rtl-sdr' in stderr_lower or 'failed to open' in stderr_lower:
|
||||||
|
error_msg = 'RTL-SDR device not found.'
|
||||||
|
suggestion = 'Ensure the device is connected. Try removing and reinserting the SDR.'
|
||||||
|
error_type = 'DEVICE_NOT_FOUND'
|
||||||
|
elif 'kernel driver is active' in stderr_lower or 'dvb' in stderr_lower:
|
||||||
|
error_msg = 'Kernel DVB-T driver is blocking the device.'
|
||||||
|
suggestion = 'Blacklist the DVB drivers: Go to Settings > Hardware > "Blacklist DVB Drivers" or run "sudo rmmod dvb_usb_rtl28xxu".'
|
||||||
|
error_type = 'KERNEL_DRIVER'
|
||||||
|
elif 'permission' in stderr_lower or 'access' in stderr_lower:
|
||||||
|
error_msg = 'Permission denied accessing RTL-SDR device.'
|
||||||
|
suggestion = 'Run Intercept with sudo, or add udev rules for RTL-SDR devices.'
|
||||||
|
error_type = 'PERMISSION_DENIED'
|
||||||
|
elif sdr_type == SDRType.RTL_SDR:
|
||||||
|
error_msg = 'dump1090 failed to start.'
|
||||||
|
suggestion = 'Try removing and reinserting the SDR device, or check if another application is using it.'
|
||||||
else:
|
else:
|
||||||
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.'
|
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}.'
|
||||||
if stderr_output:
|
suggestion = 'Ensure readsb is installed with SoapySDR support and the device is connected.'
|
||||||
error_msg += f' Error: {stderr_output[:200]}'
|
|
||||||
return jsonify({'status': 'error', 'message': error_msg})
|
full_msg = f'{error_msg} {suggestion}'
|
||||||
|
if stderr_output and len(stderr_output) < 300:
|
||||||
|
full_msg += f' (Details: {stderr_output})'
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': error_type,
|
||||||
|
'message': full_msg
|
||||||
|
})
|
||||||
|
|
||||||
adsb_using_service = True
|
adsb_using_service = True
|
||||||
adsb_active_device = device # Track which device is being used
|
adsb_active_device = device # Track which device is being used
|
||||||
|
|||||||
@@ -228,9 +228,13 @@ def init_audio_websocket(app: Flask):
|
|||||||
|
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if "timed out" not in str(e).lower():
|
msg = str(e).lower()
|
||||||
logger.error(f"WebSocket receive error: {e}")
|
if "connection closed" in msg:
|
||||||
|
logger.info("WebSocket closed by client")
|
||||||
|
break
|
||||||
|
if "timed out" not in msg:
|
||||||
|
logger.error(f"WebSocket receive error: {e}")
|
||||||
|
|
||||||
# Stream audio data if active
|
# Stream audio data if active
|
||||||
if streaming and proc and proc.poll() is None:
|
if streaming and proc and proc.poll() is None:
|
||||||
|
|||||||
+720
-257
File diff suppressed because it is too large
Load Diff
+42
-2
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response
|
from flask import Blueprint, Response, jsonify, request
|
||||||
|
|
||||||
from utils.logging import get_logger
|
from utils.logging import get_logger
|
||||||
from utils.updater import (
|
from utils.updater import (
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
get_update_status,
|
|
||||||
dismiss_update,
|
dismiss_update,
|
||||||
|
get_update_status,
|
||||||
perform_update,
|
perform_update,
|
||||||
|
restart_application,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = get_logger('intercept.routes.updater')
|
logger = get_logger('intercept.routes.updater')
|
||||||
@@ -137,3 +138,42 @@ def dismiss_notification() -> Response:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@updater_bp.route('/restart', methods=['POST'])
|
||||||
|
def restart_app() -> Response:
|
||||||
|
"""
|
||||||
|
Restart the application.
|
||||||
|
|
||||||
|
This endpoint triggers a graceful restart of the application:
|
||||||
|
1. Stops all running decoder processes
|
||||||
|
2. Cleans up global state
|
||||||
|
3. Replaces the current process with a fresh instance
|
||||||
|
|
||||||
|
The response may not be received by the client since the process
|
||||||
|
is replaced immediately. Clients should poll /health until the
|
||||||
|
server responds again.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with restart status (may not be delivered)
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
logger.info("Restart requested via API")
|
||||||
|
|
||||||
|
# Send response before restarting
|
||||||
|
# Use a short delay to allow the response to be sent
|
||||||
|
def delayed_restart():
|
||||||
|
import time
|
||||||
|
time.sleep(0.5) # Allow response to be sent
|
||||||
|
restart_application()
|
||||||
|
|
||||||
|
# Start restart in a background thread so we can return a response
|
||||||
|
restart_thread = threading.Thread(target=delayed_restart, daemon=False)
|
||||||
|
restart_thread.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Application is restarting. Please wait...',
|
||||||
|
'action': 'restart'
|
||||||
|
})
|
||||||
|
|||||||
@@ -1092,4 +1092,7 @@ main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
||||||
|
# Clear traps before exiting to prevent spurious errors during cleanup
|
||||||
|
trap - ERR EXIT
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -71,7 +73,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header - Mobile first */
|
/* Header */
|
||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -81,20 +83,19 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.header {
|
.header {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -126,14 +127,52 @@ body {
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-select-sm {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-green);
|
||||||
|
box-shadow: 0 0 6px var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-status-dot.offline {
|
||||||
|
background: var(--accent-red);
|
||||||
|
box-shadow: 0 0 6px var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .show-all-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
@@ -172,15 +211,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main dashboard grid - Mobile first */
|
/* Main dashboard grid - Mobile first */
|
||||||
/* Header ~55px + Stats strip ~55px = ~110px, using 115px for safety */
|
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
|
||||||
.dashboard {
|
.dashboard {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: calc(100dvh - 115px);
|
height: calc(100dvh - 160px);
|
||||||
height: calc(100vh - 115px); /* Fallback */
|
height: calc(100vh - 160px); /* Fallback */
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +255,7 @@ body {
|
|||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.acars-sidebar {
|
.acars-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: calc(100dvh - 115px);
|
max-height: calc(100dvh - 160px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +663,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-value {
|
.telemetry-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -680,7 +719,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-icao {
|
.aircraft-icao {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
@@ -700,7 +739,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-detail-value {
|
.aircraft-detail-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -790,7 +829,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,7 +840,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,7 +918,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -911,7 +950,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1062,7 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -1057,7 +1096,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.airband-status {
|
.airband-status {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -1217,7 +1256,7 @@ body {
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
min-height: calc(100dvh - 115px);
|
min-height: calc(100dvh - 160px);
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
@@ -1286,12 +1325,6 @@ body {
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status bar - compact on mobile */
|
|
||||||
.status-bar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Strip time smaller on mobile */
|
/* Strip time smaller on mobile */
|
||||||
.strip-time {
|
.strip-time {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -1407,7 +1440,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.strip-value {
|
.strip-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -1545,7 +1578,7 @@ body {
|
|||||||
|
|
||||||
.report-grid span:nth-child(even) {
|
.report-grid span:nth-child(even) {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-highlights {
|
.report-highlights {
|
||||||
@@ -1784,7 +1817,7 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -1938,7 +1971,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.squawk-code {
|
.squawk-code {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #141a24;
|
--bg-card: #141a24;
|
||||||
@@ -20,14 +22,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-bg {
|
.radar-bg {
|
||||||
@@ -91,7 +93,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +270,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -306,7 +308,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-meta {
|
.panel-meta {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -347,7 +349,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-row td,
|
.empty-row td,
|
||||||
|
|||||||
+331
-343
@@ -1,343 +1,331 @@
|
|||||||
/*
|
/*
|
||||||
* Agents Management CSS
|
* Agents Management CSS
|
||||||
* Styles for the remote agent management interface
|
* Styles for the remote agent management interface
|
||||||
*/
|
* Inherits CSS variables from core/variables.css
|
||||||
|
*/
|
||||||
/* CSS Variables (inherited from main theme) */
|
|
||||||
:root {
|
/* Agent indicator in navigation */
|
||||||
--bg-primary: #0a0a0f;
|
.agent-indicator {
|
||||||
--bg-secondary: #12121a;
|
display: flex;
|
||||||
--text-primary: #e0e0e0;
|
align-items: center;
|
||||||
--text-secondary: #888;
|
gap: 8px;
|
||||||
--border-color: #1a1a2e;
|
padding: 6px 12px;
|
||||||
--accent-cyan: #00d4ff;
|
background: rgba(0, 212, 255, 0.1);
|
||||||
--accent-green: #00ff88;
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
--accent-red: #ff3366;
|
border-radius: 20px;
|
||||||
--accent-orange: #ff9f1c;
|
cursor: pointer;
|
||||||
}
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
/* Agent indicator in navigation */
|
|
||||||
.agent-indicator {
|
.agent-indicator:hover {
|
||||||
display: flex;
|
background: rgba(0, 212, 255, 0.2);
|
||||||
align-items: center;
|
border-color: var(--accent-cyan);
|
||||||
gap: 8px;
|
}
|
||||||
padding: 6px 12px;
|
|
||||||
background: rgba(0, 212, 255, 0.1);
|
.agent-indicator-dot {
|
||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
width: 8px;
|
||||||
border-radius: 20px;
|
height: 8px;
|
||||||
cursor: pointer;
|
border-radius: 50%;
|
||||||
transition: all 0.2s;
|
background: var(--accent-green);
|
||||||
}
|
box-shadow: 0 0 6px var(--accent-green);
|
||||||
|
}
|
||||||
.agent-indicator:hover {
|
|
||||||
background: rgba(0, 212, 255, 0.2);
|
.agent-indicator-dot.remote {
|
||||||
border-color: var(--accent-cyan);
|
background: var(--accent-cyan);
|
||||||
}
|
box-shadow: 0 0 6px var(--accent-cyan);
|
||||||
|
}
|
||||||
.agent-indicator-dot {
|
|
||||||
width: 8px;
|
.agent-indicator-dot.multiple {
|
||||||
height: 8px;
|
background: var(--accent-orange);
|
||||||
border-radius: 50%;
|
box-shadow: 0 0 6px var(--accent-orange);
|
||||||
background: var(--accent-green);
|
}
|
||||||
box-shadow: 0 0 6px var(--accent-green);
|
|
||||||
}
|
.agent-indicator-label {
|
||||||
|
font-size: 11px;
|
||||||
.agent-indicator-dot.remote {
|
color: var(--text-primary);
|
||||||
background: var(--accent-cyan);
|
font-family: var(--font-mono);
|
||||||
box-shadow: 0 0 6px var(--accent-cyan);
|
}
|
||||||
}
|
|
||||||
|
.agent-indicator-count {
|
||||||
.agent-indicator-dot.multiple {
|
font-size: 10px;
|
||||||
background: var(--accent-orange);
|
padding: 2px 6px;
|
||||||
box-shadow: 0 0 6px var(--accent-orange);
|
background: rgba(0, 212, 255, 0.2);
|
||||||
}
|
border-radius: 10px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
.agent-indicator-label {
|
}
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-primary);
|
/* Agent selector dropdown */
|
||||||
font-family: 'JetBrains Mono', monospace;
|
.agent-selector {
|
||||||
}
|
position: relative;
|
||||||
|
}
|
||||||
.agent-indicator-count {
|
|
||||||
font-size: 10px;
|
.agent-selector-dropdown {
|
||||||
padding: 2px 6px;
|
position: absolute;
|
||||||
background: rgba(0, 212, 255, 0.2);
|
top: 100%;
|
||||||
border-radius: 10px;
|
right: 0;
|
||||||
color: var(--accent-cyan);
|
margin-top: 8px;
|
||||||
}
|
min-width: 280px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
/* Agent selector dropdown */
|
border: 1px solid var(--border-color);
|
||||||
.agent-selector {
|
border-radius: 8px;
|
||||||
position: relative;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
}
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
.agent-selector-dropdown {
|
}
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
.agent-selector-dropdown.show {
|
||||||
right: 0;
|
display: block;
|
||||||
margin-top: 8px;
|
}
|
||||||
min-width: 280px;
|
|
||||||
background: var(--bg-secondary);
|
.agent-selector-header {
|
||||||
border: 1px solid var(--border-color);
|
display: flex;
|
||||||
border-radius: 8px;
|
justify-content: space-between;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
align-items: center;
|
||||||
z-index: 1000;
|
padding: 12px 15px;
|
||||||
display: none;
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-selector-dropdown.show {
|
.agent-selector-header h4 {
|
||||||
display: block;
|
margin: 0;
|
||||||
}
|
font-size: 12px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
.agent-selector-header {
|
text-transform: uppercase;
|
||||||
display: flex;
|
letter-spacing: 1px;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: center;
|
|
||||||
padding: 12px 15px;
|
.agent-selector-manage {
|
||||||
border-bottom: 1px solid var(--border-color);
|
font-size: 11px;
|
||||||
}
|
color: var(--accent-cyan);
|
||||||
|
text-decoration: none;
|
||||||
.agent-selector-header h4 {
|
}
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
.agent-selector-manage:hover {
|
||||||
color: var(--accent-cyan);
|
text-decoration: underline;
|
||||||
text-transform: uppercase;
|
}
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
.agent-selector-list {
|
||||||
|
max-height: 300px;
|
||||||
.agent-selector-manage {
|
overflow-y: auto;
|
||||||
font-size: 11px;
|
}
|
||||||
color: var(--accent-cyan);
|
|
||||||
text-decoration: none;
|
.agent-selector-item {
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.agent-selector-manage:hover {
|
gap: 10px;
|
||||||
text-decoration: underline;
|
padding: 10px 15px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
.agent-selector-list {
|
border-bottom: 1px solid var(--border-color);
|
||||||
max-height: 300px;
|
}
|
||||||
overflow-y: auto;
|
|
||||||
}
|
.agent-selector-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
.agent-selector-item {
|
}
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
.agent-selector-item:hover {
|
||||||
gap: 10px;
|
background: rgba(0, 212, 255, 0.1);
|
||||||
padding: 10px 15px;
|
}
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
.agent-selector-item.selected {
|
||||||
border-bottom: 1px solid var(--border-color);
|
background: rgba(0, 212, 255, 0.15);
|
||||||
}
|
border-left: 3px solid var(--accent-cyan);
|
||||||
|
}
|
||||||
.agent-selector-item:last-child {
|
|
||||||
border-bottom: none;
|
.agent-selector-item.local {
|
||||||
}
|
border-left: 3px solid var(--accent-green);
|
||||||
|
}
|
||||||
.agent-selector-item:hover {
|
|
||||||
background: rgba(0, 212, 255, 0.1);
|
.agent-selector-item-status {
|
||||||
}
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
.agent-selector-item.selected {
|
border-radius: 50%;
|
||||||
background: rgba(0, 212, 255, 0.15);
|
flex-shrink: 0;
|
||||||
border-left: 3px solid var(--accent-cyan);
|
}
|
||||||
}
|
|
||||||
|
.agent-selector-item-status.online {
|
||||||
.agent-selector-item.local {
|
background: var(--accent-green);
|
||||||
border-left: 3px solid var(--accent-green);
|
}
|
||||||
}
|
|
||||||
|
.agent-selector-item-status.offline {
|
||||||
.agent-selector-item-status {
|
background: var(--accent-red);
|
||||||
width: 8px;
|
}
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
.agent-selector-item-info {
|
||||||
flex-shrink: 0;
|
flex: 1;
|
||||||
}
|
min-width: 0;
|
||||||
|
}
|
||||||
.agent-selector-item-status.online {
|
|
||||||
background: var(--accent-green);
|
.agent-selector-item-name {
|
||||||
}
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
.agent-selector-item-status.offline {
|
white-space: nowrap;
|
||||||
background: var(--accent-red);
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
.agent-selector-item-info {
|
|
||||||
flex: 1;
|
.agent-selector-item-url {
|
||||||
min-width: 0;
|
font-size: 10px;
|
||||||
}
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
.agent-selector-item-name {
|
white-space: nowrap;
|
||||||
font-size: 13px;
|
overflow: hidden;
|
||||||
color: var(--text-primary);
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
}
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
.agent-selector-item-check {
|
||||||
}
|
color: var(--accent-green);
|
||||||
|
opacity: 0;
|
||||||
.agent-selector-item-url {
|
}
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-secondary);
|
.agent-selector-item.selected .agent-selector-item-check {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
opacity: 1;
|
||||||
white-space: nowrap;
|
}
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
/* Agent badge in data displays */
|
||||||
}
|
.agent-badge {
|
||||||
|
display: inline-flex;
|
||||||
.agent-selector-item-check {
|
align-items: center;
|
||||||
color: var(--accent-green);
|
gap: 4px;
|
||||||
opacity: 0;
|
padding: 2px 8px;
|
||||||
}
|
font-size: 10px;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
.agent-selector-item.selected .agent-selector-item-check {
|
color: var(--accent-cyan);
|
||||||
opacity: 1;
|
border-radius: 10px;
|
||||||
}
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
/* Agent badge in data displays */
|
|
||||||
.agent-badge {
|
.agent-badge.local,
|
||||||
display: inline-flex;
|
.agent-badge.agent-local {
|
||||||
align-items: center;
|
background: rgba(0, 255, 136, 0.1);
|
||||||
gap: 4px;
|
color: var(--accent-green);
|
||||||
padding: 2px 8px;
|
}
|
||||||
font-size: 10px;
|
|
||||||
background: rgba(0, 212, 255, 0.1);
|
.agent-badge.agent-remote {
|
||||||
color: var(--accent-cyan);
|
background: rgba(0, 212, 255, 0.1);
|
||||||
border-radius: 10px;
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
}
|
||||||
}
|
|
||||||
|
/* WiFi table agent column */
|
||||||
.agent-badge.local,
|
.wifi-networks-table .col-agent {
|
||||||
.agent-badge.agent-local {
|
width: 100px;
|
||||||
background: rgba(0, 255, 136, 0.1);
|
text-align: center;
|
||||||
color: var(--accent-green);
|
}
|
||||||
}
|
|
||||||
|
.wifi-networks-table th.col-agent {
|
||||||
.agent-badge.agent-remote {
|
font-size: 10px;
|
||||||
background: rgba(0, 212, 255, 0.1);
|
}
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
/* Bluetooth table agent column */
|
||||||
|
.bt-devices-table .col-agent {
|
||||||
/* WiFi table agent column */
|
width: 100px;
|
||||||
.wifi-networks-table .col-agent {
|
text-align: center;
|
||||||
width: 100px;
|
}
|
||||||
text-align: center;
|
|
||||||
}
|
.agent-badge-dot {
|
||||||
|
width: 6px;
|
||||||
.wifi-networks-table th.col-agent {
|
height: 6px;
|
||||||
font-size: 10px;
|
border-radius: 50%;
|
||||||
}
|
background: currentColor;
|
||||||
|
}
|
||||||
/* Bluetooth table agent column */
|
|
||||||
.bt-devices-table .col-agent {
|
/* Agent column in data tables */
|
||||||
width: 100px;
|
.data-table .agent-col {
|
||||||
text-align: center;
|
width: 120px;
|
||||||
}
|
max-width: 120px;
|
||||||
|
}
|
||||||
.agent-badge-dot {
|
|
||||||
width: 6px;
|
/* Multi-agent stream indicator */
|
||||||
height: 6px;
|
.multi-agent-indicator {
|
||||||
border-radius: 50%;
|
position: fixed;
|
||||||
background: currentColor;
|
bottom: 20px;
|
||||||
}
|
left: 20px;
|
||||||
|
display: flex;
|
||||||
/* Agent column in data tables */
|
align-items: center;
|
||||||
.data-table .agent-col {
|
gap: 8px;
|
||||||
width: 120px;
|
padding: 8px 12px;
|
||||||
max-width: 120px;
|
background: var(--bg-secondary);
|
||||||
}
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 20px;
|
||||||
/* Multi-agent stream indicator */
|
font-size: 11px;
|
||||||
.multi-agent-indicator {
|
color: var(--text-secondary);
|
||||||
position: fixed;
|
z-index: 100;
|
||||||
bottom: 20px;
|
}
|
||||||
left: 20px;
|
|
||||||
display: flex;
|
.multi-agent-indicator.active {
|
||||||
align-items: center;
|
border-color: var(--accent-cyan);
|
||||||
gap: 8px;
|
color: var(--accent-cyan);
|
||||||
padding: 8px 12px;
|
}
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border-color);
|
.multi-agent-indicator-pulse {
|
||||||
border-radius: 20px;
|
width: 8px;
|
||||||
font-size: 11px;
|
height: 8px;
|
||||||
color: var(--text-secondary);
|
border-radius: 50%;
|
||||||
z-index: 100;
|
background: var(--accent-cyan);
|
||||||
}
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
.multi-agent-indicator.active {
|
|
||||||
border-color: var(--accent-cyan);
|
@keyframes pulse {
|
||||||
color: var(--accent-cyan);
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
}
|
50% { opacity: 0.5; transform: scale(0.8); }
|
||||||
|
}
|
||||||
.multi-agent-indicator-pulse {
|
|
||||||
width: 8px;
|
/* Agent connection status toast */
|
||||||
height: 8px;
|
.agent-toast {
|
||||||
border-radius: 50%;
|
position: fixed;
|
||||||
background: var(--accent-cyan);
|
top: 80px;
|
||||||
animation: pulse 2s infinite;
|
right: 20px;
|
||||||
}
|
padding: 10px 15px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
@keyframes pulse {
|
border: 1px solid var(--border-color);
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
border-radius: 6px;
|
||||||
50% { opacity: 0.5; transform: scale(0.8); }
|
font-size: 12px;
|
||||||
}
|
z-index: 1001;
|
||||||
|
animation: slideInRight 0.3s ease;
|
||||||
/* Agent connection status toast */
|
}
|
||||||
.agent-toast {
|
|
||||||
position: fixed;
|
.agent-toast.connected {
|
||||||
top: 80px;
|
border-color: var(--accent-green);
|
||||||
right: 20px;
|
color: var(--accent-green);
|
||||||
padding: 10px 15px;
|
}
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border-color);
|
.agent-toast.disconnected {
|
||||||
border-radius: 6px;
|
border-color: var(--accent-red);
|
||||||
font-size: 12px;
|
color: var(--accent-red);
|
||||||
z-index: 1001;
|
}
|
||||||
animation: slideInRight 0.3s ease;
|
|
||||||
}
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
.agent-toast.connected {
|
transform: translateX(100%);
|
||||||
border-color: var(--accent-green);
|
opacity: 0;
|
||||||
color: var(--accent-green);
|
}
|
||||||
}
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
.agent-toast.disconnected {
|
opacity: 1;
|
||||||
border-color: var(--accent-red);
|
}
|
||||||
color: var(--accent-red);
|
}
|
||||||
}
|
|
||||||
|
/* Responsive adjustments */
|
||||||
@keyframes slideInRight {
|
@media (max-width: 768px) {
|
||||||
from {
|
.agent-indicator {
|
||||||
transform: translateX(100%);
|
padding: 4px 8px;
|
||||||
opacity: 0;
|
}
|
||||||
}
|
|
||||||
to {
|
.agent-indicator-label {
|
||||||
transform: translateX(0);
|
display: none;
|
||||||
opacity: 1;
|
}
|
||||||
}
|
|
||||||
}
|
.agent-selector-dropdown {
|
||||||
|
position: fixed;
|
||||||
/* Responsive adjustments */
|
top: auto;
|
||||||
@media (max-width: 768px) {
|
bottom: 0;
|
||||||
.agent-indicator {
|
left: 0;
|
||||||
padding: 4px 8px;
|
right: 0;
|
||||||
}
|
margin: 0;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
.agent-indicator-label {
|
max-height: 60vh;
|
||||||
display: none;
|
}
|
||||||
}
|
|
||||||
|
.agents-grid {
|
||||||
.agent-selector-dropdown {
|
grid-template-columns: 1fr;
|
||||||
position: fixed;
|
}
|
||||||
top: auto;
|
}
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
max-height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agents-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -97,7 +99,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -132,10 +134,49 @@ body {
|
|||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-select-sm {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-green);
|
||||||
|
box-shadow: 0 0 6px var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .agent-status-dot.offline {
|
||||||
|
background: var(--accent-red);
|
||||||
|
box-shadow: 0 0 6px var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-selector-compact .show-all-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-link {
|
.back-link {
|
||||||
@@ -183,7 +224,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.strip-value {
|
.strip-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -287,7 +328,7 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -320,14 +361,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main dashboard grid - Mobile first */
|
/* Main dashboard grid - Mobile first */
|
||||||
|
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
|
||||||
.dashboard {
|
.dashboard {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: calc(100dvh - 95px);
|
height: calc(100dvh - 160px);
|
||||||
height: calc(100vh - 95px);
|
height: calc(100vh - 160px);
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +409,7 @@ body {
|
|||||||
/* Leaflet overrides - Dark map styling */
|
/* Leaflet overrides - Dark map styling */
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
background: var(--bg-dark) !important;
|
background: var(--bg-dark) !important;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Using actual dark tiles now - no filter needed */
|
/* Using actual dark tiles now - no filter needed */
|
||||||
@@ -438,7 +480,7 @@ body {
|
|||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: rgba(74, 158, 255, 0.05);
|
background: rgba(74, 158, 255, 0.05);
|
||||||
border-bottom: 1px solid rgba(74, 158, 255, 0.1);
|
border-bottom: 1px solid rgba(74, 158, 255, 0.1);
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -510,7 +552,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-name {
|
.vessel-name {
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -518,7 +560,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-mmsi {
|
.vessel-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
@@ -548,7 +590,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -604,20 +646,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-name {
|
.vessel-item-name {
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-type {
|
.vessel-item-type {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-speed {
|
.vessel-item-speed {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -687,7 +729,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +740,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +759,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -834,7 +876,7 @@ body {
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
min-height: calc(100dvh - 95px);
|
min-height: calc(100dvh - 160px);
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
@@ -1004,7 +1046,7 @@ body {
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-bottom: 1px solid rgba(245, 158, 11, 0.1);
|
border-bottom: 1px solid rgba(245, 158, 11, 0.1);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,7 +1121,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-category {
|
.dsc-message-category {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1096,13 +1138,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-time {
|
.dsc-message-time {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-mmsi {
|
.dsc-message-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-orange);
|
color: var(--accent-orange);
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1162,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-pos {
|
.dsc-message-pos {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -1148,7 +1190,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-distress-alert .dsc-alert-header {
|
.dsc-distress-alert .dsc-alert-header {
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
@@ -1157,7 +1199,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-distress-alert .dsc-alert-mmsi {
|
.dsc-distress-alert .dsc-alert-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -1177,7 +1219,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-distress-alert .dsc-alert-position {
|
.dsc-distress-alert .dsc-alert-position {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -1188,7 +1230,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
.radar-device {
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
transform-origin: center center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-device:hover {
|
.radar-device:hover {
|
||||||
transform: scale(1.3);
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invisible larger hit area to prevent hover flicker */
|
||||||
|
.radar-device-hitarea {
|
||||||
|
fill: transparent;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-dot-pulse circle:first-child {
|
.radar-dot-pulse circle:first-child {
|
||||||
|
|||||||
+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 */
|
/* Local font declarations for offline mode */
|
||||||
|
|
||||||
/* Inter - Primary UI font */
|
/* Space Mono - Console font */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Space Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
|
src: url('/static/vendor/fonts/SpaceMono-Regular.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Space Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
|
src: url('/static/vendor/fonts/SpaceMono-Bold.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* JetBrains Mono - Monospace/code font */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -7,6 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
/* Tactical dark palette */
|
/* Tactical dark palette */
|
||||||
--bg-primary: #0a0c10;
|
--bg-primary: #0a0c10;
|
||||||
--bg-secondary: #0f1218;
|
--bg-secondary: #0f1218;
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -259,7 +259,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -269,7 +269,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-tagline {
|
.welcome-tagline {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
letter-spacing: 0.15em;
|
letter-spacing: 0.15em;
|
||||||
@@ -278,7 +278,7 @@ body {
|
|||||||
|
|
||||||
.welcome-version {
|
.welcome-version {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: var(--bg-primary);
|
color: var(--bg-primary);
|
||||||
background: var(--accent-cyan);
|
background: var(--accent-cyan);
|
||||||
@@ -297,7 +297,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-content h2 {
|
.welcome-content h2 {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -333,14 +333,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.changelog-version {
|
.changelog-version {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelog-date {
|
.changelog-date {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.changelog-list li {
|
.changelog-list li {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
@@ -364,7 +364,7 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: -15px;
|
left: -15px;
|
||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mode Selection Grid */
|
/* Mode Selection Grid */
|
||||||
@@ -435,7 +435,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mode-card .mode-name {
|
.mode-card .mode-name {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -444,7 +444,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mode-card .mode-desc {
|
.mode-card .mode-desc {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@@ -463,7 +463,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -517,7 +517,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-footer p {
|
.welcome-footer p {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
@@ -731,7 +731,7 @@ header h1 {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -778,7 +778,7 @@ header h1 {
|
|||||||
border: 1px solid var(--accent-cyan);
|
border: 1px solid var(--accent-cyan);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -814,7 +814,7 @@ header h1 {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -922,7 +922,7 @@ header h1 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -1030,7 +1030,7 @@ header h1 {
|
|||||||
.version-badge {
|
.version-badge {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
@@ -1089,7 +1089,7 @@ header h1 .tagline {
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1578,7 +1578,7 @@ header h1 .tagline {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
@@ -1590,6 +1590,11 @@ header h1 .tagline {
|
|||||||
box-shadow: 0 0 0 2px var(--accent-cyan-dim);
|
box-shadow: 0 0 0 2px var(--accent-cyan-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure device select is wide enough for device name + serial */
|
||||||
|
#deviceSelect {
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1632,7 +1637,7 @@ header h1 .tagline {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -1652,7 +1657,7 @@ header h1 .tagline {
|
|||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1689,7 +1694,7 @@ header h1 .tagline {
|
|||||||
background: var(--accent-red);
|
background: var(--accent-red);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1752,7 +1757,7 @@ header h1 .tagline {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats>div {
|
.stats>div {
|
||||||
@@ -1778,7 +1783,7 @@ header h1 .tagline {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
min-height: 0; /* Allow shrinking in flex context */
|
min-height: 0; /* Allow shrinking in flex context */
|
||||||
@@ -1850,7 +1855,7 @@ header h1 .tagline {
|
|||||||
|
|
||||||
.message .address {
|
.message .address {
|
||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
@@ -1863,7 +1868,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message .content.numeric {
|
.message .content.numeric {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -2084,7 +2089,7 @@ header h1 .tagline {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn:hover {
|
.control-btn:hover {
|
||||||
@@ -2352,7 +2357,7 @@ header h1 .tagline {
|
|||||||
/* Dark theme for Leaflet */
|
/* Dark theme for Leaflet */
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Using actual dark tiles now - no filter needed */
|
/* Using actual dark tiles now - no filter needed */
|
||||||
@@ -2389,7 +2394,7 @@ header h1 .tagline {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-shadow: 0 0 5px var(--accent-cyan);
|
text-shadow: 0 0 5px var(--accent-cyan);
|
||||||
@@ -2406,7 +2411,7 @@ header h1 .tagline {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-shadow: 0 0 5px var(--accent-cyan);
|
text-shadow: 0 0 5px var(--accent-cyan);
|
||||||
@@ -2427,7 +2432,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-popup {
|
.aircraft-popup {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2471,7 +2476,7 @@ header h1 .tagline {
|
|||||||
background: rgba(0, 0, 0, 0.8) !important;
|
background: rgba(0, 0, 0, 0.8) !important;
|
||||||
border: 1px solid var(--accent-cyan) !important;
|
border: 1px solid var(--accent-cyan) !important;
|
||||||
color: var(--accent-cyan) !important;
|
color: var(--accent-cyan) !important;
|
||||||
font-family: 'JetBrains Mono', monospace !important;
|
font-family: var(--font-mono) !important;
|
||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
padding: 2px 6px !important;
|
padding: 2px 6px !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
@@ -2499,7 +2504,7 @@ header h1 .tagline {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -2715,7 +2720,7 @@ header h1 .tagline {
|
|||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
text-shadow: 0 0 15px var(--accent-cyan-dim);
|
text-shadow: 0 0 15px var(--accent-cyan-dim);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
@@ -3109,7 +3114,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sensor-card .data-value {
|
.sensor-card .data-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -3159,7 +3164,7 @@ header h1 .tagline {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recon-stats span {
|
.recon-stats span {
|
||||||
@@ -3209,14 +3214,14 @@ header h1 .tagline {
|
|||||||
|
|
||||||
.device-id {
|
.device-id {
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-meta {
|
.device-meta {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-meta.encrypted {
|
.device-meta.encrypted {
|
||||||
@@ -3292,7 +3297,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hex-dump {
|
.hex-dump {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
@@ -3383,7 +3388,7 @@ header h1 .tagline {
|
|||||||
/* WiFi Main Content - 3 columns */
|
/* WiFi Main Content - 3 columns */
|
||||||
.wifi-main-content {
|
.wifi-main-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr minmax(240px, 280px) minmax(240px, 280px);
|
grid-template-columns: minmax(300px, 1fr) minmax(240px, 280px) minmax(240px, 280px);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -3398,6 +3403,7 @@ header h1 .tagline {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0; /* Prevent content from forcing panel wider */
|
||||||
}
|
}
|
||||||
|
|
||||||
.wifi-networks-header {
|
.wifi-networks-header {
|
||||||
@@ -3565,6 +3571,8 @@ header h1 .tagline {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
min-width: 0; /* Prevent content from forcing panel wider */
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wifi-radar-panel h5 {
|
.wifi-radar-panel h5 {
|
||||||
@@ -3800,10 +3808,90 @@ header h1 .tagline {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wifi-client-count-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-identity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-mac {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-vendor {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-probes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-probe-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-signal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-rssi {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-client-lastseen {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
/* WiFi Responsive */
|
/* WiFi Responsive */
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1400px) {
|
||||||
.wifi-main-content {
|
.wifi-main-content {
|
||||||
grid-template-columns: 1fr 240px 240px;
|
grid-template-columns: minmax(280px, 1fr) 240px 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3961,7 +4049,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bt-detail-address {
|
.bt-detail-address {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #00d4ff;
|
color: #00d4ff;
|
||||||
}
|
}
|
||||||
@@ -3975,7 +4063,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bt-detail-rssi-value {
|
.bt-detail-rssi-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -4070,7 +4158,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bt-detail-services-list {
|
.bt-detail-services-list {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -4104,10 +4192,37 @@ header h1 .tagline {
|
|||||||
|
|
||||||
.bt-device-list {
|
.bt-device-list {
|
||||||
border-left-color: var(--accent-purple) !important;
|
border-left-color: var(--accent-purple) !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 320px;
|
||||||
|
max-height: 100%;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device-list .wifi-device-list-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bt-device-list .wifi-device-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bt-device-list .wifi-device-list-header h5 {
|
.bt-device-list .wifi-device-list-header h5 {
|
||||||
color: var(--accent-purple);
|
color: var(--accent-purple);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bluetooth Device Filters */
|
/* Bluetooth Device Filters */
|
||||||
@@ -4117,6 +4232,7 @@ header h1 .tagline {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bt-filter-btn {
|
.bt-filter-btn {
|
||||||
@@ -4289,7 +4405,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bt-rssi-value {
|
.bt-rssi-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
@@ -4648,7 +4764,7 @@ header h1 .tagline {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-legend-item {
|
.security-legend-item {
|
||||||
@@ -4695,7 +4811,7 @@ header h1 .tagline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signal-value {
|
.signal-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-shadow: 0 0 10px var(--accent-cyan-dim);
|
text-shadow: 0 0 10px var(--accent-cyan-dim);
|
||||||
@@ -4848,7 +4964,7 @@ body::before {
|
|||||||
color: #000;
|
color: #000;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 40px;
|
padding: 12px 40px;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -5211,7 +5327,7 @@ body::before {
|
|||||||
|
|
||||||
.meter-value {
|
.meter-value {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -5368,7 +5484,7 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.freq-digits {
|
.freq-digits {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 56px;
|
font-size: 56px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -5389,7 +5505,7 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.freq-unit {
|
.freq-unit {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -5533,7 +5649,7 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.knob-value {
|
.knob-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -5658,7 +5774,7 @@ body::before {
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -5720,13 +5836,13 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signal-arc-label {
|
.signal-arc-label {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
fill: var(--text-muted);
|
fill: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-arc-value {
|
.signal-arc-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
fill: var(--accent-cyan);
|
fill: var(--accent-cyan);
|
||||||
@@ -5758,7 +5874,7 @@ body::before {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -5894,7 +6010,7 @@ body::before {
|
|||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5983,7 +6099,7 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-header {
|
.module-header {
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -6008,7 +6124,7 @@ body::before {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -6047,16 +6163,27 @@ body::before {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-action-btn.scan {
|
.radio-action-btn.scan,
|
||||||
|
.radio-action-btn.listen {
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
border-color: var(--accent-green);
|
border-color: var(--accent-green);
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-action-btn.scan:hover:not(:disabled) {
|
.radio-action-btn.scan:hover:not(:disabled),
|
||||||
|
.radio-action-btn.listen:hover:not(:disabled) {
|
||||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-action-btn.listen.active {
|
||||||
|
background: var(--accent-red);
|
||||||
|
border-color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-action-btn.listen.active:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 0 20px var(--accent-red-dim);
|
||||||
|
}
|
||||||
|
|
||||||
/* Statistics Box */
|
/* Statistics Box */
|
||||||
.stat-box {
|
.stat-box {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
@@ -6066,7 +6193,7 @@ body::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -6114,7 +6241,7 @@ body::before {
|
|||||||
.tune-btn {
|
.tune-btn {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -6144,13 +6271,13 @@ body::before {
|
|||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Listening Mode Selector Buttons */
|
/* Listening Mode Selector Buttons */
|
||||||
.radio-mode-btn {
|
.radio-mode-btn {
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -6191,7 +6318,7 @@ body::before {
|
|||||||
/* Frequency Preset Buttons */
|
/* Frequency Preset Buttons */
|
||||||
.preset-freq-btn {
|
.preset-freq-btn {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -6255,4 +6382,4 @@ body::before {
|
|||||||
[data-animations="off"] .logo-dot,
|
[data-animations="off"] .logo-dot,
|
||||||
[data-animations="off"] .welcome-logo {
|
[data-animations="off"] .welcome-logo {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
.landing-title {
|
.landing-title {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.4em;
|
letter-spacing: 0.4em;
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.landing-tagline {
|
.landing-tagline {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
letter-spacing: 0.15em;
|
letter-spacing: 0.15em;
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
/* Hacker Style Error */
|
/* Hacker Style Error */
|
||||||
.flash-error {
|
.flash-error {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-sizing: border-box; /* Crucial for visibility */
|
box-sizing: border-box; /* Crucial for visibility */
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
border: 2px solid var(--accent-cyan);
|
border: 2px solid var(--accent-cyan);
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
.landing-version {
|
.landing-version {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(255, 255, 255, 0.3);
|
color: rgba(255, 255, 255, 0.3);
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
|
|||||||
+328
-328
@@ -1,328 +1,328 @@
|
|||||||
/* APRS Function Bar (Stats Strip) Styles */
|
/* APRS Function Bar (Stats Strip) Styles */
|
||||||
.aprs-strip {
|
.aprs-strip {
|
||||||
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
|
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.aprs-strip-inner {
|
.aprs-strip-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-stat {
|
.aprs-strip .strip-stat {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background: rgba(74, 158, 255, 0.05);
|
background: rgba(74, 158, 255, 0.05);
|
||||||
border: 1px solid rgba(74, 158, 255, 0.15);
|
border: 1px solid rgba(74, 158, 255, 0.15);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
min-width: 55px;
|
min-width: 55px;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-stat:hover {
|
.aprs-strip .strip-stat:hover {
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
border-color: rgba(74, 158, 255, 0.3);
|
border-color: rgba(74, 158, 255, 0.3);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-value {
|
.aprs-strip .strip-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-label {
|
.aprs-strip .strip-label {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-divider {
|
.aprs-strip .strip-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
/* Signal stat coloring */
|
/* Signal stat coloring */
|
||||||
.aprs-strip .signal-stat.good .strip-value { color: var(--accent-green); }
|
.aprs-strip .signal-stat.good .strip-value { color: var(--accent-green); }
|
||||||
.aprs-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); }
|
.aprs-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); }
|
||||||
.aprs-strip .signal-stat.poor .strip-value { color: var(--accent-red); }
|
.aprs-strip .signal-stat.poor .strip-value { color: var(--accent-red); }
|
||||||
|
|
||||||
/* Controls */
|
/* Controls */
|
||||||
.aprs-strip .strip-control {
|
.aprs-strip .strip-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-select {
|
.aprs-strip .strip-select {
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-select:hover {
|
.aprs-strip .strip-select:hover {
|
||||||
border-color: var(--accent-cyan);
|
border-color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-input-label {
|
.aprs-strip .strip-input-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-input {
|
.aprs-strip .strip-input {
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-input:hover,
|
.aprs-strip .strip-input:hover,
|
||||||
.aprs-strip .strip-input:focus {
|
.aprs-strip .strip-input:focus {
|
||||||
border-color: var(--accent-cyan);
|
border-color: var(--accent-cyan);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tool Status Indicators */
|
/* Tool Status Indicators */
|
||||||
.aprs-strip .strip-tools {
|
.aprs-strip .strip-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-tool {
|
.aprs-strip .strip-tool {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: rgba(255, 59, 48, 0.2);
|
background: rgba(255, 59, 48, 0.2);
|
||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
border: 1px solid rgba(255, 59, 48, 0.3);
|
border: 1px solid rgba(255, 59, 48, 0.3);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-tool.ok {
|
.aprs-strip .strip-tool.ok {
|
||||||
background: rgba(0, 255, 136, 0.1);
|
background: rgba(0, 255, 136, 0.1);
|
||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
border-color: rgba(0, 255, 136, 0.3);
|
border-color: rgba(0, 255, 136, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.aprs-strip .strip-btn {
|
.aprs-strip .strip-btn {
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
border: 1px solid rgba(74, 158, 255, 0.2);
|
border: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn:hover:not(:disabled) {
|
.aprs-strip .strip-btn:hover:not(:disabled) {
|
||||||
background: rgba(74, 158, 255, 0.2);
|
background: rgba(74, 158, 255, 0.2);
|
||||||
border-color: rgba(74, 158, 255, 0.4);
|
border-color: rgba(74, 158, 255, 0.4);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn.primary {
|
.aprs-strip .strip-btn.primary {
|
||||||
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
|
background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
|
||||||
border: none;
|
border: none;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn.primary:hover:not(:disabled) {
|
.aprs-strip .strip-btn.primary:hover:not(:disabled) {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn.stop {
|
.aprs-strip .strip-btn.stop {
|
||||||
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
|
background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn.stop:hover:not(:disabled) {
|
.aprs-strip .strip-btn.stop:hover:not(:disabled) {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
.aprs-strip .strip-btn:disabled {
|
.aprs-strip .strip-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status indicator */
|
/* Status indicator */
|
||||||
.aprs-strip .strip-status {
|
.aprs-strip .strip-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: rgba(0,0,0,0.2);
|
background: rgba(0,0,0,0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
.aprs-strip .status-dot {
|
.aprs-strip .status-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--text-muted);
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
.aprs-strip .status-dot.listening {
|
.aprs-strip .status-dot.listening {
|
||||||
background: var(--accent-cyan);
|
background: var(--accent-cyan);
|
||||||
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
|
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.aprs-strip .status-dot.tracking {
|
.aprs-strip .status-dot.tracking {
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
|
animation: aprs-strip-pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.aprs-strip .status-dot.error {
|
.aprs-strip .status-dot.error {
|
||||||
background: var(--accent-red);
|
background: var(--accent-red);
|
||||||
}
|
}
|
||||||
@keyframes aprs-strip-pulse {
|
@keyframes aprs-strip-pulse {
|
||||||
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
|
0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
|
||||||
50% { opacity: 0.6; box-shadow: none; }
|
50% { opacity: 0.6; box-shadow: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Time display */
|
/* Time display */
|
||||||
.aprs-strip .strip-time {
|
.aprs-strip .strip-time {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background: rgba(0,0,0,0.2);
|
background: rgba(0,0,0,0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* APRS Status Bar Styles (Sidebar - legacy) */
|
/* APRS Status Bar Styles (Sidebar - legacy) */
|
||||||
.aprs-status-bar {
|
.aprs-status-bar {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.aprs-status-indicator {
|
.aprs-status-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.aprs-status-dot {
|
.aprs-status-dot {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--text-muted);
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
.aprs-status-dot.standby { background: var(--text-muted); }
|
.aprs-status-dot.standby { background: var(--text-muted); }
|
||||||
.aprs-status-dot.listening {
|
.aprs-status-dot.listening {
|
||||||
background: var(--accent-cyan);
|
background: var(--accent-cyan);
|
||||||
animation: aprs-pulse 1.5s ease-in-out infinite;
|
animation: aprs-pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.aprs-status-dot.tracking { background: var(--accent-green); }
|
.aprs-status-dot.tracking { background: var(--accent-green); }
|
||||||
.aprs-status-dot.error { background: var(--accent-red); }
|
.aprs-status-dot.error { background: var(--accent-red); }
|
||||||
@keyframes aprs-pulse {
|
@keyframes aprs-pulse {
|
||||||
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
|
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
|
||||||
50% { opacity: 0.6; box-shadow: 0 0 8px 4px rgba(74, 158, 255, 0.3); }
|
50% { opacity: 0.6; box-shadow: 0 0 8px 4px rgba(74, 158, 255, 0.3); }
|
||||||
}
|
}
|
||||||
.aprs-status-text {
|
.aprs-status-text {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
.aprs-status-stats {
|
.aprs-status-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
.aprs-stat {
|
.aprs-stat {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
.aprs-stat-label {
|
.aprs-stat-label {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Signal Meter Styles */
|
/* Signal Meter Styles */
|
||||||
.aprs-signal-meter {
|
.aprs-signal-meter {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.aprs-meter-header {
|
.aprs-meter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.aprs-meter-label {
|
.aprs-meter-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
.aprs-meter-value {
|
.aprs-meter-value {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
}
|
}
|
||||||
.aprs-meter-burst {
|
.aprs-meter-burst {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--accent-yellow);
|
color: var(--accent-yellow);
|
||||||
background: rgba(255, 193, 7, 0.2);
|
background: rgba(255, 193, 7, 0.2);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
animation: burst-flash 0.3s ease-out;
|
animation: burst-flash 0.3s ease-out;
|
||||||
}
|
}
|
||||||
@keyframes burst-flash {
|
@keyframes burst-flash {
|
||||||
0% { opacity: 1; transform: scale(1.1); }
|
0% { opacity: 1; transform: scale(1.1); }
|
||||||
100% { opacity: 1; transform: scale(1); }
|
100% { opacity: 1; transform: scale(1); }
|
||||||
}
|
}
|
||||||
.aprs-meter-bar-container {
|
.aprs-meter-bar-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background: rgba(0,0,0,0.4);
|
background: rgba(0,0,0,0.4);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.aprs-meter-bar {
|
.aprs-meter-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
var(--accent-green) 0%,
|
var(--accent-green) 0%,
|
||||||
var(--accent-cyan) 50%,
|
var(--accent-cyan) 50%,
|
||||||
var(--accent-yellow) 75%,
|
var(--accent-yellow) 75%,
|
||||||
var(--accent-red) 100%
|
var(--accent-red) 100%
|
||||||
);
|
);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: width 0.1s ease-out;
|
transition: width 0.1s ease-out;
|
||||||
}
|
}
|
||||||
.aprs-meter-bar.no-signal {
|
.aprs-meter-bar.no-signal {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.aprs-meter-ticks {
|
.aprs-meter-ticks {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
.aprs-meter-status {
|
.aprs-meter-status {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
.aprs-meter-status.active {
|
.aprs-meter-status.active {
|
||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
}
|
}
|
||||||
.aprs-meter-status.no-signal {
|
.aprs-meter-status.no-signal {
|
||||||
color: var(--accent-yellow);
|
color: var(--accent-yellow);
|
||||||
}
|
}
|
||||||
|
|||||||
+1610
-1610
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-stations-title {
|
.spy-stations-title {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-station-name {
|
.spy-station-name {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
/* Type Badge */
|
/* Type Badge */
|
||||||
.spy-station-badge {
|
.spy-station-badge {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-meta-mode {
|
.spy-meta-mode {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--accent-orange);
|
color: var(--accent-orange);
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-freq-list {
|
.spy-freq-list {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-freq-item {
|
.spy-freq-item {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spy-freq-select {
|
.spy-freq-select {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
+876
-876
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 {
|
:root {
|
||||||
|
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -93,7 +95,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
@@ -142,7 +144,7 @@ body {
|
|||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +164,45 @@ body {
|
|||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-selector .location-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-selector .location-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-selector .location-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-green);
|
||||||
|
box-shadow: 0 0 6px var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-selector .location-status-dot.offline {
|
||||||
|
background: var(--accent-red);
|
||||||
|
box-shadow: 0 0 6px var(--accent-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item {
|
.status-item {
|
||||||
@@ -211,6 +248,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main dashboard grid */
|
/* Main dashboard grid */
|
||||||
|
/* Header ~52px + Nav 44px = ~96px, using 100px for safety */
|
||||||
.dashboard {
|
.dashboard {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -218,7 +256,7 @@ body {
|
|||||||
grid-template-columns: 1fr 1fr 340px;
|
grid-template-columns: 1fr 1fr 340px;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: calc(100vh - 60px);
|
height: calc(100vh - 100px);
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +495,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-value {
|
.telemetry-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -543,7 +581,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pass-time {
|
.pass-time {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom controls bar */
|
/* Bottom controls bar */
|
||||||
@@ -579,7 +617,7 @@ body {
|
|||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +734,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.polar-container,
|
.polar-container,
|
||||||
@@ -748,4 +786,4 @@ body.embedded .panel {
|
|||||||
|
|
||||||
body.embedded .controls-bar {
|
body.embedded .controls-bar {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
+444
-427
@@ -1,427 +1,444 @@
|
|||||||
/* Settings Modal Styles */
|
/* Settings Modal Styles */
|
||||||
|
|
||||||
.settings-modal {
|
.settings-modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-modal.active {
|
.settings-modal.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-content {
|
.settings-content {
|
||||||
background: var(--bg-dark, #0a0a0f);
|
background: var(--bg-dark, #0a0a0f);
|
||||||
border: 1px solid var(--border-color, #1a1a2e);
|
border: 1px solid var(--border-color, #1a1a2e);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header h2 {
|
.settings-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header h2 .icon {
|
.settings-header h2 .icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-close {
|
.settings-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-close:hover {
|
.settings-close:hover {
|
||||||
color: var(--accent-red, #ff4444);
|
color: var(--accent-red, #ff4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Tabs */
|
/* Settings Tabs */
|
||||||
.settings-tabs {
|
.settings-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab {
|
.settings-tab {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab:hover {
|
.settings-tab:hover {
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab.active {
|
.settings-tab.active {
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab.active::after {
|
.settings-tab.active::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--accent-cyan, #00d4ff);
|
background: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Sections */
|
/* Settings Sections */
|
||||||
.settings-section {
|
.settings-section {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section.active {
|
.settings-section.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group {
|
.settings-group {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group:last-child {
|
.settings-group:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group-title {
|
.settings-group-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Row */
|
/* Settings Row */
|
||||||
.settings-row {
|
.settings-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-row:last-child {
|
.settings-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label {
|
.settings-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label-text {
|
.settings-label-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label-desc {
|
.settings-label-desc {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle Switch */
|
/* Toggle Switch */
|
||||||
.toggle-switch {
|
.toggle-switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input {
|
.toggle-switch input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-slider {
|
.toggle-slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: var(--bg-tertiary, #1a1a2e);
|
background-color: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-slider:before {
|
.toggle-slider:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
background-color: var(--text-muted, #666);
|
background-color: var(--text-muted, #666);
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:checked + .toggle-slider {
|
.toggle-switch input:checked + .toggle-slider {
|
||||||
background-color: var(--accent-cyan, #00d4ff);
|
background-color: var(--accent-cyan, #00d4ff);
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:checked + .toggle-slider:before {
|
.toggle-switch input:checked + .toggle-slider:before {
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:focus + .toggle-slider {
|
.toggle-switch input:focus + .toggle-slider {
|
||||||
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
|
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Dropdown */
|
/* Select Dropdown */
|
||||||
.settings-select {
|
.settings-select {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 8px center;
|
background-position: right 8px center;
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-select:focus {
|
.settings-select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
.settings-input {
|
.settings-input {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-input:focus {
|
.settings-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-input::placeholder {
|
.settings-input::placeholder {
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Asset Status */
|
/* Asset Status */
|
||||||
.asset-status {
|
.asset-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--bg-secondary, #0f0f1a);
|
background: var(--bg-secondary, #0f0f1a);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-status-row {
|
.asset-status-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-name {
|
.asset-name {
|
||||||
color: var(--text-muted, #888);
|
color: var(--text-muted, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge {
|
.asset-badge {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.available {
|
.asset-badge.available {
|
||||||
background: rgba(0, 255, 136, 0.15);
|
background: rgba(0, 255, 136, 0.15);
|
||||||
color: var(--accent-green, #00ff88);
|
color: var(--accent-green, #00ff88);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.missing {
|
.asset-badge.missing {
|
||||||
background: rgba(255, 68, 68, 0.15);
|
background: rgba(255, 68, 68, 0.15);
|
||||||
color: var(--accent-red, #ff4444);
|
color: var(--accent-red, #ff4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.checking {
|
.asset-badge.checking {
|
||||||
background: rgba(255, 170, 0, 0.15);
|
background: rgba(255, 170, 0, 0.15);
|
||||||
color: var(--accent-orange, #ffaa00);
|
color: var(--accent-orange, #ffaa00);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check Assets Button */
|
/* Check Assets Button */
|
||||||
.check-assets-btn {
|
.check-assets-btn {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-assets-btn:hover {
|
.check-assets-btn:hover {
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-assets-btn:disabled {
|
.check-assets-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* About Section */
|
/* GPS Detection Spinner */
|
||||||
.about-info {
|
.detecting-spinner {
|
||||||
font-size: 13px;
|
display: inline-block;
|
||||||
color: var(--text-muted, #888);
|
width: 12px;
|
||||||
line-height: 1.6;
|
height: 12px;
|
||||||
}
|
border: 2px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
.about-info p {
|
border-radius: 50%;
|
||||||
margin: 0 0 12px 0;
|
animation: detecting-spin 0.8s linear infinite;
|
||||||
}
|
vertical-align: middle;
|
||||||
|
margin-right: 6px;
|
||||||
.about-info a {
|
}
|
||||||
color: var(--accent-cyan, #00d4ff);
|
|
||||||
text-decoration: none;
|
@keyframes detecting-spin {
|
||||||
}
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
.about-info a:hover {
|
|
||||||
text-decoration: underline;
|
/* About Section */
|
||||||
}
|
.about-info {
|
||||||
|
font-size: 13px;
|
||||||
.about-version {
|
color: var(--text-muted, #888);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
line-height: 1.6;
|
||||||
color: var(--accent-cyan, #00d4ff);
|
}
|
||||||
}
|
|
||||||
|
.about-info p {
|
||||||
/* Donate Button */
|
margin: 0 0 12px 0;
|
||||||
.donate-btn {
|
}
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
.about-info a {
|
||||||
justify-content: center;
|
color: var(--accent-cyan, #00d4ff);
|
||||||
padding: 10px 20px;
|
text-decoration: none;
|
||||||
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
|
}
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
.about-info a:hover {
|
||||||
color: #000;
|
text-decoration: underline;
|
||||||
font-size: 13px;
|
}
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
.about-version {
|
||||||
cursor: pointer;
|
font-family: var(--font-mono);
|
||||||
transition: all 0.2s ease;
|
color: var(--accent-cyan, #00d4ff);
|
||||||
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
|
}
|
||||||
}
|
|
||||||
|
/* Donate Button */
|
||||||
.donate-btn:hover {
|
.donate-btn {
|
||||||
transform: translateY(-1px);
|
display: inline-flex;
|
||||||
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
|
align-items: center;
|
||||||
filter: brightness(1.1);
|
justify-content: center;
|
||||||
}
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
|
||||||
.donate-btn:active {
|
border: none;
|
||||||
transform: translateY(0);
|
border-radius: 6px;
|
||||||
}
|
color: #000;
|
||||||
|
font-size: 13px;
|
||||||
/* Tile Provider Custom URL */
|
font-weight: 600;
|
||||||
.custom-url-row {
|
text-decoration: none;
|
||||||
margin-top: 8px;
|
cursor: pointer;
|
||||||
padding-top: 8px;
|
transition: all 0.2s ease;
|
||||||
}
|
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
|
||||||
|
}
|
||||||
.custom-url-row .settings-input {
|
|
||||||
width: 100%;
|
.donate-btn:hover {
|
||||||
}
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
|
||||||
/* Info Callout */
|
filter: brightness(1.1);
|
||||||
.settings-info {
|
}
|
||||||
background: rgba(0, 212, 255, 0.1);
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
.donate-btn:active {
|
||||||
border-radius: 6px;
|
transform: translateY(0);
|
||||||
padding: 12px;
|
}
|
||||||
margin-top: 16px;
|
|
||||||
font-size: 12px;
|
/* Tile Provider Custom URL */
|
||||||
color: var(--text-muted, #888);
|
.custom-url-row {
|
||||||
}
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
.settings-info strong {
|
}
|
||||||
color: var(--accent-cyan, #00d4ff);
|
|
||||||
}
|
.custom-url-row .settings-input {
|
||||||
|
width: 100%;
|
||||||
/* Responsive */
|
}
|
||||||
@media (max-width: 640px) {
|
|
||||||
.settings-modal.active {
|
/* Info Callout */
|
||||||
padding: 20px 10px;
|
.settings-info {
|
||||||
}
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
.settings-content {
|
border-radius: 6px;
|
||||||
max-width: 100%;
|
padding: 12px;
|
||||||
}
|
margin-top: 16px;
|
||||||
|
font-size: 12px;
|
||||||
.settings-row {
|
color: var(--text-muted, #888);
|
||||||
flex-direction: column;
|
}
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
.settings-info strong {
|
||||||
}
|
color: var(--accent-cyan, #00d4ff);
|
||||||
|
}
|
||||||
.settings-select,
|
|
||||||
.settings-input {
|
/* Responsive */
|
||||||
width: 100%;
|
@media (max-width: 640px) {
|
||||||
}
|
.settings-modal.active {
|
||||||
}
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select,
|
||||||
|
.settings-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -207,9 +207,14 @@ const ProximityRadar = (function() {
|
|||||||
const pulseClass = isNew ? 'radar-dot-pulse' : '';
|
const pulseClass = isNew ? 'radar-dot-pulse' : '';
|
||||||
const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey;
|
const isSelected = selectedDeviceKey && device.device_key === selectedDeviceKey;
|
||||||
|
|
||||||
|
// Hit area size (prevents hover flicker when scaling)
|
||||||
|
const hitAreaSize = Math.max(dotSize * 2, 15);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<g class="radar-device ${pulseClass}${isSelected ? ' selected' : ''}" data-device-key="${escapeAttr(device.device_key)}"
|
<g class="radar-device ${pulseClass}${isSelected ? ' selected' : ''}" data-device-key="${escapeAttr(device.device_key)}"
|
||||||
transform="translate(${x}, ${y})" style="cursor: pointer;">
|
transform="translate(${x}, ${y})" style="cursor: pointer;">
|
||||||
|
<!-- Invisible hit area to prevent hover flicker -->
|
||||||
|
<circle class="radar-device-hitarea" r="${hitAreaSize}" fill="transparent" />
|
||||||
${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
|
${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
|
||||||
<animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/>
|
<animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/>
|
||||||
<animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/>
|
<animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/>
|
||||||
|
|||||||
+494
-494
@@ -1,36 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* Intercept - Core Application Logic
|
* Intercept - Core Application Logic
|
||||||
* Global state, mode switching, and shared functionality
|
* Global state, mode switching, and shared functionality
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============== GLOBAL STATE ==============
|
// ============== GLOBAL STATE ==============
|
||||||
|
|
||||||
// Mode state flags
|
// Mode state flags
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isSensorRunning = false;
|
let isSensorRunning = false;
|
||||||
let isAdsbRunning = false;
|
let isAdsbRunning = false;
|
||||||
let isWifiRunning = false;
|
let isWifiRunning = false;
|
||||||
let isBtRunning = false;
|
let isBtRunning = false;
|
||||||
let currentMode = 'pager';
|
let currentMode = 'pager';
|
||||||
|
|
||||||
// Message counters
|
// Message counters
|
||||||
let msgCount = 0;
|
let msgCount = 0;
|
||||||
let pocsagCount = 0;
|
let pocsagCount = 0;
|
||||||
let flexCount = 0;
|
let flexCount = 0;
|
||||||
let sensorCount = 0;
|
let sensorCount = 0;
|
||||||
let filteredCount = 0;
|
let filteredCount = 0;
|
||||||
|
|
||||||
// Device list (populated from server via Jinja2)
|
// Device list (populated from server via Jinja2)
|
||||||
let deviceList = [];
|
let deviceList = [];
|
||||||
|
|
||||||
// Auto-scroll setting
|
// Auto-scroll setting
|
||||||
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
|
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
|
||||||
|
|
||||||
// Mute setting
|
// Mute setting
|
||||||
let muted = localStorage.getItem('audioMuted') === 'true';
|
let muted = localStorage.getItem('audioMuted') === 'true';
|
||||||
|
|
||||||
// Observer location (load from localStorage or default to London)
|
// Observer location (load from localStorage or default to London)
|
||||||
let observerLocation = (function() {
|
let observerLocation = (function() {
|
||||||
if (window.ObserverLocation && ObserverLocation.getForModule) {
|
if (window.ObserverLocation && ObserverLocation.getForModule) {
|
||||||
return ObserverLocation.getForModule('observerLocation');
|
return ObserverLocation.getForModule('observerLocation');
|
||||||
@@ -44,464 +44,464 @@ let observerLocation = (function() {
|
|||||||
}
|
}
|
||||||
return { lat: 51.5074, lon: -0.1278 };
|
return { lat: 51.5074, lon: -0.1278 };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Message storage for export
|
// Message storage for export
|
||||||
let allMessages = [];
|
let allMessages = [];
|
||||||
|
|
||||||
// Track unique sensor devices
|
// Track unique sensor devices
|
||||||
let uniqueDevices = new Set();
|
let uniqueDevices = new Set();
|
||||||
|
|
||||||
// SDR device usage tracking
|
// SDR device usage tracking
|
||||||
let sdrDeviceUsage = {};
|
let sdrDeviceUsage = {};
|
||||||
|
|
||||||
// ============== DISCLAIMER HANDLING ==============
|
// ============== DISCLAIMER HANDLING ==============
|
||||||
|
|
||||||
function checkDisclaimer() {
|
function checkDisclaimer() {
|
||||||
const accepted = localStorage.getItem('disclaimerAccepted');
|
const accepted = localStorage.getItem('disclaimerAccepted');
|
||||||
if (accepted === 'true') {
|
if (accepted === 'true') {
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function acceptDisclaimer() {
|
function acceptDisclaimer() {
|
||||||
localStorage.setItem('disclaimerAccepted', 'true');
|
localStorage.setItem('disclaimerAccepted', 'true');
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function declineDisclaimer() {
|
function declineDisclaimer() {
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
|
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== HEADER CLOCK ==============
|
// ============== HEADER CLOCK ==============
|
||||||
|
|
||||||
function updateHeaderClock() {
|
function updateHeaderClock() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const utc = now.toISOString().substring(11, 19);
|
const utc = now.toISOString().substring(11, 19);
|
||||||
document.getElementById('headerUtcTime').textContent = utc;
|
document.getElementById('headerUtcTime').textContent = utc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== MODE SWITCHING ==============
|
// ============== MODE SWITCHING ==============
|
||||||
|
|
||||||
function switchMode(mode) {
|
function switchMode(mode) {
|
||||||
// Stop any running scans when switching modes
|
// Stop any running scans when switching modes
|
||||||
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
|
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
|
||||||
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
|
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
|
||||||
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
|
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
|
||||||
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
|
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
|
||||||
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
|
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
|
||||||
|
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
|
|
||||||
// Remove active from all nav buttons, then add to the correct one
|
// Remove active from all nav buttons, then add to the correct one
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
const modeMap = {
|
const modeMap = {
|
||||||
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
||||||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||||||
'listening': 'listening', 'meshtastic': 'meshtastic'
|
'listening': 'listening', 'meshtastic': 'meshtastic'
|
||||||
};
|
};
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||||
const label = btn.querySelector('.nav-label');
|
const label = btn.querySelector('.nav-label');
|
||||||
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
|
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle mode content visibility
|
// Toggle mode content visibility
|
||||||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||||||
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
||||||
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
|
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
|
||||||
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
||||||
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
||||||
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
||||||
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
||||||
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
||||||
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
||||||
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
|
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
|
||||||
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
||||||
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
|
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
|
||||||
|
|
||||||
// Toggle stats visibility
|
// Toggle stats visibility
|
||||||
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
||||||
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
||||||
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
||||||
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
||||||
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
|
|
||||||
// Hide signal meter - individual panels show signal strength where needed
|
// Hide signal meter - individual panels show signal strength where needed
|
||||||
document.getElementById('signalMeter').style.display = 'none';
|
document.getElementById('signalMeter').style.display = 'none';
|
||||||
|
|
||||||
// Show/hide dashboard buttons in nav bar
|
// Show/hide dashboard buttons in nav bar
|
||||||
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
|
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
|
||||||
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
|
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
|
||||||
|
|
||||||
// Update active mode indicator
|
// Update active mode indicator
|
||||||
const modeNames = {
|
const modeNames = {
|
||||||
'pager': 'PAGER',
|
'pager': 'PAGER',
|
||||||
'sensor': '433MHZ',
|
'sensor': '433MHZ',
|
||||||
'aircraft': 'AIRCRAFT',
|
'aircraft': 'AIRCRAFT',
|
||||||
'satellite': 'SATELLITE',
|
'satellite': 'SATELLITE',
|
||||||
'wifi': 'WIFI',
|
'wifi': 'WIFI',
|
||||||
'bluetooth': 'BLUETOOTH',
|
'bluetooth': 'BLUETOOTH',
|
||||||
'listening': 'LISTENING POST',
|
'listening': 'LISTENING POST',
|
||||||
'tscm': 'TSCM',
|
'tscm': 'TSCM',
|
||||||
'aprs': 'APRS',
|
'aprs': 'APRS',
|
||||||
'meshtastic': 'MESHTASTIC'
|
'meshtastic': 'MESHTASTIC'
|
||||||
};
|
};
|
||||||
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
||||||
|
|
||||||
// Update mobile nav buttons
|
// Update mobile nav buttons
|
||||||
updateMobileNavButtons(mode);
|
updateMobileNavButtons(mode);
|
||||||
|
|
||||||
// Close mobile drawer when mode is switched (on mobile)
|
// Close mobile drawer when mode is switched (on mobile)
|
||||||
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
|
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
|
||||||
window.closeMobileDrawer();
|
window.closeMobileDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle layout containers
|
// Toggle layout containers
|
||||||
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||||||
|
|
||||||
// Respect the "Show Radar Display" checkbox for aircraft mode
|
// Respect the "Show Radar Display" checkbox for aircraft mode
|
||||||
const showRadar = document.getElementById('adsbEnableMap')?.checked;
|
const showRadar = document.getElementById('adsbEnableMap')?.checked;
|
||||||
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
||||||
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
|
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
|
||||||
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
|
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
|
||||||
|
|
||||||
// Update output panel title based on mode
|
// Update output panel title based on mode
|
||||||
const titles = {
|
const titles = {
|
||||||
'pager': 'Pager Decoder',
|
'pager': 'Pager Decoder',
|
||||||
'sensor': '433MHz Sensor Monitor',
|
'sensor': '433MHz Sensor Monitor',
|
||||||
'aircraft': 'ADS-B Aircraft Tracker',
|
'aircraft': 'ADS-B Aircraft Tracker',
|
||||||
'satellite': 'Satellite Monitor',
|
'satellite': 'Satellite Monitor',
|
||||||
'wifi': 'WiFi Scanner',
|
'wifi': 'WiFi Scanner',
|
||||||
'bluetooth': 'Bluetooth Scanner',
|
'bluetooth': 'Bluetooth Scanner',
|
||||||
'listening': 'Listening Post',
|
'listening': 'Listening Post',
|
||||||
'meshtastic': 'Meshtastic Mesh Monitor'
|
'meshtastic': 'Meshtastic Mesh Monitor'
|
||||||
};
|
};
|
||||||
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
||||||
|
|
||||||
// Show/hide Device Intelligence for modes that use it
|
// Show/hide Device Intelligence for modes that use it
|
||||||
const reconBtn = document.getElementById('reconBtn');
|
const reconBtn = document.getElementById('reconBtn');
|
||||||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||||||
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
|
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
|
||||||
document.getElementById('reconPanel').style.display = 'none';
|
document.getElementById('reconPanel').style.display = 'none';
|
||||||
if (reconBtn) reconBtn.style.display = 'none';
|
if (reconBtn) reconBtn.style.display = 'none';
|
||||||
if (intelBtn) intelBtn.style.display = 'none';
|
if (intelBtn) intelBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
if (reconBtn) reconBtn.style.display = 'inline-block';
|
if (reconBtn) reconBtn.style.display = 'inline-block';
|
||||||
if (intelBtn) intelBtn.style.display = 'inline-block';
|
if (intelBtn) intelBtn.style.display = 'inline-block';
|
||||||
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
|
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
|
||||||
document.getElementById('reconPanel').style.display = 'block';
|
document.getElementById('reconPanel').style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show RTL-SDR device section for modes that use it
|
// Show RTL-SDR device section for modes that use it
|
||||||
document.getElementById('rtlDeviceSection').style.display =
|
document.getElementById('rtlDeviceSection').style.display =
|
||||||
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
||||||
|
|
||||||
// Toggle mode-specific tool status displays
|
// Toggle mode-specific tool status displays
|
||||||
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
||||||
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
|
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
|
||||||
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
|
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
|
||||||
|
|
||||||
// Hide waterfall and output console for modes with their own visualizations
|
// Hide waterfall and output console for modes with their own visualizations
|
||||||
document.querySelector('.waterfall-container').style.display =
|
document.querySelector('.waterfall-container').style.display =
|
||||||
(mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
(mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
||||||
document.getElementById('output').style.display =
|
document.getElementById('output').style.display =
|
||||||
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
||||||
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
|
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
|
||||||
|
|
||||||
// Load interfaces and initialize visualizations when switching modes
|
// Load interfaces and initialize visualizations when switching modes
|
||||||
if (mode === 'wifi') {
|
if (mode === 'wifi') {
|
||||||
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
|
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
|
||||||
if (typeof initRadar === 'function') initRadar();
|
if (typeof initRadar === 'function') initRadar();
|
||||||
if (typeof initWatchList === 'function') initWatchList();
|
if (typeof initWatchList === 'function') initWatchList();
|
||||||
} else if (mode === 'bluetooth') {
|
} else if (mode === 'bluetooth') {
|
||||||
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
|
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
|
||||||
if (typeof initBtRadar === 'function') initBtRadar();
|
if (typeof initBtRadar === 'function') initBtRadar();
|
||||||
} else if (mode === 'aircraft') {
|
} else if (mode === 'aircraft') {
|
||||||
if (typeof checkAdsbTools === 'function') checkAdsbTools();
|
if (typeof checkAdsbTools === 'function') checkAdsbTools();
|
||||||
if (typeof initAircraftRadar === 'function') initAircraftRadar();
|
if (typeof initAircraftRadar === 'function') initAircraftRadar();
|
||||||
} else if (mode === 'satellite') {
|
} else if (mode === 'satellite') {
|
||||||
if (typeof initPolarPlot === 'function') initPolarPlot();
|
if (typeof initPolarPlot === 'function') initPolarPlot();
|
||||||
if (typeof initSatelliteList === 'function') initSatelliteList();
|
if (typeof initSatelliteList === 'function') initSatelliteList();
|
||||||
} else if (mode === 'listening') {
|
} else if (mode === 'listening') {
|
||||||
if (typeof checkScannerTools === 'function') checkScannerTools();
|
if (typeof checkScannerTools === 'function') checkScannerTools();
|
||||||
if (typeof checkAudioTools === 'function') checkAudioTools();
|
if (typeof checkAudioTools === 'function') checkAudioTools();
|
||||||
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
|
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
|
||||||
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
|
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
|
||||||
} else if (mode === 'meshtastic') {
|
} else if (mode === 'meshtastic') {
|
||||||
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
|
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== SECTION COLLAPSE ==============
|
// ============== SECTION COLLAPSE ==============
|
||||||
|
|
||||||
function toggleSection(el) {
|
function toggleSection(el) {
|
||||||
el.closest('.section').classList.toggle('collapsed');
|
el.closest('.section').classList.toggle('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== THEME MANAGEMENT ==============
|
// ============== THEME MANAGEMENT ==============
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
const currentTheme = html.getAttribute('data-theme');
|
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
html.setAttribute('data-theme', newTheme);
|
html.setAttribute('data-theme', newTheme);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('intercept-theme', newTheme);
|
||||||
|
|
||||||
// Update button text
|
// Update button text
|
||||||
const btn = document.getElementById('themeToggle');
|
const btn = document.getElementById('themeToggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTheme() {
|
function loadTheme() {
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
const savedTheme = localStorage.getItem('intercept-theme') || 'dark';
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
const btn = document.getElementById('themeToggle');
|
const btn = document.getElementById('themeToggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
|
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== AUTO-SCROLL ==============
|
// ============== AUTO-SCROLL ==============
|
||||||
|
|
||||||
function toggleAutoScroll() {
|
function toggleAutoScroll() {
|
||||||
autoScroll = !autoScroll;
|
autoScroll = !autoScroll;
|
||||||
localStorage.setItem('autoScroll', autoScroll);
|
localStorage.setItem('autoScroll', autoScroll);
|
||||||
updateAutoScrollButton();
|
updateAutoScrollButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAutoScrollButton() {
|
function updateAutoScrollButton() {
|
||||||
const btn = document.getElementById('autoScrollBtn');
|
const btn = document.getElementById('autoScrollBtn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
|
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
|
||||||
btn.classList.toggle('active', autoScroll);
|
btn.classList.toggle('active', autoScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== SDR DEVICE MANAGEMENT ==============
|
// ============== SDR DEVICE MANAGEMENT ==============
|
||||||
|
|
||||||
function getSelectedDevice() {
|
function getSelectedDevice() {
|
||||||
return document.getElementById('deviceSelect').value;
|
return document.getElementById('deviceSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedSDRType() {
|
function getSelectedSDRType() {
|
||||||
return document.getElementById('sdrTypeSelect').value;
|
return document.getElementById('sdrTypeSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reserveDevice(deviceIndex, modeId) {
|
function reserveDevice(deviceIndex, modeId) {
|
||||||
sdrDeviceUsage[modeId] = deviceIndex;
|
sdrDeviceUsage[modeId] = deviceIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseDevice(modeId) {
|
function releaseDevice(modeId) {
|
||||||
delete sdrDeviceUsage[modeId];
|
delete sdrDeviceUsage[modeId];
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDeviceAvailability(requestingMode) {
|
function checkDeviceAvailability(requestingMode) {
|
||||||
const selectedDevice = parseInt(getSelectedDevice());
|
const selectedDevice = parseInt(getSelectedDevice());
|
||||||
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
|
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
|
||||||
if (mode !== requestingMode && device === selectedDevice) {
|
if (mode !== requestingMode && device === selectedDevice) {
|
||||||
alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`);
|
alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== BIAS-T SETTINGS ==============
|
// ============== BIAS-T SETTINGS ==============
|
||||||
|
|
||||||
function saveBiasTSetting() {
|
function saveBiasTSetting() {
|
||||||
const enabled = document.getElementById('biasT')?.checked || false;
|
const enabled = document.getElementById('biasT')?.checked || false;
|
||||||
localStorage.setItem('biasTEnabled', enabled);
|
localStorage.setItem('biasTEnabled', enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBiasTEnabled() {
|
function getBiasTEnabled() {
|
||||||
return document.getElementById('biasT')?.checked || false;
|
return document.getElementById('biasT')?.checked || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBiasTSetting() {
|
function loadBiasTSetting() {
|
||||||
const saved = localStorage.getItem('biasTEnabled');
|
const saved = localStorage.getItem('biasTEnabled');
|
||||||
if (saved === 'true') {
|
if (saved === 'true') {
|
||||||
const checkbox = document.getElementById('biasT');
|
const checkbox = document.getElementById('biasT');
|
||||||
if (checkbox) checkbox.checked = true;
|
if (checkbox) checkbox.checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== REMOTE SDR ==============
|
// ============== REMOTE SDR ==============
|
||||||
|
|
||||||
function toggleRemoteSDR() {
|
function toggleRemoteSDR() {
|
||||||
const useRemote = document.getElementById('useRemoteSDR').checked;
|
const useRemote = document.getElementById('useRemoteSDR').checked;
|
||||||
const configDiv = document.getElementById('remoteSDRConfig');
|
const configDiv = document.getElementById('remoteSDRConfig');
|
||||||
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
|
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
|
||||||
|
|
||||||
if (useRemote) {
|
if (useRemote) {
|
||||||
configDiv.style.display = 'block';
|
configDiv.style.display = 'block';
|
||||||
localControls.forEach(el => el.disabled = true);
|
localControls.forEach(el => el.disabled = true);
|
||||||
} else {
|
} else {
|
||||||
configDiv.style.display = 'none';
|
configDiv.style.display = 'none';
|
||||||
localControls.forEach(el => el.disabled = false);
|
localControls.forEach(el => el.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteSDRConfig() {
|
function getRemoteSDRConfig() {
|
||||||
const useRemote = document.getElementById('useRemoteSDR')?.checked;
|
const useRemote = document.getElementById('useRemoteSDR')?.checked;
|
||||||
if (!useRemote) return null;
|
if (!useRemote) return null;
|
||||||
|
|
||||||
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
|
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
|
||||||
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
|
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
|
||||||
|
|
||||||
if (!host || isNaN(port)) {
|
if (!host || isNaN(port)) {
|
||||||
alert('Please enter valid rtl_tcp host and port');
|
alert('Please enter valid rtl_tcp host and port');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { host, port };
|
return { host, port };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== OUTPUT DISPLAY ==============
|
// ============== OUTPUT DISPLAY ==============
|
||||||
|
|
||||||
function showInfo(text) {
|
function showInfo(text) {
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
if (!output) return;
|
if (!output) return;
|
||||||
|
|
||||||
const placeholder = output.querySelector('.placeholder');
|
const placeholder = output.querySelector('.placeholder');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
const infoEl = document.createElement('div');
|
const infoEl = document.createElement('div');
|
||||||
infoEl.className = 'info-msg';
|
infoEl.className = 'info-msg';
|
||||||
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
||||||
infoEl.textContent = text;
|
infoEl.textContent = text;
|
||||||
output.insertBefore(infoEl, output.firstChild);
|
output.insertBefore(infoEl, output.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(text) {
|
function showError(text) {
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
if (!output) return;
|
if (!output) return;
|
||||||
|
|
||||||
const placeholder = output.querySelector('.placeholder');
|
const placeholder = output.querySelector('.placeholder');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
const errorEl = document.createElement('div');
|
const errorEl = document.createElement('div');
|
||||||
errorEl.className = 'error-msg';
|
errorEl.className = 'error-msg';
|
||||||
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
||||||
errorEl.textContent = '⚠ ' + text;
|
errorEl.textContent = '⚠ ' + text;
|
||||||
output.insertBefore(errorEl, output.firstChild);
|
output.insertBefore(errorEl, output.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== INITIALIZATION ==============
|
// ============== INITIALIZATION ==============
|
||||||
|
|
||||||
// ============== MOBILE NAVIGATION ==============
|
// ============== MOBILE NAVIGATION ==============
|
||||||
|
|
||||||
function initMobileNav() {
|
function initMobileNav() {
|
||||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||||
const sidebar = document.getElementById('mainSidebar');
|
const sidebar = document.getElementById('mainSidebar');
|
||||||
const overlay = document.getElementById('drawerOverlay');
|
const overlay = document.getElementById('drawerOverlay');
|
||||||
|
|
||||||
if (!hamburgerBtn || !sidebar || !overlay) return;
|
if (!hamburgerBtn || !sidebar || !overlay) return;
|
||||||
|
|
||||||
function openDrawer() {
|
function openDrawer() {
|
||||||
sidebar.classList.add('open');
|
sidebar.classList.add('open');
|
||||||
overlay.classList.add('visible');
|
overlay.classList.add('visible');
|
||||||
hamburgerBtn.classList.add('active');
|
hamburgerBtn.classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
overlay.classList.remove('visible');
|
overlay.classList.remove('visible');
|
||||||
hamburgerBtn.classList.remove('active');
|
hamburgerBtn.classList.remove('active');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDrawer() {
|
function toggleDrawer() {
|
||||||
if (sidebar.classList.contains('open')) {
|
if (sidebar.classList.contains('open')) {
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
} else {
|
} else {
|
||||||
openDrawer();
|
openDrawer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hamburgerBtn.addEventListener('click', toggleDrawer);
|
hamburgerBtn.addEventListener('click', toggleDrawer);
|
||||||
overlay.addEventListener('click', closeDrawer);
|
overlay.addEventListener('click', closeDrawer);
|
||||||
|
|
||||||
// Close drawer when resizing to desktop
|
// Close drawer when resizing to desktop
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (window.innerWidth >= 1024) {
|
if (window.innerWidth >= 1024) {
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose for external use
|
// Expose for external use
|
||||||
window.toggleMobileDrawer = toggleDrawer;
|
window.toggleMobileDrawer = toggleDrawer;
|
||||||
window.closeMobileDrawer = closeDrawer;
|
window.closeMobileDrawer = closeDrawer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewportHeight() {
|
function setViewportHeight() {
|
||||||
// Fix for iOS Safari address bar height
|
// Fix for iOS Safari address bar height
|
||||||
const vh = window.innerHeight * 0.01;
|
const vh = window.innerHeight * 0.01;
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMobileNavButtons(mode) {
|
function updateMobileNavButtons(mode) {
|
||||||
// Update mobile nav bar buttons
|
// Update mobile nav bar buttons
|
||||||
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
|
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
|
||||||
const btnMode = btn.getAttribute('data-mode');
|
const btnMode = btn.getAttribute('data-mode');
|
||||||
btn.classList.toggle('active', btnMode === mode);
|
btn.classList.toggle('active', btnMode === mode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initApp() {
|
function initApp() {
|
||||||
// Check disclaimer
|
// Check disclaimer
|
||||||
checkDisclaimer();
|
checkDisclaimer();
|
||||||
|
|
||||||
// Load theme
|
// Load theme
|
||||||
loadTheme();
|
loadTheme();
|
||||||
|
|
||||||
// Start clock
|
// Start clock
|
||||||
updateHeaderClock();
|
updateHeaderClock();
|
||||||
setInterval(updateHeaderClock, 1000);
|
setInterval(updateHeaderClock, 1000);
|
||||||
|
|
||||||
// Load bias-T setting
|
// Load bias-T setting
|
||||||
loadBiasTSetting();
|
loadBiasTSetting();
|
||||||
|
|
||||||
// Initialize observer location inputs
|
// Initialize observer location inputs
|
||||||
const adsbLatInput = document.getElementById('adsbObsLat');
|
const adsbLatInput = document.getElementById('adsbObsLat');
|
||||||
const adsbLonInput = document.getElementById('adsbObsLon');
|
const adsbLonInput = document.getElementById('adsbObsLon');
|
||||||
const obsLatInput = document.getElementById('obsLat');
|
const obsLatInput = document.getElementById('obsLat');
|
||||||
const obsLonInput = document.getElementById('obsLon');
|
const obsLonInput = document.getElementById('obsLon');
|
||||||
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
|
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
|
||||||
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
|
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
|
||||||
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
|
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
|
||||||
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
|
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
|
||||||
|
|
||||||
// Update UI state
|
// Update UI state
|
||||||
updateAutoScrollButton();
|
updateAutoScrollButton();
|
||||||
|
|
||||||
// Make sections collapsible
|
// Make sections collapsible
|
||||||
document.querySelectorAll('.section h3').forEach(h3 => {
|
document.querySelectorAll('.section h3').forEach(h3 => {
|
||||||
h3.addEventListener('click', function() {
|
h3.addEventListener('click', function() {
|
||||||
this.parentElement.classList.toggle('collapsed');
|
this.parentElement.classList.toggle('collapsed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collapse all sections by default (except SDR Device which is first)
|
// Collapse all sections by default (except SDR Device which is first)
|
||||||
document.querySelectorAll('.section').forEach((section, index) => {
|
document.querySelectorAll('.section').forEach((section, index) => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
section.classList.add('collapsed');
|
section.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize mobile navigation
|
// Initialize mobile navigation
|
||||||
initMobileNav();
|
initMobileNav();
|
||||||
|
|
||||||
// Set viewport height for mobile browsers
|
// Set viewport height for mobile browsers
|
||||||
setViewportHeight();
|
setViewportHeight();
|
||||||
window.addEventListener('resize', setViewportHeight);
|
window.addEventListener('resize', setViewportHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run initialization when DOM is ready
|
// Run initialization when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initApp);
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
|
|||||||
@@ -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
|
* Initialize the Leaflet map
|
||||||
*/
|
*/
|
||||||
function initMap() {
|
async function initMap() {
|
||||||
if (meshMap) return;
|
if (meshMap) return;
|
||||||
|
|
||||||
const mapContainer = document.getElementById('meshMap');
|
const mapContainer = document.getElementById('meshMap');
|
||||||
@@ -111,7 +111,9 @@ const Meshtastic = (function() {
|
|||||||
window.meshMap = meshMap;
|
window.meshMap = meshMap;
|
||||||
|
|
||||||
// Use settings manager for tile layer (allows runtime changes)
|
// Use settings manager for tile layer (allows runtime changes)
|
||||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
if (typeof Settings !== 'undefined') {
|
||||||
|
// Wait for settings to load from server before applying tiles
|
||||||
|
await Settings.init();
|
||||||
Settings.createTileLayer().addTo(meshMap);
|
Settings.createTileLayer().addTo(meshMap);
|
||||||
Settings.registerMap(meshMap);
|
Settings.registerMap(meshMap);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const SpyStations = (function() {
|
|||||||
modeContainer.innerHTML = modes.map(m => `
|
modeContainer.innerHTML = modes.map(m => `
|
||||||
<label class="inline-checkbox">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
|
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
|
||||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 10px;">${m}</span>
|
<span style="font-family: 'Space Mono', monospace; font-size: 10px;">${m}</span>
|
||||||
</label>
|
</label>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-35
@@ -37,20 +37,20 @@ const SSTV = (function() {
|
|||||||
/**
|
/**
|
||||||
* Load location into input fields
|
* Load location into input fields
|
||||||
*/
|
*/
|
||||||
function loadLocationInputs() {
|
function loadLocationInputs() {
|
||||||
const latInput = document.getElementById('sstvObsLat');
|
const latInput = document.getElementById('sstvObsLat');
|
||||||
const lonInput = document.getElementById('sstvObsLon');
|
const lonInput = document.getElementById('sstvObsLon');
|
||||||
|
|
||||||
let storedLat = localStorage.getItem('observerLat');
|
let storedLat = localStorage.getItem('observerLat');
|
||||||
let storedLon = localStorage.getItem('observerLon');
|
let storedLon = localStorage.getItem('observerLon');
|
||||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||||
const shared = ObserverLocation.getShared();
|
const shared = ObserverLocation.getShared();
|
||||||
storedLat = shared.lat.toString();
|
storedLat = shared.lat.toString();
|
||||||
storedLon = shared.lon.toString();
|
storedLon = shared.lon.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latInput && storedLat) latInput.value = storedLat;
|
if (latInput && storedLat) latInput.value = storedLat;
|
||||||
if (lonInput && storedLon) lonInput.value = storedLon;
|
if (lonInput && storedLon) lonInput.value = storedLon;
|
||||||
|
|
||||||
// Add change handlers to save and refresh
|
// Add change handlers to save and refresh
|
||||||
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
if (latInput) latInput.addEventListener('change', saveLocationFromInputs);
|
||||||
@@ -60,23 +60,23 @@ const SSTV = (function() {
|
|||||||
/**
|
/**
|
||||||
* Save location from input fields
|
* Save location from input fields
|
||||||
*/
|
*/
|
||||||
function saveLocationFromInputs() {
|
function saveLocationFromInputs() {
|
||||||
const latInput = document.getElementById('sstvObsLat');
|
const latInput = document.getElementById('sstvObsLat');
|
||||||
const lonInput = document.getElementById('sstvObsLon');
|
const lonInput = document.getElementById('sstvObsLon');
|
||||||
|
|
||||||
const lat = parseFloat(latInput?.value);
|
const lat = parseFloat(latInput?.value);
|
||||||
const lon = parseFloat(lonInput?.value);
|
const lon = parseFloat(lonInput?.value);
|
||||||
|
|
||||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 &&
|
if (!isNaN(lat) && lat >= -90 && lat <= 90 &&
|
||||||
!isNaN(lon) && lon >= -180 && lon <= 180) {
|
!isNaN(lon) && lon >= -180 && lon <= 180) {
|
||||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||||
ObserverLocation.setShared({ lat, lon });
|
ObserverLocation.setShared({ lat, lon });
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('observerLat', lat.toString());
|
localStorage.setItem('observerLat', lat.toString());
|
||||||
localStorage.setItem('observerLon', lon.toString());
|
localStorage.setItem('observerLon', lon.toString());
|
||||||
}
|
}
|
||||||
loadIssSchedule(); // Refresh pass predictions
|
loadIssSchedule(); // Refresh pass predictions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,12 +103,12 @@ const SSTV = (function() {
|
|||||||
if (latInput) latInput.value = lat;
|
if (latInput) latInput.value = lat;
|
||||||
if (lonInput) lonInput.value = lon;
|
if (lonInput) lonInput.value = lon;
|
||||||
|
|
||||||
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
||||||
ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) });
|
ObserverLocation.setShared({ lat: parseFloat(lat), lon: parseFloat(lon) });
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('observerLat', lat);
|
localStorage.setItem('observerLat', lat);
|
||||||
localStorage.setItem('observerLon', lon);
|
localStorage.setItem('observerLon', lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -159,7 +159,7 @@ const SSTV = (function() {
|
|||||||
/**
|
/**
|
||||||
* Initialize Leaflet map for ISS tracking
|
* Initialize Leaflet map for ISS tracking
|
||||||
*/
|
*/
|
||||||
function initMap() {
|
async function initMap() {
|
||||||
const mapContainer = document.getElementById('sstvIssMap');
|
const mapContainer = document.getElementById('sstvIssMap');
|
||||||
if (!mapContainer || issMap) return;
|
if (!mapContainer || issMap) return;
|
||||||
|
|
||||||
@@ -173,10 +173,14 @@ const SSTV = (function() {
|
|||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
worldCopyJump: true
|
worldCopyJump: true
|
||||||
});
|
});
|
||||||
|
window.issMap = issMap;
|
||||||
|
|
||||||
// Add tile layer using settings manager if available
|
// Add tile layer using settings manager if available
|
||||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
if (typeof Settings !== 'undefined') {
|
||||||
|
// Wait for settings to load from server before applying tiles
|
||||||
|
await Settings.init();
|
||||||
Settings.createTileLayer().addTo(issMap);
|
Settings.createTileLayer().addTo(issMap);
|
||||||
|
Settings.registerMap(issMap);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to dark theme tiles
|
// Fallback to dark theme tiles
|
||||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
|||||||
@@ -887,6 +887,9 @@ const WiFiMode = (function() {
|
|||||||
clients.set(client.mac, client);
|
clients.set(client.mac, client);
|
||||||
updateStats();
|
updateStats();
|
||||||
|
|
||||||
|
// Update client display if this client belongs to the selected network
|
||||||
|
updateClientInList(client);
|
||||||
|
|
||||||
if (onClientUpdate) onClientUpdate(client);
|
if (onClientUpdate) onClientUpdate(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1135,6 +1138,9 @@ const WiFiMode = (function() {
|
|||||||
|
|
||||||
// Show the drawer
|
// Show the drawer
|
||||||
elements.detailDrawer.classList.add('open');
|
elements.detailDrawer.classList.add('open');
|
||||||
|
|
||||||
|
// Fetch and display clients for this network
|
||||||
|
fetchClientsForNetwork(network.bssid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
@@ -1147,6 +1153,130 @@ const WiFiMode = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Client Display
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
async function fetchClientsForNetwork(bssid) {
|
||||||
|
if (!elements.detailClientList) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (isAgentMode) {
|
||||||
|
// Route through agent proxy
|
||||||
|
response = await fetch(`/controller/agents/${currentAgent}/wifi/v2/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||||
|
} else {
|
||||||
|
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// Hide client list on error
|
||||||
|
elements.detailClientList.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// Handle agent response format (may be nested in 'result')
|
||||||
|
const result = isAgentMode && data.result ? data.result : data;
|
||||||
|
const clientList = result.clients || [];
|
||||||
|
|
||||||
|
if (clientList.length > 0) {
|
||||||
|
renderClientList(clientList, bssid);
|
||||||
|
elements.detailClientList.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
elements.detailClientList.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||||
|
elements.detailClientList.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderClientList(clientList, bssid) {
|
||||||
|
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||||
|
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Update count badge
|
||||||
|
if (countBadge) {
|
||||||
|
countBadge.textContent = clientList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render client cards
|
||||||
|
container.innerHTML = clientList.map(client => {
|
||||||
|
const rssi = client.rssi_current;
|
||||||
|
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||||
|
rssi >= -70 ? 'signal-medium' :
|
||||||
|
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||||
|
|
||||||
|
// Format last seen time
|
||||||
|
const lastSeen = client.last_seen ? formatTime(client.last_seen) : '--';
|
||||||
|
|
||||||
|
// Build probed SSIDs badges
|
||||||
|
let probesHtml = '';
|
||||||
|
if (client.probed_ssids && client.probed_ssids.length > 0) {
|
||||||
|
const probes = client.probed_ssids.slice(0, 5); // Show max 5
|
||||||
|
probesHtml = `
|
||||||
|
<div class="wifi-client-probes">
|
||||||
|
${probes.map(ssid => `<span class="wifi-client-probe-badge">${escapeHtml(ssid)}</span>`).join('')}
|
||||||
|
${client.probed_ssids.length > 5 ? `<span class="wifi-client-probe-badge">+${client.probed_ssids.length - 5}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="wifi-client-card" data-mac="${escapeHtml(client.mac)}">
|
||||||
|
<div class="wifi-client-identity">
|
||||||
|
<span class="wifi-client-mac">${escapeHtml(client.mac)}</span>
|
||||||
|
<span class="wifi-client-vendor">${escapeHtml(client.vendor || 'Unknown vendor')}</span>
|
||||||
|
${probesHtml}
|
||||||
|
</div>
|
||||||
|
<div class="wifi-client-signal">
|
||||||
|
<span class="wifi-client-rssi ${signalClass}">${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'}</span>
|
||||||
|
<span class="wifi-client-lastseen">${lastSeen}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClientInList(client) {
|
||||||
|
// Check if this client belongs to the currently selected network
|
||||||
|
if (!selectedNetwork || client.associated_bssid !== selectedNetwork) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const existingCard = container.querySelector(`[data-mac="${client.mac}"]`);
|
||||||
|
|
||||||
|
if (existingCard) {
|
||||||
|
// Update existing card's RSSI and last seen
|
||||||
|
const rssiEl = existingCard.querySelector('.wifi-client-rssi');
|
||||||
|
const lastSeenEl = existingCard.querySelector('.wifi-client-lastseen');
|
||||||
|
|
||||||
|
if (rssiEl && client.rssi_current !== null && client.rssi_current !== undefined) {
|
||||||
|
const rssi = client.rssi_current;
|
||||||
|
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||||
|
rssi >= -70 ? 'signal-medium' :
|
||||||
|
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||||
|
rssiEl.textContent = rssi + ' dBm';
|
||||||
|
rssiEl.className = 'wifi-client-rssi ' + signalClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSeenEl && client.last_seen) {
|
||||||
|
lastSeenEl.textContent = formatTime(client.last_seen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New client for this network - re-fetch the full list
|
||||||
|
fetchClientsForNetwork(selectedNetwork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Statistics
|
// Statistics
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
+4916
-4899
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,15 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ADS-B History // INTERCEPT</title>
|
<title>ADS-B History // INTERCEPT</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
|
{% else %}
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -22,6 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% set active_mode = 'adsb' %}
|
||||||
|
{% include 'partials/nav.html' with context %}
|
||||||
|
|
||||||
<main class="history-shell">
|
<main class="history-shell">
|
||||||
<section class="summary-strip">
|
<section class="summary-strip">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
@@ -462,7 +472,7 @@
|
|||||||
|
|
||||||
if (!points.length) {
|
if (!points.length) {
|
||||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||||
ctx.font = '12px "JetBrains Mono", monospace';
|
ctx.font = '12px "Space Mono", monospace';
|
||||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -470,7 +480,7 @@
|
|||||||
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
|
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
|
||||||
if (!series.length) {
|
if (!series.length) {
|
||||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||||
ctx.font = '12px "JetBrains Mono", monospace';
|
ctx.font = '12px "Space Mono", monospace';
|
||||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -511,7 +521,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
|
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
|
||||||
ctx.font = '11px "JetBrains Mono", monospace';
|
ctx.font = '11px "Space Mono", monospace';
|
||||||
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
|
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
|
||||||
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
|
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
|
||||||
}
|
}
|
||||||
@@ -761,5 +771,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
{% include 'partials/settings-modal.html' %}
|
||||||
|
|
||||||
|
<!-- Help Modal -->
|
||||||
|
{% include 'partials/help-modal.html' %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+568
-588
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
|||||||
{% if offline_settings.fonts_source == 'local' %}
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Leaflet.js - Conditional CDN/Local loading -->
|
<!-- Leaflet.js - Conditional CDN/Local loading -->
|
||||||
{% if offline_settings.assets_source == 'local' %}
|
{% if offline_settings.assets_source == 'local' %}
|
||||||
@@ -18,8 +18,13 @@
|
|||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!-- Core CSS variables -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
|
||||||
<script>
|
<script>
|
||||||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||||||
</script>
|
</script>
|
||||||
@@ -46,11 +51,12 @@
|
|||||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
|
||||||
<a href="/" class="back-link">Main Dashboard</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% set active_mode = 'ais' %}
|
||||||
|
{% include 'partials/nav.html' with context %}
|
||||||
|
|
||||||
<div class="stats-strip">
|
<div class="stats-strip">
|
||||||
<div class="stats-strip-inner">
|
<div class="stats-strip-inner">
|
||||||
<div class="strip-stat">
|
<div class="strip-stat">
|
||||||
@@ -384,7 +390,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initialize map
|
// Initialize map
|
||||||
function initMap() {
|
async function initMap() {
|
||||||
if (observerLocation) {
|
if (observerLocation) {
|
||||||
document.getElementById('obsLat').value = observerLocation.lat;
|
document.getElementById('obsLat').value = observerLocation.lat;
|
||||||
document.getElementById('obsLon').value = observerLocation.lon;
|
document.getElementById('obsLon').value = observerLocation.lon;
|
||||||
@@ -398,7 +404,9 @@
|
|||||||
|
|
||||||
// Use settings manager for tile layer (allows runtime changes)
|
// Use settings manager for tile layer (allows runtime changes)
|
||||||
window.vesselMap = vesselMap;
|
window.vesselMap = vesselMap;
|
||||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
if (typeof Settings !== 'undefined') {
|
||||||
|
// Wait for settings to load from server before applying tiles
|
||||||
|
await Settings.init();
|
||||||
Settings.createTileLayer().addTo(vesselMap);
|
Settings.createTileLayer().addTo(vesselMap);
|
||||||
Settings.registerMap(vesselMap);
|
Settings.registerMap(vesselMap);
|
||||||
} else {
|
} else {
|
||||||
@@ -1495,7 +1503,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.agent-select-sm:focus {
|
.agent-select-sm:focus {
|
||||||
@@ -1547,8 +1555,17 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
{% include 'partials/settings-modal.html' %}
|
||||||
|
|
||||||
|
<!-- Help Modal -->
|
||||||
|
{% include 'partials/help-modal.html' %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
|
||||||
|
|
||||||
<!-- Agent Manager -->
|
<!-- Agent Manager -->
|
||||||
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
// AIS-specific agent integration
|
// AIS-specific agent integration
|
||||||
let aisCurrentAgent = 'local';
|
let aisCurrentAgent = 'local';
|
||||||
|
|||||||
@@ -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' %}
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
|
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
|
||||||
{% if offline_settings.assets_source == 'local' %}
|
{% if offline_settings.assets_source == 'local' %}
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
|
||||||
@@ -290,7 +291,7 @@
|
|||||||
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
|
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
|
||||||
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
|
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
|
||||||
<p
|
<p
|
||||||
style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #888; text-align: left; margin: 0;">
|
style="font-family: var(--font-mono); font-size: 11px; color: #888; text-align: left; margin: 0;">
|
||||||
<span style="color: var(--accent-red);">root@intercepted:</span><span
|
<span style="color: var(--accent-red);">root@intercepted:</span><span
|
||||||
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
|
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
|
||||||
<span style="color: #666;">[sudo] password for user: ********</span><br>
|
<span style="color: #666;">[sudo] password for user: ********</span><br>
|
||||||
@@ -348,107 +349,9 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mode Navigation Bar -->
|
<!-- Mode Navigation Bar -->
|
||||||
<nav class="mode-nav">
|
{% set is_index_page = true %}
|
||||||
<div class="mode-nav-dropdown" data-group="sdr">
|
{% set active_mode = 'pager' %}
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('sdr')">
|
{% include 'partials/nav.html' with context %}
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
|
|
||||||
<span class="nav-label">SDR / RF</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn active" onclick="switchMode('pager')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="nav-label">Pager</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span><span class="nav-label">433MHz</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span><span class="nav-label">Meters</span></button>
|
|
||||||
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span><span class="nav-label">Aircraft</span></a>
|
|
||||||
<a href="/ais/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span><span class="nav-label">Vessels</span></a>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span><span class="nav-label">APRS</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="nav-label">Listening Post</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('spystations')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span><span class="nav-label">Spy Stations</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('meshtastic')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span><span class="nav-label">Meshtastic</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="wireless">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('wireless')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
|
||||||
<span class="nav-label">Wireless</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('wifi')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span><span class="nav-label">WiFi</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('bluetooth')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span><span class="nav-label">Bluetooth</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="security">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('security')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
|
||||||
<span class="nav-label">Security</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('tscm')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="nav-label">TSCM</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="space">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('space')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
|
|
||||||
<span class="nav-label">Space</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="nav-label">Satellite</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('sstv')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span><span class="nav-label">ISS SSTV</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-actions">
|
|
||||||
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn"
|
|
||||||
style="display: none;">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span><span class="nav-label">Full Dashboard</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-utilities">
|
|
||||||
<div class="nav-clock">
|
|
||||||
<span class="utc-label">UTC</span>
|
|
||||||
<span class="utc-time" id="headerUtcTime">--:--:--</span>
|
|
||||||
</div>
|
|
||||||
<div class="nav-divider"></div>
|
|
||||||
<div class="nav-tools">
|
|
||||||
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
|
|
||||||
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
|
|
||||||
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
|
|
||||||
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
|
|
||||||
<span class="icon-sun icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span></a>
|
|
||||||
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span></a>
|
|
||||||
<button class="nav-tool-btn" onclick="showSettings()" title="Settings"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span></button>
|
|
||||||
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="nav-tool-btn nav-tool-btn--donate" title="Support the Project"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg></span></a>
|
|
||||||
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
|
||||||
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
|
|
||||||
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile Navigation Bar (simplified mode switching) -->
|
|
||||||
<nav class="mobile-nav-bar" id="mobileNavBar">
|
|
||||||
<button class="mobile-nav-btn active" data-mode="pager" onclick="switchMode('pager')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span> Pager</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span> 433MHz</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Meters</button>
|
|
||||||
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span> Aircraft</a>
|
|
||||||
<a href="/ais/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span> Vessels</a>
|
|
||||||
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span> APRS</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> WiFi</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="bluetooth" onclick="switchMode('bluetooth')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span> BT</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="tscm" onclick="switchMode('tscm')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span> TSCM</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="satellite" onclick="switchMode('satellite')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg></span> Sat</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="sstv" onclick="switchMode('sstv')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg></span> SSTV</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="listening" onclick="switchMode('listening')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span> Scanner</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="spystations" onclick="switchMode('spystations')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span> Spy</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="meshtastic" onclick="switchMode('meshtastic')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span> Mesh</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile Drawer Overlay -->
|
<!-- Mobile Drawer Overlay -->
|
||||||
<div class="drawer-overlay" id="drawerOverlay"></div>
|
<div class="drawer-overlay" id="drawerOverlay"></div>
|
||||||
@@ -508,8 +411,7 @@
|
|||||||
{% if devices %}
|
{% if devices %}
|
||||||
{% for device in devices %}
|
{% for device in devices %}
|
||||||
<option value="{{ device.index }}"
|
<option value="{{ device.index }}"
|
||||||
data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{
|
data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}{% if device.serial and device.serial != 'N/A' and device.serial != 'Unknown' %} (SN: {{ device.serial }}){% endif %}</option>
|
||||||
device.name }}</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="0">No devices found</option>
|
<option value="0">No devices found</option>
|
||||||
@@ -806,7 +708,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
|
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
|
||||||
<h6>Connected Clients</h6>
|
<h6>Connected Clients <span class="wifi-client-count-badge" id="wifiClientCountBadge"></span></h6>
|
||||||
<div class="wifi-client-list"></div>
|
<div class="wifi-client-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1088,7 +990,7 @@
|
|||||||
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
|
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
|
||||||
PACKET LOG</h5>
|
PACKET LOG</h5>
|
||||||
<div id="aprsPacketLog"
|
<div id="aprsPacketLog"
|
||||||
style="flex: 1; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
|
style="flex: 1; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
|
||||||
<div style="color: var(--text-muted);">Waiting for packets...</div>
|
<div style="color: var(--text-muted);">Waiting for packets...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1108,7 +1010,7 @@
|
|||||||
STOPPED</div>
|
STOPPED</div>
|
||||||
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
|
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
|
||||||
<div class="freq-digits" id="mainScannerFreq"
|
<div class="freq-digits" id="mainScannerFreq"
|
||||||
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: 'JetBrains Mono', monospace; letter-spacing: 3px;">
|
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: var(--font-mono); letter-spacing: 3px;">
|
||||||
118.000</div>
|
118.000</div>
|
||||||
<span class="freq-unit"
|
<span class="freq-unit"
|
||||||
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
|
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
|
||||||
@@ -1154,6 +1056,10 @@
|
|||||||
Audio Output</div>
|
Audio Output</div>
|
||||||
</div>
|
</div>
|
||||||
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
|
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
|
||||||
|
<button id="audioUnlockBtn" type="button"
|
||||||
|
style="display: none; margin-top: 8px; width: 100%; padding: 6px 10px; font-size: 10px; letter-spacing: 1px; background: var(--accent-cyan); color: #000; border: 1px solid var(--accent-cyan); border-radius: 4px; cursor: pointer;">
|
||||||
|
CLICK TO ENABLE AUDIO
|
||||||
|
</button>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
|
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
|
||||||
<div
|
<div
|
||||||
@@ -1166,6 +1072,13 @@
|
|||||||
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
|
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
|
||||||
dB</span>
|
dB</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
|
<span style="font-size: 7px; color: var(--text-muted); letter-spacing: 1px;">SNR THRESH</span>
|
||||||
|
<input type="range" id="snrThresholdSlider" min="6" max="20" step="1" value="8"
|
||||||
|
style="flex: 1;" />
|
||||||
|
<span id="snrThresholdValue"
|
||||||
|
style="font-size: 8px; color: var(--text-muted); min-width: 26px; text-align: right;">8</span>
|
||||||
|
</div>
|
||||||
<!-- Signal Alert inline -->
|
<!-- Signal Alert inline -->
|
||||||
<div id="mainSignalAlert"
|
<div id="mainSignalAlert"
|
||||||
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
|
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
|
||||||
@@ -1260,10 +1173,10 @@
|
|||||||
<!-- SQL, Gain, Vol Knobs -->
|
<!-- SQL, Gain, Vol Knobs -->
|
||||||
<div style="display: flex; gap: 15px;">
|
<div style="display: flex; gap: 15px;">
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="radio-knob" id="radioSquelchKnob" data-value="30" data-min="0"
|
<div class="radio-knob" id="radioSquelchKnob" data-value="0" data-min="0"
|
||||||
data-max="100" data-step="1"></div>
|
data-max="100" data-step="1"></div>
|
||||||
<div class="knob-label">SQL</div>
|
<div class="knob-label">SQL</div>
|
||||||
<div class="knob-value" id="radioSquelchValue">30</div>
|
<div class="knob-value" id="radioSquelchValue">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
|
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
|
||||||
@@ -1336,7 +1249,7 @@
|
|||||||
START</div>
|
START</div>
|
||||||
<input type="number" id="radioScanStart" value="118" step="0.1"
|
<input type="number" id="radioScanStart" value="118" step="0.1"
|
||||||
class="radio-input"
|
class="radio-input"
|
||||||
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);">
|
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
|
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
|
||||||
@@ -1345,15 +1258,17 @@
|
|||||||
END</div>
|
END</div>
|
||||||
<input type="number" id="radioScanEnd" value="137" step="0.1"
|
<input type="number" id="radioScanEnd" value="137" step="0.1"
|
||||||
class="radio-input"
|
class="radio-input"
|
||||||
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);">
|
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()"
|
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()">
|
||||||
style="padding: 12px; font-size: 13px; width: 100%; font-weight: bold;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN</button>
|
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN
|
||||||
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()"
|
</button>
|
||||||
style="padding: 10px; font-size: 12px; width: 100%; background: var(--accent-green); border: none; color: #fff;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN</button>
|
<button class="radio-action-btn listen" id="radioListenBtn" onclick="toggleDirectListen()">
|
||||||
|
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1411,19 +1326,19 @@
|
|||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
|
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainSignalCount">0</span>
|
id="mainSignalCount">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
|
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainFreqsScanned">0</span>
|
id="mainFreqsScanned">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
|
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainScanCycles">0</span>
|
id="mainScanCycles">0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2118,7 +2033,7 @@
|
|||||||
// After fade out, hide welcome and switch to mode
|
// After fade out, hide welcome and switch to mode
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
welcome.style.display = 'none';
|
welcome.style.display = 'none';
|
||||||
switchMode(mode);
|
switchMode(mode, { updateUrl: true });
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2127,6 +2042,53 @@
|
|||||||
document.getElementById('disclaimerModal').style.display = 'flex';
|
document.getElementById('disclaimerModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mode from query string (e.g., /?mode=wifi)
|
||||||
|
let pendingStartMode = null;
|
||||||
|
const validModes = new Set([
|
||||||
|
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
||||||
|
'spystations', 'meshtastic', 'wifi', 'bluetooth',
|
||||||
|
'tscm', 'satellite', 'sstv'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getModeFromQuery() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const mode = params.get('mode');
|
||||||
|
if (!mode || !validModes.has(mode)) return null;
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyModeFromQuery() {
|
||||||
|
const mode = getModeFromQuery();
|
||||||
|
if (!mode) return;
|
||||||
|
const accepted = localStorage.getItem('disclaimerAccepted') === 'true';
|
||||||
|
if (accepted) {
|
||||||
|
const welcome = document.getElementById('welcomePage');
|
||||||
|
if (welcome) welcome.style.display = 'none';
|
||||||
|
switchMode(mode, { updateUrl: false });
|
||||||
|
updateModeUrl(mode, true);
|
||||||
|
} else {
|
||||||
|
pendingStartMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySettingsFromQuery() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('settings') === '1') {
|
||||||
|
// Remove settings param from URL to avoid reopening on refresh
|
||||||
|
params.delete('settings');
|
||||||
|
const newUrl = params.toString()
|
||||||
|
? window.location.pathname + '?' + params.toString()
|
||||||
|
: window.location.pathname;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
// Open settings modal after a brief delay to ensure page is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof showSettings === 'function') {
|
||||||
|
showSettings();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function acceptDisclaimer() {
|
function acceptDisclaimer() {
|
||||||
localStorage.setItem('disclaimerAccepted', 'true');
|
localStorage.setItem('disclaimerAccepted', 'true');
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
@@ -2138,7 +2100,14 @@
|
|||||||
const gateStyle = document.getElementById('disclaimer-gate');
|
const gateStyle = document.getElementById('disclaimer-gate');
|
||||||
if (gateStyle) gateStyle.remove();
|
if (gateStyle) gateStyle.remove();
|
||||||
// Ensure welcome page is visible
|
// Ensure welcome page is visible
|
||||||
document.getElementById('welcomePage').style.display = '';
|
const welcome = document.getElementById('welcomePage');
|
||||||
|
if (welcome) welcome.style.display = '';
|
||||||
|
if (pendingStartMode) {
|
||||||
|
// Bypass welcome and jump to requested mode
|
||||||
|
welcome.style.display = 'none';
|
||||||
|
switchMode(pendingStartMode, { updateUrl: true });
|
||||||
|
pendingStartMode = null;
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2457,6 +2426,12 @@
|
|||||||
|
|
||||||
// Start SDR device status polling
|
// Start SDR device status polling
|
||||||
startSdrStatusPolling();
|
startSdrStatusPolling();
|
||||||
|
|
||||||
|
// Apply mode from URL query (e.g., /?mode=wifi)
|
||||||
|
applyModeFromQuery();
|
||||||
|
|
||||||
|
// Check for settings=1 query param (from dashboard settings button)
|
||||||
|
applySettingsFromQuery();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle section collapse
|
// Toggle section collapse
|
||||||
@@ -2512,8 +2487,20 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateModeUrl(mode, replace = false) {
|
||||||
|
if (!validModes.has(mode)) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('mode', mode);
|
||||||
|
if (replace) {
|
||||||
|
window.history.replaceState({ mode }, '', url);
|
||||||
|
} else {
|
||||||
|
window.history.pushState({ mode }, '', url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mode switching
|
// Mode switching
|
||||||
function switchMode(mode) {
|
function switchMode(mode, options = {}) {
|
||||||
|
const { updateUrl = true } = options;
|
||||||
// Only stop local scans if in local mode (not agent mode)
|
// Only stop local scans if in local mode (not agent mode)
|
||||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
if (!isAgentMode) {
|
if (!isAgentMode) {
|
||||||
@@ -2526,6 +2513,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
|
if (updateUrl) {
|
||||||
|
updateModeUrl(mode);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync mode state with current agent/local after switching
|
// Sync mode state with current agent/local after switching
|
||||||
if (isAgentMode && typeof syncAgentModeStates === 'function') {
|
if (isAgentMode && typeof syncAgentModeStates === 'function') {
|
||||||
@@ -2758,6 +2748,13 @@
|
|||||||
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
|
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', function () {
|
||||||
|
const mode = getModeFromQuery();
|
||||||
|
if (mode && mode !== currentMode) {
|
||||||
|
switchMode(mode, { updateUrl: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Also handle orientation changes explicitly for mobile
|
// Also handle orientation changes explicitly for mobile
|
||||||
window.addEventListener('orientationchange', function () {
|
window.addEventListener('orientationchange', function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -3696,9 +3693,10 @@
|
|||||||
if (filteredDevices.length === 0) {
|
if (filteredDevices.length === 0) {
|
||||||
select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`;
|
select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`;
|
||||||
} else {
|
} else {
|
||||||
select.innerHTML = filteredDevices.map(d =>
|
select.innerHTML = filteredDevices.map(d => {
|
||||||
`<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}</option>`
|
const serialSuffix = d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (SN: ${d.serial})` : '';
|
||||||
).join('');
|
return `<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}${serialSuffix}</option>`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update capabilities display
|
// Update capabilities display
|
||||||
@@ -3764,7 +3762,7 @@
|
|||||||
return `<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border-color);">
|
return `<div style="display: flex; align-items: center; justify-content: space-between; padding: 6px 8px; border-bottom: 1px solid var(--border-color);">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
${statusDot}
|
${statusDot}
|
||||||
<span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}</span>
|
<span style="font-size: 11px;">#${d.index} ${d.name || 'Unknown'}${d.serial && d.serial !== 'N/A' && d.serial !== 'Unknown' ? ` (${d.serial})` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 6px;">
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
<span style="font-size: 10px; color: ${modeColor}; font-weight: bold;">${modeName}</span>
|
<span style="font-size: 10px; color: ${modeColor}; font-weight: bold;">${modeName}</span>
|
||||||
@@ -4283,7 +4281,7 @@
|
|||||||
|
|
||||||
const infoEl = document.createElement('div');
|
const infoEl = document.createElement('div');
|
||||||
infoEl.className = 'info-msg';
|
infoEl.className = 'info-msg';
|
||||||
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
||||||
infoEl.textContent = text;
|
infoEl.textContent = text;
|
||||||
output.insertBefore(infoEl, output.firstChild);
|
output.insertBefore(infoEl, output.firstChild);
|
||||||
}
|
}
|
||||||
@@ -4299,7 +4297,7 @@
|
|||||||
|
|
||||||
const errorEl = document.createElement('div');
|
const errorEl = document.createElement('div');
|
||||||
errorEl.className = 'error-msg';
|
errorEl.className = 'error-msg';
|
||||||
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
||||||
errorEl.textContent = '⚠ ' + text;
|
errorEl.textContent = '⚠ ' + text;
|
||||||
output.insertBefore(errorEl, output.firstChild);
|
output.insertBefore(errorEl, output.firstChild);
|
||||||
}
|
}
|
||||||
@@ -6915,7 +6913,7 @@
|
|||||||
|
|
||||||
// Draw total in center
|
// Draw total in center
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.font = 'bold 16px JetBrains Mono';
|
ctx.font = 'bold 16px Space Mono';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(total, cx, cy);
|
ctx.fillText(total, cx, cy);
|
||||||
@@ -7947,7 +7945,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAprsMap() {
|
async function initAprsMap() {
|
||||||
if (aprsMap) return;
|
if (aprsMap) return;
|
||||||
|
|
||||||
const mapContainer = document.getElementById('aprsMap');
|
const mapContainer = document.getElementById('aprsMap');
|
||||||
@@ -7962,7 +7960,9 @@
|
|||||||
window.aprsMap = aprsMap;
|
window.aprsMap = aprsMap;
|
||||||
|
|
||||||
// Use settings manager for tile layer (allows runtime changes)
|
// Use settings manager for tile layer (allows runtime changes)
|
||||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
if (typeof Settings !== 'undefined') {
|
||||||
|
// Wait for settings to load from server before applying tiles
|
||||||
|
await Settings.init();
|
||||||
Settings.createTileLayer().addTo(aprsMap);
|
Settings.createTileLayer().addTo(aprsMap);
|
||||||
Settings.registerMap(aprsMap);
|
Settings.registerMap(aprsMap);
|
||||||
} else {
|
} else {
|
||||||
@@ -8685,7 +8685,7 @@
|
|||||||
// Label
|
// Label
|
||||||
if (el > 0) {
|
if (el > 0) {
|
||||||
ctx.fillStyle = '#444';
|
ctx.fillStyle = '#444';
|
||||||
ctx.font = '10px JetBrains Mono';
|
ctx.font = '10px Space Mono';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(el + '°', cx, cy - r + 12);
|
ctx.fillText(el + '°', cx, cy - r + 12);
|
||||||
}
|
}
|
||||||
@@ -8758,7 +8758,7 @@
|
|||||||
|
|
||||||
// Label
|
// Label
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.font = '11px JetBrains Mono';
|
ctx.font = '11px Space Mono';
|
||||||
ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
|
ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8870,7 +8870,7 @@
|
|||||||
let observerMarker = null;
|
let observerMarker = null;
|
||||||
let satPositionInterval = null;
|
let satPositionInterval = null;
|
||||||
|
|
||||||
function initGroundTrackMap() {
|
async function initGroundTrackMap() {
|
||||||
const mapContainer = document.getElementById('groundTrackMap');
|
const mapContainer = document.getElementById('groundTrackMap');
|
||||||
if (!mapContainer || groundTrackMap) return;
|
if (!mapContainer || groundTrackMap) return;
|
||||||
|
|
||||||
@@ -8883,7 +8883,9 @@
|
|||||||
window.groundTrackMap = groundTrackMap;
|
window.groundTrackMap = groundTrackMap;
|
||||||
|
|
||||||
// Use settings manager for tile layer (allows runtime changes)
|
// Use settings manager for tile layer (allows runtime changes)
|
||||||
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
|
if (typeof Settings !== 'undefined') {
|
||||||
|
// Wait for settings to load from server before applying tiles
|
||||||
|
await Settings.init();
|
||||||
Settings.createTileLayer().addTo(groundTrackMap);
|
Settings.createTileLayer().addTo(groundTrackMap);
|
||||||
Settings.registerMap(groundTrackMap);
|
Settings.registerMap(groundTrackMap);
|
||||||
} else {
|
} else {
|
||||||
@@ -9343,14 +9345,10 @@
|
|||||||
// Theme toggle functions
|
// Theme toggle functions
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
const currentTheme = html.getAttribute('data-theme');
|
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
|
||||||
if (newTheme === 'dark') {
|
html.setAttribute('data-theme', newTheme);
|
||||||
html.removeAttribute('data-theme');
|
|
||||||
} else {
|
|
||||||
html.setAttribute('data-theme', newTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to localStorage for instant load on next visit
|
// Save to localStorage for instant load on next visit
|
||||||
localStorage.setItem('intercept-theme', newTheme);
|
localStorage.setItem('intercept-theme', newTheme);
|
||||||
@@ -9382,10 +9380,8 @@
|
|||||||
// Load saved theme and animations on page load
|
// Load saved theme and animations on page load
|
||||||
(function () {
|
(function () {
|
||||||
// First apply localStorage theme for instant load (no flash)
|
// First apply localStorage theme for instant load (no flash)
|
||||||
const localTheme = localStorage.getItem('intercept-theme');
|
const localTheme = localStorage.getItem('intercept-theme') || 'dark';
|
||||||
if (localTheme === 'light') {
|
document.documentElement.setAttribute('data-theme', localTheme);
|
||||||
document.documentElement.setAttribute('data-theme', 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply animations preference
|
// Apply animations preference
|
||||||
const localAnimations = localStorage.getItem('intercept-animations');
|
const localAnimations = localStorage.getItem('intercept-animations');
|
||||||
@@ -9401,11 +9397,7 @@
|
|||||||
const serverTheme = data.value;
|
const serverTheme = data.value;
|
||||||
if (serverTheme !== localTheme) {
|
if (serverTheme !== localTheme) {
|
||||||
// Server has different theme, apply it
|
// Server has different theme, apply it
|
||||||
if (serverTheme === 'light') {
|
document.documentElement.setAttribute('data-theme', serverTheme);
|
||||||
document.documentElement.setAttribute('data-theme', 'light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.removeAttribute('data-theme');
|
|
||||||
}
|
|
||||||
localStorage.setItem('intercept-theme', serverTheme);
|
localStorage.setItem('intercept-theme', serverTheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12205,7 +12197,7 @@
|
|||||||
<textarea id="tleInput" placeholder="ISS (ZARYA)
|
<textarea id="tleInput" placeholder="ISS (ZARYA)
|
||||||
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
||||||
2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450"
|
2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450"
|
||||||
style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px; resize: vertical;"></textarea>
|
style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: var(--font-mono); font-size: 11px; resize: vertical;"></textarea>
|
||||||
<button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add
|
<button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add
|
||||||
Satellite</button>
|
Satellite</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Frequency</span>
|
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Frequency</span>
|
||||||
<span id="lpQuickFreq" style="font-size: 14px; font-family: 'JetBrains Mono', monospace; color: var(--text-primary);">---.--- MHz</span>
|
<span id="lpQuickFreq" style="font-size: 14px; font-family: var(--font-mono); color: var(--text-primary);">---.--- MHz</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Signals</span>
|
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Signals</span>
|
||||||
|
|||||||
@@ -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 -->
|
<!-- Settings Modal -->
|
||||||
<div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()">
|
<div id="settingsModal" class="settings-modal" onclick="if(event.target === this) hideSettings()">
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||||
Settings
|
Settings
|
||||||
</h2>
|
</h2>
|
||||||
<button class="settings-close" onclick="hideSettings()">×</button>
|
<button class="settings-close" onclick="hideSettings()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-tabs">
|
<div class="settings-tabs">
|
||||||
<button class="settings-tab active" data-tab="offline" onclick="switchSettingsTab('offline')">Offline</button>
|
<button class="settings-tab active" data-tab="offline" onclick="switchSettingsTab('offline')">Offline</button>
|
||||||
<button class="settings-tab" data-tab="location" onclick="switchSettingsTab('location')">Location</button>
|
<button class="settings-tab" data-tab="location" onclick="switchSettingsTab('location')">Location</button>
|
||||||
<button class="settings-tab" data-tab="display" onclick="switchSettingsTab('display')">Display</button>
|
<button class="settings-tab" data-tab="display" onclick="switchSettingsTab('display')">Display</button>
|
||||||
<button class="settings-tab" data-tab="updates" onclick="switchSettingsTab('updates')">Updates</button>
|
<button class="settings-tab" data-tab="updates" onclick="switchSettingsTab('updates')">Updates</button>
|
||||||
<button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button>
|
<button class="settings-tab" data-tab="tools" onclick="switchSettingsTab('tools')">Tools</button>
|
||||||
<button class="settings-tab" data-tab="about" onclick="switchSettingsTab('about')">About</button>
|
<button class="settings-tab" data-tab="about" onclick="switchSettingsTab('about')">About</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Offline Section -->
|
<!-- Offline Section -->
|
||||||
<div id="settings-offline" class="settings-section active">
|
<div id="settings-offline" class="settings-section active">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Offline Mode</div>
|
<div class="settings-group-title">Offline Mode</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Enable Offline Mode</span>
|
<span class="settings-label-text">Enable Offline Mode</span>
|
||||||
<span class="settings-label-desc">Use local assets instead of CDN</span>
|
<span class="settings-label-desc">Use local assets instead of CDN</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)">
|
<input type="checkbox" id="offlineEnabled" onchange="Settings.toggleOfflineMode(this.checked)">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Asset Sources</div>
|
<div class="settings-group-title">Asset Sources</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">JavaScript/CSS Libraries</span>
|
<span class="settings-label-text">JavaScript/CSS Libraries</span>
|
||||||
<span class="settings-label-desc">Leaflet, Chart.js</span>
|
<span class="settings-label-desc">Leaflet, Chart.js</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)">
|
<select id="assetsSource" class="settings-select" onchange="Settings.setAssetSource(this.value)">
|
||||||
<option value="cdn">CDN (Online)</option>
|
<option value="cdn">CDN (Online)</option>
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Web Fonts</span>
|
<span class="settings-label-text">Web Fonts</span>
|
||||||
<span class="settings-label-desc">Inter, JetBrains Mono</span>
|
<span class="settings-label-desc">Space Mono</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
|
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
|
||||||
<option value="cdn">Google Fonts (Online)</option>
|
<option value="cdn">Google Fonts (Online)</option>
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Map Tiles</div>
|
<div class="settings-group-title">Map Tiles</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Tile Provider</span>
|
<span class="settings-label-text">Tile Provider</span>
|
||||||
<span class="settings-label-desc">Map background imagery</span>
|
<span class="settings-label-desc">Map background imagery</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
|
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
|
||||||
<option value="openstreetmap">OpenStreetMap</option>
|
<option value="openstreetmap">OpenStreetMap</option>
|
||||||
<option value="cartodb_dark">CartoDB Dark</option>
|
<option value="cartodb_dark">CartoDB Dark</option>
|
||||||
<option value="cartodb_light">CartoDB Positron</option>
|
<option value="cartodb_light">CartoDB Positron</option>
|
||||||
<option value="esri_world">ESRI World Imagery</option>
|
<option value="esri_world">ESRI World Imagery</option>
|
||||||
<option value="custom">Custom URL</option>
|
<option value="custom">Custom URL</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
|
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
|
||||||
<div class="settings-label" style="width: 100%;">
|
<div class="settings-label" style="width: 100%;">
|
||||||
<span class="settings-label-text">Custom Tile URL</span>
|
<span class="settings-label-text">Custom Tile URL</span>
|
||||||
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
|
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
|
||||||
<input type="text" id="customTileUrl" class="settings-input"
|
<input type="text" id="customTileUrl" class="settings-input"
|
||||||
placeholder="http://tile-server/{z}/{x}/{y}.png"
|
placeholder="http://tile-server/{z}/{x}/{y}.png"
|
||||||
onchange="Settings.setCustomTileUrl(this.value)">
|
onchange="Settings.setCustomTileUrl(this.value)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Local Asset Status</div>
|
<div class="settings-group-title">Local Asset Status</div>
|
||||||
<div class="asset-status" id="assetStatus">
|
<div class="asset-status" id="assetStatus">
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Leaflet JS/CSS</span>
|
<span class="asset-name">Leaflet JS/CSS</span>
|
||||||
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
|
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Chart.js</span>
|
<span class="asset-name">Chart.js</span>
|
||||||
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
|
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Inter Font</span>
|
<span class="asset-name">Inter Font</span>
|
||||||
<span class="asset-badge checking" id="statusInter">Checking...</span>
|
<span class="asset-badge checking" id="statusInter">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">JetBrains Mono</span>
|
<span class="asset-name">Space Mono</span>
|
||||||
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
|
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="check-assets-btn" onclick="Settings.checkAssets()">
|
<button class="check-assets-btn" onclick="Settings.checkAssets()">
|
||||||
Check Assets
|
Check Assets
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
|
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
|
||||||
Local assets must be available in <code>/static/vendor/</code>.
|
Local assets must be available in <code>/static/vendor/</code>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location Section -->
|
<!-- Location Section -->
|
||||||
<div id="settings-location" class="settings-section">
|
<div id="settings-location" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Observer Location</div>
|
<div class="settings-group-title">Observer Location</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
Set your geographic coordinates for satellite pass predictions and ISS tracking.
|
Set your geographic coordinates for satellite pass predictions and ISS tracking.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Latitude</span>
|
<span class="settings-label-text">Latitude</span>
|
||||||
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
|
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="number" id="observerLatInput" class="settings-input"
|
<input type="number" id="observerLatInput" class="settings-input"
|
||||||
step="0.0001" min="-90" max="90" placeholder="51.5074"
|
step="0.0001" min="-90" max="90" placeholder="51.5074"
|
||||||
style="width: 120px; text-align: right;">
|
style="width: 120px; text-align: right;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Longitude</span>
|
<span class="settings-label-text">Longitude</span>
|
||||||
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
|
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="number" id="observerLonInput" class="settings-input"
|
<input type="number" id="observerLonInput" class="settings-input"
|
||||||
step="0.0001" min="-180" max="180" placeholder="-0.1278"
|
step="0.0001" min="-180" max="180" placeholder="-0.1278"
|
||||||
style="width: 120px; text-align: right;">
|
style="width: 120px; text-align: right;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||||||
<button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;">
|
<button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
|
||||||
<circle cx="12" cy="12" r="10"/>
|
<circle cx="12" cy="12" r="10"/>
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<line x1="12" y1="2" x2="12" y2="6"/>
|
<line x1="12" y1="2" x2="12" y2="6"/>
|
||||||
<line x1="12" y1="18" x2="12" y2="22"/>
|
<line x1="12" y1="18" x2="12" y2="22"/>
|
||||||
<line x1="2" y1="12" x2="6" y2="12"/>
|
<line x1="2" y1="12" x2="6" y2="12"/>
|
||||||
<line x1="18" y1="12" x2="22" y2="12"/>
|
<line x1="18" y1="12" x2="22" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Use GPS
|
Use GPS
|
||||||
</button>
|
</button>
|
||||||
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
|
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
|
||||||
Save Location
|
Save Location
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Current Location</div>
|
<div class="settings-group-title">Current Location</div>
|
||||||
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12px;">
|
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: var(--font-mono); font-size: 12px;">
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||||
<span style="color: var(--text-dim);">Latitude</span>
|
<span style="color: var(--text-dim);">Latitude</span>
|
||||||
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
|
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<div style="display: flex; justify-content: space-between;">
|
||||||
<span style="color: var(--text-dim);">Longitude</span>
|
<span style="color: var(--text-dim);">Longitude</span>
|
||||||
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
|
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
|
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
|
||||||
Your location is stored locally and never sent to external servers.
|
Your location is stored locally and never sent to external servers.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display Section -->
|
<!-- Display Section -->
|
||||||
<div id="settings-display" class="settings-section">
|
<div id="settings-display" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Visual Preferences</div>
|
<div class="settings-group-title">Visual Preferences</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Theme</span>
|
<span class="settings-label-text">Theme</span>
|
||||||
<span class="settings-label-desc">Color scheme preference</span>
|
<span class="settings-label-desc">Color scheme preference</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
|
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Animations</span>
|
<span class="settings-label-text">Animations</span>
|
||||||
<span class="settings-label-desc">Enable visual effects and animations</span>
|
<span class="settings-label-desc">Enable visual effects and animations</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
|
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Updates Section -->
|
<!-- Updates Section -->
|
||||||
<div id="settings-updates" class="settings-section">
|
<div id="settings-updates" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Update Status</div>
|
<div class="settings-group-title">Update Status</div>
|
||||||
<div id="updateStatusContent" style="padding: 10px 0;">
|
<div id="updateStatusContent" style="padding: 10px 0;">
|
||||||
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
|
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
|
||||||
Loading update status...
|
Loading update status...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
|
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
|
||||||
Check Now
|
Check Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Update Settings</div>
|
<div class="settings-group-title">Update Settings</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Auto-Check for Updates</span>
|
<span class="settings-label-text">Auto-Check for Updates</span>
|
||||||
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
|
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
|
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Updates are fetched from GitHub and applied via git pull.
|
<strong>Note:</strong> Updates are fetched from GitHub and applied via git pull.
|
||||||
Make sure you have git installed and the application is in a git repository.
|
Make sure you have git installed and the application is in a git repository.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tools Section -->
|
<!-- Tools Section -->
|
||||||
<div id="settings-tools" class="settings-section">
|
<div id="settings-tools" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Tool Dependencies</div>
|
<div class="settings-group-title">Tool Dependencies</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
Check which external tools are installed for each mode.
|
Check which external tools are installed for each mode.
|
||||||
<span style="color: var(--accent-green);">●</span> = Installed,
|
<span style="color: var(--accent-green);">●</span> = Installed,
|
||||||
<span style="color: var(--accent-red);">●</span> = Missing
|
<span style="color: var(--accent-red);">●</span> = Missing
|
||||||
</p>
|
</p>
|
||||||
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
|
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
|
||||||
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
|
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
|
||||||
Loading dependencies...
|
Loading dependencies...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group" style="margin-top: 15px;">
|
<div class="settings-group" style="margin-top: 15px;">
|
||||||
<div class="settings-group-title">Quick Install (Debian/Ubuntu)</div>
|
<div class="settings-group-title">Quick Install (Debian/Ubuntu)</div>
|
||||||
<div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;">
|
<div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;">
|
||||||
<div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div>
|
<div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div>
|
||||||
<div style="margin-top: 5px;">pip install skyfield flask</div>
|
<div style="margin-top: 5px;">pip install skyfield flask</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);">
|
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);">
|
||||||
<strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source.
|
<strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source.
|
||||||
See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation.
|
See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<div id="settings-about" class="settings-section">
|
<div id="settings-about" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="about-info">
|
<div class="about-info">
|
||||||
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
|
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
|
||||||
<p>Version: <span class="about-version">{{ version }}</span></p>
|
<p>Version: <span class="about-version">{{ version }}</span></p>
|
||||||
<p>
|
<p>
|
||||||
A unified web interface for software-defined radio (SDR) tools,
|
A unified web interface for software-defined radio (SDR) tools,
|
||||||
supporting pager decoding, sensor monitoring, aircraft tracking,
|
supporting pager decoding, sensor monitoring, aircraft tracking,
|
||||||
WiFi/Bluetooth scanning, and more.
|
WiFi/Bluetooth scanning, and more.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
|
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Support the Project</div>
|
<div class="settings-group-title">Support the Project</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
If you find iNTERCEPT useful, consider supporting its development.
|
If you find iNTERCEPT useful, consider supporting its development.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
|
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
|
||||||
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
|
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
|
||||||
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
|
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
|
||||||
<line x1="6" y1="2" x2="6" y2="4"/>
|
<line x1="6" y1="2" x2="6" y2="4"/>
|
||||||
<line x1="10" y1="2" x2="10" y2="4"/>
|
<line x1="10" y1="2" x2="10" y2="4"/>
|
||||||
<line x1="14" y1="2" x2="14" y2="4"/>
|
<line x1="14" y1="2" x2="14" y2="4"/>
|
||||||
</svg>
|
</svg>
|
||||||
Buy Me a Coffee
|
Buy Me a Coffee
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1021
-1003
File diff suppressed because it is too large
Load Diff
+84
-2
@@ -9,11 +9,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.request import urlopen, Request
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.error import URLError, HTTPError
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from utils.database import get_setting, set_setting
|
from utils.database import get_setting, set_setting
|
||||||
@@ -509,6 +510,7 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
|||||||
'success': True,
|
'success': True,
|
||||||
'updated': True,
|
'updated': True,
|
||||||
'message': 'Update successful! Please restart the application.',
|
'message': 'Update successful! Please restart the application.',
|
||||||
|
'restart_required': True,
|
||||||
'requirements_changed': requirements_changed,
|
'requirements_changed': requirements_changed,
|
||||||
'stashed': stashed,
|
'stashed': stashed,
|
||||||
'stash_restored': stashed,
|
'stash_restored': stashed,
|
||||||
@@ -527,3 +529,83 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def restart_application() -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Restart the application using os.execv to replace the current process.
|
||||||
|
|
||||||
|
This function:
|
||||||
|
1. Cleans up all running decoder processes
|
||||||
|
2. Stops the cleanup manager
|
||||||
|
3. Replaces the current process with a fresh Python interpreter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with status (though this is typically not reached due to execv)
|
||||||
|
"""
|
||||||
|
import app as app_module
|
||||||
|
from utils.cleanup import cleanup_manager
|
||||||
|
from utils.process import cleanup_all_processes
|
||||||
|
|
||||||
|
logger.info("Application restart requested")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Kill all decoder processes
|
||||||
|
logger.info("Stopping all decoder processes...")
|
||||||
|
cleanup_all_processes()
|
||||||
|
|
||||||
|
# Step 2: Clear global process state
|
||||||
|
with app_module.process_lock:
|
||||||
|
app_module.current_process = None
|
||||||
|
with app_module.sensor_lock:
|
||||||
|
app_module.sensor_process = None
|
||||||
|
with app_module.wifi_lock:
|
||||||
|
app_module.wifi_process = None
|
||||||
|
with app_module.adsb_lock:
|
||||||
|
app_module.adsb_process = None
|
||||||
|
with app_module.ais_lock:
|
||||||
|
app_module.ais_process = None
|
||||||
|
with app_module.acars_lock:
|
||||||
|
app_module.acars_process = None
|
||||||
|
with app_module.aprs_lock:
|
||||||
|
app_module.aprs_process = None
|
||||||
|
app_module.aprs_rtl_process = None
|
||||||
|
with app_module.dsc_lock:
|
||||||
|
app_module.dsc_process = None
|
||||||
|
app_module.dsc_rtl_process = None
|
||||||
|
|
||||||
|
# Step 3: Clear SDR device registry
|
||||||
|
with app_module.sdr_device_registry_lock:
|
||||||
|
app_module.sdr_device_registry.clear()
|
||||||
|
|
||||||
|
# Step 4: Stop cleanup manager
|
||||||
|
logger.info("Stopping cleanup manager...")
|
||||||
|
cleanup_manager.stop()
|
||||||
|
|
||||||
|
# Step 5: Prepare for restart using os.execv
|
||||||
|
# Get the Python executable and script path
|
||||||
|
python_executable = sys.executable
|
||||||
|
script_path = os.path.abspath(sys.argv[0])
|
||||||
|
|
||||||
|
# Build argument list (preserve original command-line args)
|
||||||
|
args = [python_executable, script_path] + sys.argv[1:]
|
||||||
|
|
||||||
|
logger.info(f"Restarting with: {' '.join(args)}")
|
||||||
|
|
||||||
|
# Flush any pending log output
|
||||||
|
logging.shutdown()
|
||||||
|
|
||||||
|
# Use os.execv to replace the current process
|
||||||
|
# This will not return - the process is replaced entirely
|
||||||
|
os.execv(python_executable, args)
|
||||||
|
|
||||||
|
# This code is never reached
|
||||||
|
return {'success': True, 'message': 'Restarting...'}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Restart failed: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e),
|
||||||
|
'message': 'Failed to restart application. Please restart manually.'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user