mirror of
https://github.com/smittix/intercept.git
synced 2026-07-25 09:18:10 -07:00
Merge branch 'codex/new-ui'
# Conflicts: # static/css/index.css
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
@@ -5,6 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -94,7 +96,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -132,7 +134,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +626,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-value {
|
.telemetry-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -680,7 +682,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-icao {
|
.aircraft-icao {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
@@ -700,7 +702,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aircraft-detail-value {
|
.aircraft-detail-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -790,7 +792,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,7 +803,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,7 +881,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -911,7 +913,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1025,7 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -1057,7 +1059,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.airband-status {
|
.airband-status {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -1407,7 +1409,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.strip-value {
|
.strip-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -1545,7 +1547,7 @@ body {
|
|||||||
|
|
||||||
.report-grid span:nth-child(even) {
|
.report-grid span:nth-child(even) {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-highlights {
|
.report-highlights {
|
||||||
@@ -1784,7 +1786,7 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -1938,7 +1940,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.squawk-code {
|
.squawk-code {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #141a24;
|
--bg-card: #141a24;
|
||||||
@@ -20,14 +22,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-bg {
|
.radar-bg {
|
||||||
@@ -91,7 +93,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +270,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -306,7 +308,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-meta {
|
.panel-meta {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -347,7 +349,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-row td,
|
.empty-row td,
|
||||||
|
|||||||
+345
-343
@@ -1,343 +1,345 @@
|
|||||||
/*
|
/*
|
||||||
* Agents Management CSS
|
* Agents Management CSS
|
||||||
* Styles for the remote agent management interface
|
* Styles for the remote agent management interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* CSS Variables (inherited from main theme) */
|
/* CSS Variables (inherited from main theme) */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0a0a0f;
|
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
--bg-secondary: #12121a;
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--text-primary: #e0e0e0;
|
--bg-primary: #0a0a0f;
|
||||||
--text-secondary: #888;
|
--bg-secondary: #12121a;
|
||||||
--border-color: #1a1a2e;
|
--text-primary: #e0e0e0;
|
||||||
--accent-cyan: #00d4ff;
|
--text-secondary: #888;
|
||||||
--accent-green: #00ff88;
|
--border-color: #1a1a2e;
|
||||||
--accent-red: #ff3366;
|
--accent-cyan: #00d4ff;
|
||||||
--accent-orange: #ff9f1c;
|
--accent-green: #00ff88;
|
||||||
}
|
--accent-red: #ff3366;
|
||||||
|
--accent-orange: #ff9f1c;
|
||||||
/* Agent indicator in navigation */
|
}
|
||||||
.agent-indicator {
|
|
||||||
display: flex;
|
/* Agent indicator in navigation */
|
||||||
align-items: center;
|
.agent-indicator {
|
||||||
gap: 8px;
|
display: flex;
|
||||||
padding: 6px 12px;
|
align-items: center;
|
||||||
background: rgba(0, 212, 255, 0.1);
|
gap: 8px;
|
||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
padding: 6px 12px;
|
||||||
border-radius: 20px;
|
background: rgba(0, 212, 255, 0.1);
|
||||||
cursor: pointer;
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
transition: all 0.2s;
|
border-radius: 20px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
.agent-indicator:hover {
|
}
|
||||||
background: rgba(0, 212, 255, 0.2);
|
|
||||||
border-color: var(--accent-cyan);
|
.agent-indicator:hover {
|
||||||
}
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
.agent-indicator-dot {
|
}
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
.agent-indicator-dot {
|
||||||
border-radius: 50%;
|
width: 8px;
|
||||||
background: var(--accent-green);
|
height: 8px;
|
||||||
box-shadow: 0 0 6px var(--accent-green);
|
border-radius: 50%;
|
||||||
}
|
background: var(--accent-green);
|
||||||
|
box-shadow: 0 0 6px var(--accent-green);
|
||||||
.agent-indicator-dot.remote {
|
}
|
||||||
background: var(--accent-cyan);
|
|
||||||
box-shadow: 0 0 6px var(--accent-cyan);
|
.agent-indicator-dot.remote {
|
||||||
}
|
background: var(--accent-cyan);
|
||||||
|
box-shadow: 0 0 6px var(--accent-cyan);
|
||||||
.agent-indicator-dot.multiple {
|
}
|
||||||
background: var(--accent-orange);
|
|
||||||
box-shadow: 0 0 6px var(--accent-orange);
|
.agent-indicator-dot.multiple {
|
||||||
}
|
background: var(--accent-orange);
|
||||||
|
box-shadow: 0 0 6px var(--accent-orange);
|
||||||
.agent-indicator-label {
|
}
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-primary);
|
.agent-indicator-label {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-size: 11px;
|
||||||
}
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
.agent-indicator-count {
|
}
|
||||||
font-size: 10px;
|
|
||||||
padding: 2px 6px;
|
.agent-indicator-count {
|
||||||
background: rgba(0, 212, 255, 0.2);
|
font-size: 10px;
|
||||||
border-radius: 10px;
|
padding: 2px 6px;
|
||||||
color: var(--accent-cyan);
|
background: rgba(0, 212, 255, 0.2);
|
||||||
}
|
border-radius: 10px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
/* Agent selector dropdown */
|
}
|
||||||
.agent-selector {
|
|
||||||
position: relative;
|
/* Agent selector dropdown */
|
||||||
}
|
.agent-selector {
|
||||||
|
position: relative;
|
||||||
.agent-selector-dropdown {
|
}
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
.agent-selector-dropdown {
|
||||||
right: 0;
|
position: absolute;
|
||||||
margin-top: 8px;
|
top: 100%;
|
||||||
min-width: 280px;
|
right: 0;
|
||||||
background: var(--bg-secondary);
|
margin-top: 8px;
|
||||||
border: 1px solid var(--border-color);
|
min-width: 280px;
|
||||||
border-radius: 8px;
|
background: var(--bg-secondary);
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
border: 1px solid var(--border-color);
|
||||||
z-index: 1000;
|
border-radius: 8px;
|
||||||
display: none;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
}
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
.agent-selector-dropdown.show {
|
}
|
||||||
display: block;
|
|
||||||
}
|
.agent-selector-dropdown.show {
|
||||||
|
display: block;
|
||||||
.agent-selector-header {
|
}
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
.agent-selector-header {
|
||||||
align-items: center;
|
display: flex;
|
||||||
padding: 12px 15px;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--border-color);
|
align-items: center;
|
||||||
}
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
.agent-selector-header h4 {
|
}
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
.agent-selector-header h4 {
|
||||||
color: var(--accent-cyan);
|
margin: 0;
|
||||||
text-transform: uppercase;
|
font-size: 12px;
|
||||||
letter-spacing: 1px;
|
color: var(--accent-cyan);
|
||||||
}
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
.agent-selector-manage {
|
}
|
||||||
font-size: 11px;
|
|
||||||
color: var(--accent-cyan);
|
.agent-selector-manage {
|
||||||
text-decoration: none;
|
font-size: 11px;
|
||||||
}
|
color: var(--accent-cyan);
|
||||||
|
text-decoration: none;
|
||||||
.agent-selector-manage:hover {
|
}
|
||||||
text-decoration: underline;
|
|
||||||
}
|
.agent-selector-manage:hover {
|
||||||
|
text-decoration: underline;
|
||||||
.agent-selector-list {
|
}
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
.agent-selector-list {
|
||||||
}
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
.agent-selector-item {
|
}
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
.agent-selector-item {
|
||||||
gap: 10px;
|
display: flex;
|
||||||
padding: 10px 15px;
|
align-items: center;
|
||||||
cursor: pointer;
|
gap: 10px;
|
||||||
transition: background 0.2s;
|
padding: 10px 15px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
cursor: pointer;
|
||||||
}
|
transition: background 0.2s;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
.agent-selector-item:last-child {
|
}
|
||||||
border-bottom: none;
|
|
||||||
}
|
.agent-selector-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
.agent-selector-item:hover {
|
}
|
||||||
background: rgba(0, 212, 255, 0.1);
|
|
||||||
}
|
.agent-selector-item:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
.agent-selector-item.selected {
|
}
|
||||||
background: rgba(0, 212, 255, 0.15);
|
|
||||||
border-left: 3px solid var(--accent-cyan);
|
.agent-selector-item.selected {
|
||||||
}
|
background: rgba(0, 212, 255, 0.15);
|
||||||
|
border-left: 3px solid var(--accent-cyan);
|
||||||
.agent-selector-item.local {
|
}
|
||||||
border-left: 3px solid var(--accent-green);
|
|
||||||
}
|
.agent-selector-item.local {
|
||||||
|
border-left: 3px solid var(--accent-green);
|
||||||
.agent-selector-item-status {
|
}
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
.agent-selector-item-status {
|
||||||
border-radius: 50%;
|
width: 8px;
|
||||||
flex-shrink: 0;
|
height: 8px;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
.agent-selector-item-status.online {
|
}
|
||||||
background: var(--accent-green);
|
|
||||||
}
|
.agent-selector-item-status.online {
|
||||||
|
background: var(--accent-green);
|
||||||
.agent-selector-item-status.offline {
|
}
|
||||||
background: var(--accent-red);
|
|
||||||
}
|
.agent-selector-item-status.offline {
|
||||||
|
background: var(--accent-red);
|
||||||
.agent-selector-item-info {
|
}
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
.agent-selector-item-info {
|
||||||
}
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
.agent-selector-item-name {
|
}
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-primary);
|
.agent-selector-item-name {
|
||||||
white-space: nowrap;
|
font-size: 13px;
|
||||||
overflow: hidden;
|
color: var(--text-primary);
|
||||||
text-overflow: ellipsis;
|
white-space: nowrap;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
.agent-selector-item-url {
|
}
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-secondary);
|
.agent-selector-item-url {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-size: 10px;
|
||||||
white-space: nowrap;
|
color: var(--text-secondary);
|
||||||
overflow: hidden;
|
font-family: var(--font-mono);
|
||||||
text-overflow: ellipsis;
|
white-space: nowrap;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
.agent-selector-item-check {
|
}
|
||||||
color: var(--accent-green);
|
|
||||||
opacity: 0;
|
.agent-selector-item-check {
|
||||||
}
|
color: var(--accent-green);
|
||||||
|
opacity: 0;
|
||||||
.agent-selector-item.selected .agent-selector-item-check {
|
}
|
||||||
opacity: 1;
|
|
||||||
}
|
.agent-selector-item.selected .agent-selector-item-check {
|
||||||
|
opacity: 1;
|
||||||
/* Agent badge in data displays */
|
}
|
||||||
.agent-badge {
|
|
||||||
display: inline-flex;
|
/* Agent badge in data displays */
|
||||||
align-items: center;
|
.agent-badge {
|
||||||
gap: 4px;
|
display: inline-flex;
|
||||||
padding: 2px 8px;
|
align-items: center;
|
||||||
font-size: 10px;
|
gap: 4px;
|
||||||
background: rgba(0, 212, 255, 0.1);
|
padding: 2px 8px;
|
||||||
color: var(--accent-cyan);
|
font-size: 10px;
|
||||||
border-radius: 10px;
|
background: rgba(0, 212, 255, 0.1);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
color: var(--accent-cyan);
|
||||||
}
|
border-radius: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
.agent-badge.local,
|
}
|
||||||
.agent-badge.agent-local {
|
|
||||||
background: rgba(0, 255, 136, 0.1);
|
.agent-badge.local,
|
||||||
color: var(--accent-green);
|
.agent-badge.agent-local {
|
||||||
}
|
background: rgba(0, 255, 136, 0.1);
|
||||||
|
color: var(--accent-green);
|
||||||
.agent-badge.agent-remote {
|
}
|
||||||
background: rgba(0, 212, 255, 0.1);
|
|
||||||
color: var(--accent-cyan);
|
.agent-badge.agent-remote {
|
||||||
}
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
color: var(--accent-cyan);
|
||||||
/* WiFi table agent column */
|
}
|
||||||
.wifi-networks-table .col-agent {
|
|
||||||
width: 100px;
|
/* WiFi table agent column */
|
||||||
text-align: center;
|
.wifi-networks-table .col-agent {
|
||||||
}
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
.wifi-networks-table th.col-agent {
|
}
|
||||||
font-size: 10px;
|
|
||||||
}
|
.wifi-networks-table th.col-agent {
|
||||||
|
font-size: 10px;
|
||||||
/* Bluetooth table agent column */
|
}
|
||||||
.bt-devices-table .col-agent {
|
|
||||||
width: 100px;
|
/* Bluetooth table agent column */
|
||||||
text-align: center;
|
.bt-devices-table .col-agent {
|
||||||
}
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
.agent-badge-dot {
|
}
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
.agent-badge-dot {
|
||||||
border-radius: 50%;
|
width: 6px;
|
||||||
background: currentColor;
|
height: 6px;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
/* Agent column in data tables */
|
}
|
||||||
.data-table .agent-col {
|
|
||||||
width: 120px;
|
/* Agent column in data tables */
|
||||||
max-width: 120px;
|
.data-table .agent-col {
|
||||||
}
|
width: 120px;
|
||||||
|
max-width: 120px;
|
||||||
/* Multi-agent stream indicator */
|
}
|
||||||
.multi-agent-indicator {
|
|
||||||
position: fixed;
|
/* Multi-agent stream indicator */
|
||||||
bottom: 20px;
|
.multi-agent-indicator {
|
||||||
left: 20px;
|
position: fixed;
|
||||||
display: flex;
|
bottom: 20px;
|
||||||
align-items: center;
|
left: 20px;
|
||||||
gap: 8px;
|
display: flex;
|
||||||
padding: 8px 12px;
|
align-items: center;
|
||||||
background: var(--bg-secondary);
|
gap: 8px;
|
||||||
border: 1px solid var(--border-color);
|
padding: 8px 12px;
|
||||||
border-radius: 20px;
|
background: var(--bg-secondary);
|
||||||
font-size: 11px;
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-secondary);
|
border-radius: 20px;
|
||||||
z-index: 100;
|
font-size: 11px;
|
||||||
}
|
color: var(--text-secondary);
|
||||||
|
z-index: 100;
|
||||||
.multi-agent-indicator.active {
|
}
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
color: var(--accent-cyan);
|
.multi-agent-indicator.active {
|
||||||
}
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--accent-cyan);
|
||||||
.multi-agent-indicator-pulse {
|
}
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
.multi-agent-indicator-pulse {
|
||||||
border-radius: 50%;
|
width: 8px;
|
||||||
background: var(--accent-cyan);
|
height: 8px;
|
||||||
animation: pulse 2s infinite;
|
border-radius: 50%;
|
||||||
}
|
background: var(--accent-cyan);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
@keyframes pulse {
|
}
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.5; transform: scale(0.8); }
|
@keyframes pulse {
|
||||||
}
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.5; transform: scale(0.8); }
|
||||||
/* Agent connection status toast */
|
}
|
||||||
.agent-toast {
|
|
||||||
position: fixed;
|
/* Agent connection status toast */
|
||||||
top: 80px;
|
.agent-toast {
|
||||||
right: 20px;
|
position: fixed;
|
||||||
padding: 10px 15px;
|
top: 80px;
|
||||||
background: var(--bg-secondary);
|
right: 20px;
|
||||||
border: 1px solid var(--border-color);
|
padding: 10px 15px;
|
||||||
border-radius: 6px;
|
background: var(--bg-secondary);
|
||||||
font-size: 12px;
|
border: 1px solid var(--border-color);
|
||||||
z-index: 1001;
|
border-radius: 6px;
|
||||||
animation: slideInRight 0.3s ease;
|
font-size: 12px;
|
||||||
}
|
z-index: 1001;
|
||||||
|
animation: slideInRight 0.3s ease;
|
||||||
.agent-toast.connected {
|
}
|
||||||
border-color: var(--accent-green);
|
|
||||||
color: var(--accent-green);
|
.agent-toast.connected {
|
||||||
}
|
border-color: var(--accent-green);
|
||||||
|
color: var(--accent-green);
|
||||||
.agent-toast.disconnected {
|
}
|
||||||
border-color: var(--accent-red);
|
|
||||||
color: var(--accent-red);
|
.agent-toast.disconnected {
|
||||||
}
|
border-color: var(--accent-red);
|
||||||
|
color: var(--accent-red);
|
||||||
@keyframes slideInRight {
|
}
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
@keyframes slideInRight {
|
||||||
opacity: 0;
|
from {
|
||||||
}
|
transform: translateX(100%);
|
||||||
to {
|
opacity: 0;
|
||||||
transform: translateX(0);
|
}
|
||||||
opacity: 1;
|
to {
|
||||||
}
|
transform: translateX(0);
|
||||||
}
|
opacity: 1;
|
||||||
|
}
|
||||||
/* Responsive adjustments */
|
}
|
||||||
@media (max-width: 768px) {
|
|
||||||
.agent-indicator {
|
/* Responsive adjustments */
|
||||||
padding: 4px 8px;
|
@media (max-width: 768px) {
|
||||||
}
|
.agent-indicator {
|
||||||
|
padding: 4px 8px;
|
||||||
.agent-indicator-label {
|
}
|
||||||
display: none;
|
|
||||||
}
|
.agent-indicator-label {
|
||||||
|
display: none;
|
||||||
.agent-selector-dropdown {
|
}
|
||||||
position: fixed;
|
|
||||||
top: auto;
|
.agent-selector-dropdown {
|
||||||
bottom: 0;
|
position: fixed;
|
||||||
left: 0;
|
top: auto;
|
||||||
right: 0;
|
bottom: 0;
|
||||||
margin: 0;
|
left: 0;
|
||||||
border-radius: 16px 16px 0 0;
|
right: 0;
|
||||||
max-height: 60vh;
|
margin: 0;
|
||||||
}
|
border-radius: 16px 16px 0 0;
|
||||||
|
max-height: 60vh;
|
||||||
.agents-grid {
|
}
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
.agents-grid {
|
||||||
}
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -97,7 +99,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
@@ -134,7 +136,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +185,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.strip-value {
|
.strip-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -287,7 +289,7 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
border-left: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -367,7 +369,7 @@ body {
|
|||||||
/* Leaflet overrides - Dark map styling */
|
/* Leaflet overrides - Dark map styling */
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
background: var(--bg-dark) !important;
|
background: var(--bg-dark) !important;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Using actual dark tiles now - no filter needed */
|
/* Using actual dark tiles now - no filter needed */
|
||||||
@@ -518,7 +520,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-mmsi {
|
.vessel-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
@@ -548,7 +550,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -611,13 +613,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-type {
|
.vessel-item-type {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vessel-item-speed {
|
.vessel-item-speed {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -687,7 +689,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +700,7 @@ body {
|
|||||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +719,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1004,7 +1006,7 @@ body {
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-bottom: 1px solid rgba(245, 158, 11, 0.1);
|
border-bottom: 1px solid rgba(245, 158, 11, 0.1);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,7 +1081,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-category {
|
.dsc-message-category {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -1096,13 +1098,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-time {
|
.dsc-message-time {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-mmsi {
|
.dsc-message-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--accent-orange);
|
color: var(--accent-orange);
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1122,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-message-pos {
|
.dsc-message-pos {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -1157,7 +1159,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-distress-alert .dsc-alert-mmsi {
|
.dsc-distress-alert .dsc-alert-mmsi {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -1177,7 +1179,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dsc-distress-alert .dsc-alert-position {
|
.dsc-distress-alert .dsc-alert-position {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -1188,7 +1190,7 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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); }
|
||||||
+1934
-1934
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: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
|
|
||||||
|
/* Font sizes */
|
||||||
|
--text-xs: 10px;
|
||||||
|
--text-sm: 12px;
|
||||||
|
--text-base: 14px;
|
||||||
|
--text-lg: 16px;
|
||||||
|
--text-xl: 18px;
|
||||||
|
--text-2xl: 20px;
|
||||||
|
--text-3xl: 24px;
|
||||||
|
--text-4xl: 30px;
|
||||||
|
|
||||||
|
/* Font weights */
|
||||||
|
--font-normal: 400;
|
||||||
|
--font-medium: 500;
|
||||||
|
--font-semibold: 600;
|
||||||
|
--font-bold: 700;
|
||||||
|
|
||||||
|
/* Line heights */
|
||||||
|
--leading-tight: 1.25;
|
||||||
|
--leading-normal: 1.5;
|
||||||
|
--leading-relaxed: 1.75;
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BORDERS & RADIUS
|
||||||
|
============================================ */
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 6px;
|
||||||
|
--radius-lg: 8px;
|
||||||
|
--radius-xl: 12px;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SHADOWS
|
||||||
|
============================================ */
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||||
|
--shadow-glow: 0 0 20px rgba(74, 158, 255, 0.15);
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TRANSITIONS
|
||||||
|
============================================ */
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 200ms ease;
|
||||||
|
--transition-slow: 300ms ease;
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Z-INDEX SCALE
|
||||||
|
============================================ */
|
||||||
|
--z-base: 0;
|
||||||
|
--z-dropdown: 100;
|
||||||
|
--z-sticky: 200;
|
||||||
|
--z-fixed: 300;
|
||||||
|
--z-modal-backdrop: 400;
|
||||||
|
--z-modal: 500;
|
||||||
|
--z-toast: 600;
|
||||||
|
--z-tooltip: 700;
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LAYOUT
|
||||||
|
============================================ */
|
||||||
|
--header-height: 60px;
|
||||||
|
--nav-height: 44px;
|
||||||
|
--sidebar-width: 280px;
|
||||||
|
--stats-strip-height: 36px;
|
||||||
|
--content-max-width: 1400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LIGHT THEME OVERRIDES
|
||||||
|
============================================ */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg-primary: #f8fafc;
|
||||||
|
--bg-secondary: #f1f5f9;
|
||||||
|
--bg-tertiary: #e2e8f0;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--bg-elevated: #f8fafc;
|
||||||
|
--bg-overlay: rgba(255, 255, 255, 0.9);
|
||||||
|
|
||||||
|
/* Background aliases for components */
|
||||||
|
--bg-dark: var(--bg-primary);
|
||||||
|
--bg-panel: var(--bg-secondary);
|
||||||
|
|
||||||
|
--accent-cyan: #2563eb;
|
||||||
|
--accent-cyan-dim: rgba(37, 99, 235, 0.1);
|
||||||
|
--accent-cyan-hover: #1d4ed8;
|
||||||
|
--accent-green: #16a34a;
|
||||||
|
--accent-green-dim: rgba(22, 163, 74, 0.1);
|
||||||
|
--accent-red: #dc2626;
|
||||||
|
--accent-red-dim: rgba(220, 38, 38, 0.1);
|
||||||
|
--accent-orange: #d97706;
|
||||||
|
--accent-orange-dim: rgba(217, 119, 6, 0.1);
|
||||||
|
--accent-amber: #b45309;
|
||||||
|
--accent-amber-dim: rgba(180, 83, 9, 0.1);
|
||||||
|
|
||||||
|
--text-primary: #0f172a;
|
||||||
|
--text-secondary: #475569;
|
||||||
|
--text-dim: #94a3b8;
|
||||||
|
--text-muted: #cbd5e1;
|
||||||
|
--text-inverse: #f8fafc;
|
||||||
|
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--border-light: #cbd5e1;
|
||||||
|
--border-glow: rgba(37, 99, 235, 0.15);
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-glow: 0 0 20px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
REDUCED MOTION
|
||||||
|
============================================ */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
:root {
|
||||||
|
--transition-fast: 0ms;
|
||||||
|
--transition-base: 0ms;
|
||||||
|
--transition-slow: 0ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
-67
@@ -1,67 +1,67 @@
|
|||||||
/* Local font declarations for offline mode */
|
/* Local font declarations for offline mode */
|
||||||
|
|
||||||
/* Inter - Primary UI font */
|
/* Inter - Primary UI font */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
|
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
|
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
|
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
|
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* JetBrains Mono - Monospace/code font */
|
/* JetBrains Mono - Monospace/code font */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
|
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
|
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
/* ============================================
|
||||||
|
Global Navigation Styles
|
||||||
|
Shared across all pages using nav.html
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Icon base (kept lightweight for nav usage) */
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon--sm {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mode Navigation Bar */
|
||||||
|
.mode-nav {
|
||||||
|
display: none;
|
||||||
|
background: linear-gradient(180deg, rgba(17, 22, 32, 0.92), rgba(15, 20, 28, 0.88));
|
||||||
|
border-bottom: 1px solid var(--border-color, #202833);
|
||||||
|
padding: 0 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.mode-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-secondary, #b7c1cf);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--border-color, #202833);
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary, #b7c1cf);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn .nav-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn:hover {
|
||||||
|
background: rgba(27, 36, 51, 0.8);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
border-color: var(--border-color, #202833);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn.active {
|
||||||
|
background: rgba(27, 36, 51, 0.9);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
border-color: var(--accent-cyan, #4d7dbf);
|
||||||
|
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn.active .nav-icon {
|
||||||
|
color: var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: rgba(24, 31, 44, 0.85);
|
||||||
|
border: 1px solid var(--border-light, #2b3645);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-action-btn .nav-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-action-btn:hover {
|
||||||
|
background: rgba(27, 36, 51, 0.95);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
box-shadow: 0 8px 16px rgba(5, 9, 15, 0.35);
|
||||||
|
border-color: var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Navigation */
|
||||||
|
.mode-nav-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary, #b7c1cf);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-btn .nav-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-btn .dropdown-arrow {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-btn .dropdown-arrow svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-btn:hover {
|
||||||
|
background: rgba(27, 36, 51, 0.8);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
border-color: var(--border-color, #202833);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
|
||||||
|
background: rgba(27, 36, 51, 0.9);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
border-color: var(--border-color, #202833);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown.open .dropdown-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
|
||||||
|
background: rgba(27, 36, 51, 0.9);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
border-color: var(--accent-cyan, #4d7dbf);
|
||||||
|
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
|
||||||
|
color: var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
min-width: 180px;
|
||||||
|
background: rgba(16, 22, 32, 0.98);
|
||||||
|
border: 1px solid var(--border-color, #202833);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 16px 36px rgba(5, 9, 15, 0.55);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-menu .mode-nav-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-menu .mode-nav-btn:hover {
|
||||||
|
background: rgba(27, 36, 51, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-dropdown-menu .mode-nav-btn.active {
|
||||||
|
background: rgba(27, 36, 51, 0.95);
|
||||||
|
color: var(--text-primary, #e7ebf2);
|
||||||
|
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav Bar Utilities */
|
||||||
|
.nav-utilities {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.nav-utilities {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-clock {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-clock .utc-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-dim, #8a97a8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-clock .utc-time {
|
||||||
|
color: var(--accent-cyan, #4d7dbf);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--border-color, #202833);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tool-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(20, 33, 53, 0.6);
|
||||||
|
border: 1px solid rgba(77, 125, 191, 0.12);
|
||||||
|
color: var(--text-secondary, #b7c1cf);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tool-btn:hover {
|
||||||
|
background: rgba(27, 36, 51, 0.9);
|
||||||
|
border-color: var(--accent-cyan, #4d7dbf);
|
||||||
|
color: var(--accent-cyan, #4d7dbf);
|
||||||
|
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-nav-btn:focus-visible,
|
||||||
|
.mode-nav-dropdown-btn:focus-visible,
|
||||||
|
.nav-action-btn:focus-visible,
|
||||||
|
.nav-tool-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-cyan, #4d7dbf);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
+215
-294
File diff suppressed because it is too large
Load Diff
@@ -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: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
--bg-dark: #0a0c10;
|
--bg-dark: #0a0c10;
|
||||||
--bg-panel: #0f1218;
|
--bg-panel: #0f1218;
|
||||||
--bg-card: #151a23;
|
--bg-card: #151a23;
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: var(--font-sans);
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -93,7 +95,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-sans);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
@@ -142,7 +144,7 @@ body {
|
|||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +166,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +459,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-value {
|
.telemetry-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -543,7 +545,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pass-time {
|
.pass-time {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom controls bar */
|
/* Bottom controls bar */
|
||||||
@@ -579,7 +581,7 @@ body {
|
|||||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,4 +750,4 @@ body.embedded .panel {
|
|||||||
|
|
||||||
body.embedded .controls-bar {
|
body.embedded .controls-bar {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
+444
-444
@@ -1,444 +1,444 @@
|
|||||||
/* Settings Modal Styles */
|
/* Settings Modal Styles */
|
||||||
|
|
||||||
.settings-modal {
|
.settings-modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-modal.active {
|
.settings-modal.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-content {
|
.settings-content {
|
||||||
background: var(--bg-dark, #0a0a0f);
|
background: var(--bg-dark, #0a0a0f);
|
||||||
border: 1px solid var(--border-color, #1a1a2e);
|
border: 1px solid var(--border-color, #1a1a2e);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header h2 {
|
.settings-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header h2 .icon {
|
.settings-header h2 .icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-close {
|
.settings-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-close:hover {
|
.settings-close:hover {
|
||||||
color: var(--accent-red, #ff4444);
|
color: var(--accent-red, #ff4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Tabs */
|
/* Settings Tabs */
|
||||||
.settings-tabs {
|
.settings-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
border-bottom: 1px solid var(--border-color, #1a1a2e);
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab {
|
.settings-tab {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab:hover {
|
.settings-tab:hover {
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab.active {
|
.settings-tab.active {
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab.active::after {
|
.settings-tab.active::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--accent-cyan, #00d4ff);
|
background: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Sections */
|
/* Settings Sections */
|
||||||
.settings-section {
|
.settings-section {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section.active {
|
.settings-section.active {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group {
|
.settings-group {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group:last-child {
|
.settings-group:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group-title {
|
.settings-group-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Row */
|
/* Settings Row */
|
||||||
.settings-row {
|
.settings-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-row:last-child {
|
.settings-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label {
|
.settings-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label-text {
|
.settings-label-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-label-desc {
|
.settings-label-desc {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle Switch */
|
/* Toggle Switch */
|
||||||
.toggle-switch {
|
.toggle-switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input {
|
.toggle-switch input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-slider {
|
.toggle-slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: var(--bg-tertiary, #1a1a2e);
|
background-color: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-slider:before {
|
.toggle-slider:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
background-color: var(--text-muted, #666);
|
background-color: var(--text-muted, #666);
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:checked + .toggle-slider {
|
.toggle-switch input:checked + .toggle-slider {
|
||||||
background-color: var(--accent-cyan, #00d4ff);
|
background-color: var(--accent-cyan, #00d4ff);
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:checked + .toggle-slider:before {
|
.toggle-switch input:checked + .toggle-slider:before {
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch input:focus + .toggle-slider {
|
.toggle-switch input:focus + .toggle-slider {
|
||||||
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
|
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Dropdown */
|
/* Select Dropdown */
|
||||||
.settings-select {
|
.settings-select {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 8px center;
|
background-position: right 8px center;
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-select:focus {
|
.settings-select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text Input */
|
/* Text Input */
|
||||||
.settings-input {
|
.settings-input {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-input:focus {
|
.settings-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-input::placeholder {
|
.settings-input::placeholder {
|
||||||
color: var(--text-muted, #666);
|
color: var(--text-muted, #666);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Asset Status */
|
/* Asset Status */
|
||||||
.asset-status {
|
.asset-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--bg-secondary, #0f0f1a);
|
background: var(--bg-secondary, #0f0f1a);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-status-row {
|
.asset-status-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-name {
|
.asset-name {
|
||||||
color: var(--text-muted, #888);
|
color: var(--text-muted, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge {
|
.asset-badge {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.available {
|
.asset-badge.available {
|
||||||
background: rgba(0, 255, 136, 0.15);
|
background: rgba(0, 255, 136, 0.15);
|
||||||
color: var(--accent-green, #00ff88);
|
color: var(--accent-green, #00ff88);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.missing {
|
.asset-badge.missing {
|
||||||
background: rgba(255, 68, 68, 0.15);
|
background: rgba(255, 68, 68, 0.15);
|
||||||
color: var(--accent-red, #ff4444);
|
color: var(--accent-red, #ff4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-badge.checking {
|
.asset-badge.checking {
|
||||||
background: rgba(255, 170, 0, 0.15);
|
background: rgba(255, 170, 0, 0.15);
|
||||||
color: var(--accent-orange, #ffaa00);
|
color: var(--accent-orange, #ffaa00);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check Assets Button */
|
/* Check Assets Button */
|
||||||
.check-assets-btn {
|
.check-assets-btn {
|
||||||
background: var(--bg-tertiary, #1a1a2e);
|
background: var(--bg-tertiary, #1a1a2e);
|
||||||
border: 1px solid var(--border-color, #2a2a3e);
|
border: 1px solid var(--border-color, #2a2a3e);
|
||||||
color: var(--text-primary, #e0e0e0);
|
color: var(--text-primary, #e0e0e0);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-assets-btn:hover {
|
.check-assets-btn:hover {
|
||||||
border-color: var(--accent-cyan, #00d4ff);
|
border-color: var(--accent-cyan, #00d4ff);
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-assets-btn:disabled {
|
.check-assets-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GPS Detection Spinner */
|
/* GPS Detection Spinner */
|
||||||
.detecting-spinner {
|
.detecting-spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border: 2px solid currentColor;
|
border: 2px solid currentColor;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: detecting-spin 0.8s linear infinite;
|
animation: detecting-spin 0.8s linear infinite;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes detecting-spin {
|
@keyframes detecting-spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* About Section */
|
/* About Section */
|
||||||
.about-info {
|
.about-info {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-muted, #888);
|
color: var(--text-muted, #888);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-info p {
|
.about-info p {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-info a {
|
.about-info a {
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-info a:hover {
|
.about-info a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-version {
|
.about-version {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Donate Button */
|
/* Donate Button */
|
||||||
.donate-btn {
|
.donate-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
|
background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
|
box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-btn:hover {
|
.donate-btn:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
|
box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4);
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-btn:active {
|
.donate-btn:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tile Provider Custom URL */
|
/* Tile Provider Custom URL */
|
||||||
.custom-url-row {
|
.custom-url-row {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-url-row .settings-input {
|
.custom-url-row .settings-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Info Callout */
|
/* Info Callout */
|
||||||
.settings-info {
|
.settings-info {
|
||||||
background: rgba(0, 212, 255, 0.1);
|
background: rgba(0, 212, 255, 0.1);
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted, #888);
|
color: var(--text-muted, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-info strong {
|
.settings-info strong {
|
||||||
color: var(--accent-cyan, #00d4ff);
|
color: var(--accent-cyan, #00d4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.settings-modal.active {
|
.settings-modal.active {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-content {
|
.settings-content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-row {
|
.settings-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-select,
|
.settings-select,
|
||||||
.settings-input {
|
.settings-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+494
-494
@@ -1,36 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* Intercept - Core Application Logic
|
* Intercept - Core Application Logic
|
||||||
* Global state, mode switching, and shared functionality
|
* Global state, mode switching, and shared functionality
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============== GLOBAL STATE ==============
|
// ============== GLOBAL STATE ==============
|
||||||
|
|
||||||
// Mode state flags
|
// Mode state flags
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isSensorRunning = false;
|
let isSensorRunning = false;
|
||||||
let isAdsbRunning = false;
|
let isAdsbRunning = false;
|
||||||
let isWifiRunning = false;
|
let isWifiRunning = false;
|
||||||
let isBtRunning = false;
|
let isBtRunning = false;
|
||||||
let currentMode = 'pager';
|
let currentMode = 'pager';
|
||||||
|
|
||||||
// Message counters
|
// Message counters
|
||||||
let msgCount = 0;
|
let msgCount = 0;
|
||||||
let pocsagCount = 0;
|
let pocsagCount = 0;
|
||||||
let flexCount = 0;
|
let flexCount = 0;
|
||||||
let sensorCount = 0;
|
let sensorCount = 0;
|
||||||
let filteredCount = 0;
|
let filteredCount = 0;
|
||||||
|
|
||||||
// Device list (populated from server via Jinja2)
|
// Device list (populated from server via Jinja2)
|
||||||
let deviceList = [];
|
let deviceList = [];
|
||||||
|
|
||||||
// Auto-scroll setting
|
// Auto-scroll setting
|
||||||
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
|
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
|
||||||
|
|
||||||
// Mute setting
|
// Mute setting
|
||||||
let muted = localStorage.getItem('audioMuted') === 'true';
|
let muted = localStorage.getItem('audioMuted') === 'true';
|
||||||
|
|
||||||
// Observer location (load from localStorage or default to London)
|
// Observer location (load from localStorage or default to London)
|
||||||
let observerLocation = (function() {
|
let observerLocation = (function() {
|
||||||
if (window.ObserverLocation && ObserverLocation.getForModule) {
|
if (window.ObserverLocation && ObserverLocation.getForModule) {
|
||||||
return ObserverLocation.getForModule('observerLocation');
|
return ObserverLocation.getForModule('observerLocation');
|
||||||
@@ -44,464 +44,464 @@ let observerLocation = (function() {
|
|||||||
}
|
}
|
||||||
return { lat: 51.5074, lon: -0.1278 };
|
return { lat: 51.5074, lon: -0.1278 };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Message storage for export
|
// Message storage for export
|
||||||
let allMessages = [];
|
let allMessages = [];
|
||||||
|
|
||||||
// Track unique sensor devices
|
// Track unique sensor devices
|
||||||
let uniqueDevices = new Set();
|
let uniqueDevices = new Set();
|
||||||
|
|
||||||
// SDR device usage tracking
|
// SDR device usage tracking
|
||||||
let sdrDeviceUsage = {};
|
let sdrDeviceUsage = {};
|
||||||
|
|
||||||
// ============== DISCLAIMER HANDLING ==============
|
// ============== DISCLAIMER HANDLING ==============
|
||||||
|
|
||||||
function checkDisclaimer() {
|
function checkDisclaimer() {
|
||||||
const accepted = localStorage.getItem('disclaimerAccepted');
|
const accepted = localStorage.getItem('disclaimerAccepted');
|
||||||
if (accepted === 'true') {
|
if (accepted === 'true') {
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function acceptDisclaimer() {
|
function acceptDisclaimer() {
|
||||||
localStorage.setItem('disclaimerAccepted', 'true');
|
localStorage.setItem('disclaimerAccepted', 'true');
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function declineDisclaimer() {
|
function declineDisclaimer() {
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
|
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== HEADER CLOCK ==============
|
// ============== HEADER CLOCK ==============
|
||||||
|
|
||||||
function updateHeaderClock() {
|
function updateHeaderClock() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const utc = now.toISOString().substring(11, 19);
|
const utc = now.toISOString().substring(11, 19);
|
||||||
document.getElementById('headerUtcTime').textContent = utc;
|
document.getElementById('headerUtcTime').textContent = utc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== MODE SWITCHING ==============
|
// ============== MODE SWITCHING ==============
|
||||||
|
|
||||||
function switchMode(mode) {
|
function switchMode(mode) {
|
||||||
// Stop any running scans when switching modes
|
// Stop any running scans when switching modes
|
||||||
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
|
if (isRunning && typeof stopDecoding === 'function') stopDecoding();
|
||||||
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
|
if (isSensorRunning && typeof stopSensorDecoding === 'function') stopSensorDecoding();
|
||||||
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
|
if (isWifiRunning && typeof stopWifiScan === 'function') stopWifiScan();
|
||||||
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
|
if (isBtRunning && typeof stopBtScan === 'function') stopBtScan();
|
||||||
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
|
if (isAdsbRunning && typeof stopAdsbScan === 'function') stopAdsbScan();
|
||||||
|
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
|
|
||||||
// Remove active from all nav buttons, then add to the correct one
|
// Remove active from all nav buttons, then add to the correct one
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
const modeMap = {
|
const modeMap = {
|
||||||
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
||||||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||||||
'listening': 'listening', 'meshtastic': 'meshtastic'
|
'listening': 'listening', 'meshtastic': 'meshtastic'
|
||||||
};
|
};
|
||||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||||
const label = btn.querySelector('.nav-label');
|
const label = btn.querySelector('.nav-label');
|
||||||
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
|
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle mode content visibility
|
// Toggle mode content visibility
|
||||||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||||||
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
||||||
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
|
document.getElementById('aircraftMode')?.classList.toggle('active', mode === 'aircraft');
|
||||||
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
||||||
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
||||||
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
||||||
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
||||||
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
||||||
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
||||||
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
|
document.getElementById('rtlamrMode')?.classList.toggle('active', mode === 'rtlamr');
|
||||||
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
||||||
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
|
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
|
||||||
|
|
||||||
// Toggle stats visibility
|
// Toggle stats visibility
|
||||||
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
||||||
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
||||||
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
||||||
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
||||||
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
|
|
||||||
// Hide signal meter - individual panels show signal strength where needed
|
// Hide signal meter - individual panels show signal strength where needed
|
||||||
document.getElementById('signalMeter').style.display = 'none';
|
document.getElementById('signalMeter').style.display = 'none';
|
||||||
|
|
||||||
// Show/hide dashboard buttons in nav bar
|
// Show/hide dashboard buttons in nav bar
|
||||||
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
|
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
|
||||||
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
|
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
|
||||||
|
|
||||||
// Update active mode indicator
|
// Update active mode indicator
|
||||||
const modeNames = {
|
const modeNames = {
|
||||||
'pager': 'PAGER',
|
'pager': 'PAGER',
|
||||||
'sensor': '433MHZ',
|
'sensor': '433MHZ',
|
||||||
'aircraft': 'AIRCRAFT',
|
'aircraft': 'AIRCRAFT',
|
||||||
'satellite': 'SATELLITE',
|
'satellite': 'SATELLITE',
|
||||||
'wifi': 'WIFI',
|
'wifi': 'WIFI',
|
||||||
'bluetooth': 'BLUETOOTH',
|
'bluetooth': 'BLUETOOTH',
|
||||||
'listening': 'LISTENING POST',
|
'listening': 'LISTENING POST',
|
||||||
'tscm': 'TSCM',
|
'tscm': 'TSCM',
|
||||||
'aprs': 'APRS',
|
'aprs': 'APRS',
|
||||||
'meshtastic': 'MESHTASTIC'
|
'meshtastic': 'MESHTASTIC'
|
||||||
};
|
};
|
||||||
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
||||||
|
|
||||||
// Update mobile nav buttons
|
// Update mobile nav buttons
|
||||||
updateMobileNavButtons(mode);
|
updateMobileNavButtons(mode);
|
||||||
|
|
||||||
// Close mobile drawer when mode is switched (on mobile)
|
// Close mobile drawer when mode is switched (on mobile)
|
||||||
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
|
if (window.innerWidth < 1024 && typeof window.closeMobileDrawer === 'function') {
|
||||||
window.closeMobileDrawer();
|
window.closeMobileDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle layout containers
|
// Toggle layout containers
|
||||||
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||||
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||||||
|
|
||||||
// Respect the "Show Radar Display" checkbox for aircraft mode
|
// Respect the "Show Radar Display" checkbox for aircraft mode
|
||||||
const showRadar = document.getElementById('adsbEnableMap')?.checked;
|
const showRadar = document.getElementById('adsbEnableMap')?.checked;
|
||||||
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
||||||
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
|
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
|
||||||
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
|
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
|
||||||
|
|
||||||
// Update output panel title based on mode
|
// Update output panel title based on mode
|
||||||
const titles = {
|
const titles = {
|
||||||
'pager': 'Pager Decoder',
|
'pager': 'Pager Decoder',
|
||||||
'sensor': '433MHz Sensor Monitor',
|
'sensor': '433MHz Sensor Monitor',
|
||||||
'aircraft': 'ADS-B Aircraft Tracker',
|
'aircraft': 'ADS-B Aircraft Tracker',
|
||||||
'satellite': 'Satellite Monitor',
|
'satellite': 'Satellite Monitor',
|
||||||
'wifi': 'WiFi Scanner',
|
'wifi': 'WiFi Scanner',
|
||||||
'bluetooth': 'Bluetooth Scanner',
|
'bluetooth': 'Bluetooth Scanner',
|
||||||
'listening': 'Listening Post',
|
'listening': 'Listening Post',
|
||||||
'meshtastic': 'Meshtastic Mesh Monitor'
|
'meshtastic': 'Meshtastic Mesh Monitor'
|
||||||
};
|
};
|
||||||
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
||||||
|
|
||||||
// Show/hide Device Intelligence for modes that use it
|
// Show/hide Device Intelligence for modes that use it
|
||||||
const reconBtn = document.getElementById('reconBtn');
|
const reconBtn = document.getElementById('reconBtn');
|
||||||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||||||
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
|
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
|
||||||
document.getElementById('reconPanel').style.display = 'none';
|
document.getElementById('reconPanel').style.display = 'none';
|
||||||
if (reconBtn) reconBtn.style.display = 'none';
|
if (reconBtn) reconBtn.style.display = 'none';
|
||||||
if (intelBtn) intelBtn.style.display = 'none';
|
if (intelBtn) intelBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
if (reconBtn) reconBtn.style.display = 'inline-block';
|
if (reconBtn) reconBtn.style.display = 'inline-block';
|
||||||
if (intelBtn) intelBtn.style.display = 'inline-block';
|
if (intelBtn) intelBtn.style.display = 'inline-block';
|
||||||
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
|
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
|
||||||
document.getElementById('reconPanel').style.display = 'block';
|
document.getElementById('reconPanel').style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show RTL-SDR device section for modes that use it
|
// Show RTL-SDR device section for modes that use it
|
||||||
document.getElementById('rtlDeviceSection').style.display =
|
document.getElementById('rtlDeviceSection').style.display =
|
||||||
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft' || mode === 'listening') ? 'block' : 'none';
|
||||||
|
|
||||||
// Toggle mode-specific tool status displays
|
// Toggle mode-specific tool status displays
|
||||||
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
||||||
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
|
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
|
||||||
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
|
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
|
||||||
|
|
||||||
// Hide waterfall and output console for modes with their own visualizations
|
// Hide waterfall and output console for modes with their own visualizations
|
||||||
document.querySelector('.waterfall-container').style.display =
|
document.querySelector('.waterfall-container').style.display =
|
||||||
(mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
(mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
||||||
document.getElementById('output').style.display =
|
document.getElementById('output').style.display =
|
||||||
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block';
|
||||||
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
|
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
|
||||||
|
|
||||||
// Load interfaces and initialize visualizations when switching modes
|
// Load interfaces and initialize visualizations when switching modes
|
||||||
if (mode === 'wifi') {
|
if (mode === 'wifi') {
|
||||||
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
|
if (typeof refreshWifiInterfaces === 'function') refreshWifiInterfaces();
|
||||||
if (typeof initRadar === 'function') initRadar();
|
if (typeof initRadar === 'function') initRadar();
|
||||||
if (typeof initWatchList === 'function') initWatchList();
|
if (typeof initWatchList === 'function') initWatchList();
|
||||||
} else if (mode === 'bluetooth') {
|
} else if (mode === 'bluetooth') {
|
||||||
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
|
if (typeof refreshBtInterfaces === 'function') refreshBtInterfaces();
|
||||||
if (typeof initBtRadar === 'function') initBtRadar();
|
if (typeof initBtRadar === 'function') initBtRadar();
|
||||||
} else if (mode === 'aircraft') {
|
} else if (mode === 'aircraft') {
|
||||||
if (typeof checkAdsbTools === 'function') checkAdsbTools();
|
if (typeof checkAdsbTools === 'function') checkAdsbTools();
|
||||||
if (typeof initAircraftRadar === 'function') initAircraftRadar();
|
if (typeof initAircraftRadar === 'function') initAircraftRadar();
|
||||||
} else if (mode === 'satellite') {
|
} else if (mode === 'satellite') {
|
||||||
if (typeof initPolarPlot === 'function') initPolarPlot();
|
if (typeof initPolarPlot === 'function') initPolarPlot();
|
||||||
if (typeof initSatelliteList === 'function') initSatelliteList();
|
if (typeof initSatelliteList === 'function') initSatelliteList();
|
||||||
} else if (mode === 'listening') {
|
} else if (mode === 'listening') {
|
||||||
if (typeof checkScannerTools === 'function') checkScannerTools();
|
if (typeof checkScannerTools === 'function') checkScannerTools();
|
||||||
if (typeof checkAudioTools === 'function') checkAudioTools();
|
if (typeof checkAudioTools === 'function') checkAudioTools();
|
||||||
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
|
if (typeof populateScannerDeviceSelect === 'function') populateScannerDeviceSelect();
|
||||||
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
|
if (typeof populateAudioDeviceSelect === 'function') populateAudioDeviceSelect();
|
||||||
} else if (mode === 'meshtastic') {
|
} else if (mode === 'meshtastic') {
|
||||||
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
|
if (typeof Meshtastic !== 'undefined' && Meshtastic.init) Meshtastic.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== SECTION COLLAPSE ==============
|
// ============== SECTION COLLAPSE ==============
|
||||||
|
|
||||||
function toggleSection(el) {
|
function toggleSection(el) {
|
||||||
el.closest('.section').classList.toggle('collapsed');
|
el.closest('.section').classList.toggle('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== THEME MANAGEMENT ==============
|
// ============== THEME MANAGEMENT ==============
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
const currentTheme = html.getAttribute('data-theme');
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
html.setAttribute('data-theme', newTheme);
|
html.setAttribute('data-theme', newTheme);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
// Update button text
|
// Update button text
|
||||||
const btn = document.getElementById('themeToggle');
|
const btn = document.getElementById('themeToggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
btn.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTheme() {
|
function loadTheme() {
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
const btn = document.getElementById('themeToggle');
|
const btn = document.getElementById('themeToggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
|
btn.textContent = savedTheme === 'light' ? '🌙' : '☀️';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== AUTO-SCROLL ==============
|
// ============== AUTO-SCROLL ==============
|
||||||
|
|
||||||
function toggleAutoScroll() {
|
function toggleAutoScroll() {
|
||||||
autoScroll = !autoScroll;
|
autoScroll = !autoScroll;
|
||||||
localStorage.setItem('autoScroll', autoScroll);
|
localStorage.setItem('autoScroll', autoScroll);
|
||||||
updateAutoScrollButton();
|
updateAutoScrollButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAutoScrollButton() {
|
function updateAutoScrollButton() {
|
||||||
const btn = document.getElementById('autoScrollBtn');
|
const btn = document.getElementById('autoScrollBtn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
|
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
|
||||||
btn.classList.toggle('active', autoScroll);
|
btn.classList.toggle('active', autoScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== SDR DEVICE MANAGEMENT ==============
|
// ============== SDR DEVICE MANAGEMENT ==============
|
||||||
|
|
||||||
function getSelectedDevice() {
|
function getSelectedDevice() {
|
||||||
return document.getElementById('deviceSelect').value;
|
return document.getElementById('deviceSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedSDRType() {
|
function getSelectedSDRType() {
|
||||||
return document.getElementById('sdrTypeSelect').value;
|
return document.getElementById('sdrTypeSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reserveDevice(deviceIndex, modeId) {
|
function reserveDevice(deviceIndex, modeId) {
|
||||||
sdrDeviceUsage[modeId] = deviceIndex;
|
sdrDeviceUsage[modeId] = deviceIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseDevice(modeId) {
|
function releaseDevice(modeId) {
|
||||||
delete sdrDeviceUsage[modeId];
|
delete sdrDeviceUsage[modeId];
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDeviceAvailability(requestingMode) {
|
function checkDeviceAvailability(requestingMode) {
|
||||||
const selectedDevice = parseInt(getSelectedDevice());
|
const selectedDevice = parseInt(getSelectedDevice());
|
||||||
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
|
for (const [mode, device] of Object.entries(sdrDeviceUsage)) {
|
||||||
if (mode !== requestingMode && device === selectedDevice) {
|
if (mode !== requestingMode && device === selectedDevice) {
|
||||||
alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`);
|
alert(`Device ${selectedDevice} is currently in use by ${mode} mode. Please select a different device or stop the other scan first.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== BIAS-T SETTINGS ==============
|
// ============== BIAS-T SETTINGS ==============
|
||||||
|
|
||||||
function saveBiasTSetting() {
|
function saveBiasTSetting() {
|
||||||
const enabled = document.getElementById('biasT')?.checked || false;
|
const enabled = document.getElementById('biasT')?.checked || false;
|
||||||
localStorage.setItem('biasTEnabled', enabled);
|
localStorage.setItem('biasTEnabled', enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBiasTEnabled() {
|
function getBiasTEnabled() {
|
||||||
return document.getElementById('biasT')?.checked || false;
|
return document.getElementById('biasT')?.checked || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBiasTSetting() {
|
function loadBiasTSetting() {
|
||||||
const saved = localStorage.getItem('biasTEnabled');
|
const saved = localStorage.getItem('biasTEnabled');
|
||||||
if (saved === 'true') {
|
if (saved === 'true') {
|
||||||
const checkbox = document.getElementById('biasT');
|
const checkbox = document.getElementById('biasT');
|
||||||
if (checkbox) checkbox.checked = true;
|
if (checkbox) checkbox.checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== REMOTE SDR ==============
|
// ============== REMOTE SDR ==============
|
||||||
|
|
||||||
function toggleRemoteSDR() {
|
function toggleRemoteSDR() {
|
||||||
const useRemote = document.getElementById('useRemoteSDR').checked;
|
const useRemote = document.getElementById('useRemoteSDR').checked;
|
||||||
const configDiv = document.getElementById('remoteSDRConfig');
|
const configDiv = document.getElementById('remoteSDRConfig');
|
||||||
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
|
const localControls = document.querySelectorAll('#sdrTypeSelect, #deviceSelect');
|
||||||
|
|
||||||
if (useRemote) {
|
if (useRemote) {
|
||||||
configDiv.style.display = 'block';
|
configDiv.style.display = 'block';
|
||||||
localControls.forEach(el => el.disabled = true);
|
localControls.forEach(el => el.disabled = true);
|
||||||
} else {
|
} else {
|
||||||
configDiv.style.display = 'none';
|
configDiv.style.display = 'none';
|
||||||
localControls.forEach(el => el.disabled = false);
|
localControls.forEach(el => el.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteSDRConfig() {
|
function getRemoteSDRConfig() {
|
||||||
const useRemote = document.getElementById('useRemoteSDR')?.checked;
|
const useRemote = document.getElementById('useRemoteSDR')?.checked;
|
||||||
if (!useRemote) return null;
|
if (!useRemote) return null;
|
||||||
|
|
||||||
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
|
const host = document.getElementById('rtlTcpHost')?.value || 'localhost';
|
||||||
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
|
const port = parseInt(document.getElementById('rtlTcpPort')?.value || '1234');
|
||||||
|
|
||||||
if (!host || isNaN(port)) {
|
if (!host || isNaN(port)) {
|
||||||
alert('Please enter valid rtl_tcp host and port');
|
alert('Please enter valid rtl_tcp host and port');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { host, port };
|
return { host, port };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== OUTPUT DISPLAY ==============
|
// ============== OUTPUT DISPLAY ==============
|
||||||
|
|
||||||
function showInfo(text) {
|
function showInfo(text) {
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
if (!output) return;
|
if (!output) return;
|
||||||
|
|
||||||
const placeholder = output.querySelector('.placeholder');
|
const placeholder = output.querySelector('.placeholder');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
const infoEl = document.createElement('div');
|
const infoEl = document.createElement('div');
|
||||||
infoEl.className = 'info-msg';
|
infoEl.className = 'info-msg';
|
||||||
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
|
||||||
infoEl.textContent = text;
|
infoEl.textContent = text;
|
||||||
output.insertBefore(infoEl, output.firstChild);
|
output.insertBefore(infoEl, output.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(text) {
|
function showError(text) {
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
if (!output) return;
|
if (!output) return;
|
||||||
|
|
||||||
const placeholder = output.querySelector('.placeholder');
|
const placeholder = output.querySelector('.placeholder');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
const errorEl = document.createElement('div');
|
const errorEl = document.createElement('div');
|
||||||
errorEl.className = 'error-msg';
|
errorEl.className = 'error-msg';
|
||||||
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
|
||||||
errorEl.textContent = '⚠ ' + text;
|
errorEl.textContent = '⚠ ' + text;
|
||||||
output.insertBefore(errorEl, output.firstChild);
|
output.insertBefore(errorEl, output.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== INITIALIZATION ==============
|
// ============== INITIALIZATION ==============
|
||||||
|
|
||||||
// ============== MOBILE NAVIGATION ==============
|
// ============== MOBILE NAVIGATION ==============
|
||||||
|
|
||||||
function initMobileNav() {
|
function initMobileNav() {
|
||||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||||
const sidebar = document.getElementById('mainSidebar');
|
const sidebar = document.getElementById('mainSidebar');
|
||||||
const overlay = document.getElementById('drawerOverlay');
|
const overlay = document.getElementById('drawerOverlay');
|
||||||
|
|
||||||
if (!hamburgerBtn || !sidebar || !overlay) return;
|
if (!hamburgerBtn || !sidebar || !overlay) return;
|
||||||
|
|
||||||
function openDrawer() {
|
function openDrawer() {
|
||||||
sidebar.classList.add('open');
|
sidebar.classList.add('open');
|
||||||
overlay.classList.add('visible');
|
overlay.classList.add('visible');
|
||||||
hamburgerBtn.classList.add('active');
|
hamburgerBtn.classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
overlay.classList.remove('visible');
|
overlay.classList.remove('visible');
|
||||||
hamburgerBtn.classList.remove('active');
|
hamburgerBtn.classList.remove('active');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDrawer() {
|
function toggleDrawer() {
|
||||||
if (sidebar.classList.contains('open')) {
|
if (sidebar.classList.contains('open')) {
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
} else {
|
} else {
|
||||||
openDrawer();
|
openDrawer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hamburgerBtn.addEventListener('click', toggleDrawer);
|
hamburgerBtn.addEventListener('click', toggleDrawer);
|
||||||
overlay.addEventListener('click', closeDrawer);
|
overlay.addEventListener('click', closeDrawer);
|
||||||
|
|
||||||
// Close drawer when resizing to desktop
|
// Close drawer when resizing to desktop
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (window.innerWidth >= 1024) {
|
if (window.innerWidth >= 1024) {
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose for external use
|
// Expose for external use
|
||||||
window.toggleMobileDrawer = toggleDrawer;
|
window.toggleMobileDrawer = toggleDrawer;
|
||||||
window.closeMobileDrawer = closeDrawer;
|
window.closeMobileDrawer = closeDrawer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewportHeight() {
|
function setViewportHeight() {
|
||||||
// Fix for iOS Safari address bar height
|
// Fix for iOS Safari address bar height
|
||||||
const vh = window.innerHeight * 0.01;
|
const vh = window.innerHeight * 0.01;
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMobileNavButtons(mode) {
|
function updateMobileNavButtons(mode) {
|
||||||
// Update mobile nav bar buttons
|
// Update mobile nav bar buttons
|
||||||
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
|
document.querySelectorAll('.mobile-nav-btn').forEach(btn => {
|
||||||
const btnMode = btn.getAttribute('data-mode');
|
const btnMode = btn.getAttribute('data-mode');
|
||||||
btn.classList.toggle('active', btnMode === mode);
|
btn.classList.toggle('active', btnMode === mode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initApp() {
|
function initApp() {
|
||||||
// Check disclaimer
|
// Check disclaimer
|
||||||
checkDisclaimer();
|
checkDisclaimer();
|
||||||
|
|
||||||
// Load theme
|
// Load theme
|
||||||
loadTheme();
|
loadTheme();
|
||||||
|
|
||||||
// Start clock
|
// Start clock
|
||||||
updateHeaderClock();
|
updateHeaderClock();
|
||||||
setInterval(updateHeaderClock, 1000);
|
setInterval(updateHeaderClock, 1000);
|
||||||
|
|
||||||
// Load bias-T setting
|
// Load bias-T setting
|
||||||
loadBiasTSetting();
|
loadBiasTSetting();
|
||||||
|
|
||||||
// Initialize observer location inputs
|
// Initialize observer location inputs
|
||||||
const adsbLatInput = document.getElementById('adsbObsLat');
|
const adsbLatInput = document.getElementById('adsbObsLat');
|
||||||
const adsbLonInput = document.getElementById('adsbObsLon');
|
const adsbLonInput = document.getElementById('adsbObsLon');
|
||||||
const obsLatInput = document.getElementById('obsLat');
|
const obsLatInput = document.getElementById('obsLat');
|
||||||
const obsLonInput = document.getElementById('obsLon');
|
const obsLonInput = document.getElementById('obsLon');
|
||||||
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
|
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
|
||||||
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
|
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
|
||||||
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
|
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
|
||||||
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
|
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
|
||||||
|
|
||||||
// Update UI state
|
// Update UI state
|
||||||
updateAutoScrollButton();
|
updateAutoScrollButton();
|
||||||
|
|
||||||
// Make sections collapsible
|
// Make sections collapsible
|
||||||
document.querySelectorAll('.section h3').forEach(h3 => {
|
document.querySelectorAll('.section h3').forEach(h3 => {
|
||||||
h3.addEventListener('click', function() {
|
h3.addEventListener('click', function() {
|
||||||
this.parentElement.classList.toggle('collapsed');
|
this.parentElement.classList.toggle('collapsed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collapse all sections by default (except SDR Device which is first)
|
// Collapse all sections by default (except SDR Device which is first)
|
||||||
document.querySelectorAll('.section').forEach((section, index) => {
|
document.querySelectorAll('.section').forEach((section, index) => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
section.classList.add('collapsed');
|
section.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize mobile navigation
|
// Initialize mobile navigation
|
||||||
initMobileNav();
|
initMobileNav();
|
||||||
|
|
||||||
// Set viewport height for mobile browsers
|
// Set viewport height for mobile browsers
|
||||||
setViewportHeight();
|
setViewportHeight();
|
||||||
window.addEventListener('resize', setViewportHeight);
|
window.addEventListener('resize', setViewportHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run initialization when DOM is ready
|
// Run initialization when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initApp);
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
+906
-906
File diff suppressed because it is too large
Load Diff
+2951
-2599
File diff suppressed because it is too large
Load Diff
+4904
-4899
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ADS-B History // INTERCEPT</title>
|
<title>ADS-B History // INTERCEPT</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
|
{% else %}
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -22,6 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% set active_mode = 'adsb' %}
|
||||||
|
{% include 'partials/nav.html' with context %}
|
||||||
|
|
||||||
<main class="history-shell">
|
<main class="history-shell">
|
||||||
<section class="summary-strip">
|
<section class="summary-strip">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
@@ -761,5 +769,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+592
-588
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' %}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
<script>
|
<script>
|
||||||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||||||
</script>
|
</script>
|
||||||
@@ -51,6 +52,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% set active_mode = 'ais' %}
|
||||||
|
{% include 'partials/nav.html' with context %}
|
||||||
|
|
||||||
<div class="stats-strip">
|
<div class="stats-strip">
|
||||||
<div class="stats-strip-inner">
|
<div class="stats-strip-inner">
|
||||||
<div class="strip-stat">
|
<div class="strip-stat">
|
||||||
@@ -1495,7 +1499,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.agent-select-sm:focus {
|
.agent-select-sm:focus {
|
||||||
@@ -1549,6 +1553,7 @@
|
|||||||
|
|
||||||
<!-- Agent Manager -->
|
<!-- Agent Manager -->
|
||||||
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
// AIS-specific agent integration
|
// AIS-specific agent integration
|
||||||
let aisCurrentAgent = 'local';
|
let aisCurrentAgent = 'local';
|
||||||
|
|||||||
@@ -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>
|
||||||
+96
-120
@@ -22,7 +22,7 @@
|
|||||||
{% if offline_settings.fonts_source == 'local' %}
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
|
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
|
||||||
{% if offline_settings.assets_source == 'local' %}
|
{% if offline_settings.assets_source == 'local' %}
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
|
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
|
||||||
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
|
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
|
||||||
<p
|
<p
|
||||||
style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #888; text-align: left; margin: 0;">
|
style="font-family: var(--font-mono); font-size: 11px; color: #888; text-align: left; margin: 0;">
|
||||||
<span style="color: var(--accent-red);">root@intercepted:</span><span
|
<span style="color: var(--accent-red);">root@intercepted:</span><span
|
||||||
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
|
style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
|
||||||
<span style="color: #666;">[sudo] password for user: ********</span><br>
|
<span style="color: #666;">[sudo] password for user: ********</span><br>
|
||||||
@@ -348,107 +348,9 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mode Navigation Bar -->
|
<!-- Mode Navigation Bar -->
|
||||||
<nav class="mode-nav">
|
{% set is_index_page = true %}
|
||||||
<div class="mode-nav-dropdown" data-group="sdr">
|
{% set active_mode = 'pager' %}
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('sdr')">
|
{% include 'partials/nav.html' with context %}
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
|
|
||||||
<span class="nav-label">SDR / RF</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn active" onclick="switchMode('pager')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="nav-label">Pager</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span><span class="nav-label">433MHz</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span><span class="nav-label">Meters</span></button>
|
|
||||||
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span><span class="nav-label">Aircraft</span></a>
|
|
||||||
<a href="/ais/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span><span class="nav-label">Vessels</span></a>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span><span class="nav-label">APRS</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="nav-label">Listening Post</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('spystations')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span><span class="nav-label">Spy Stations</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('meshtastic')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span><span class="nav-label">Meshtastic</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="wireless">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('wireless')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
|
||||||
<span class="nav-label">Wireless</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('wifi')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span><span class="nav-label">WiFi</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('bluetooth')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span><span class="nav-label">Bluetooth</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="security">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('security')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
|
||||||
<span class="nav-label">Security</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('tscm')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="nav-label">TSCM</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-dropdown" data-group="space">
|
|
||||||
<button class="mode-nav-dropdown-btn" onclick="toggleNavDropdown('space')">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
|
|
||||||
<span class="nav-label">Space</span>
|
|
||||||
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<div class="mode-nav-dropdown-menu">
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="nav-label">Satellite</span></button>
|
|
||||||
<button class="mode-nav-btn" onclick="switchMode('sstv')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span><span class="nav-label">ISS SSTV</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mode-nav-actions">
|
|
||||||
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn"
|
|
||||||
style="display: none;">
|
|
||||||
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span><span class="nav-label">Full Dashboard</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-utilities">
|
|
||||||
<div class="nav-clock">
|
|
||||||
<span class="utc-label">UTC</span>
|
|
||||||
<span class="utc-time" id="headerUtcTime">--:--:--</span>
|
|
||||||
</div>
|
|
||||||
<div class="nav-divider"></div>
|
|
||||||
<div class="nav-tools">
|
|
||||||
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
|
|
||||||
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
|
|
||||||
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
|
|
||||||
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
|
|
||||||
<span class="icon-sun icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
|
|
||||||
</button>
|
|
||||||
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span></a>
|
|
||||||
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span></a>
|
|
||||||
<button class="nav-tool-btn" onclick="showSettings()" title="Settings"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span></button>
|
|
||||||
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="nav-tool-btn nav-tool-btn--donate" title="Support the Project"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg></span></a>
|
|
||||||
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
|
||||||
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
|
|
||||||
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile Navigation Bar (simplified mode switching) -->
|
|
||||||
<nav class="mobile-nav-bar" id="mobileNavBar">
|
|
||||||
<button class="mobile-nav-btn active" data-mode="pager" onclick="switchMode('pager')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span> Pager</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span> 433MHz</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Meters</button>
|
|
||||||
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span> Aircraft</a>
|
|
||||||
<a href="/ais/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg></span> Vessels</a>
|
|
||||||
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span> APRS</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> WiFi</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="bluetooth" onclick="switchMode('bluetooth')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span> BT</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="tscm" onclick="switchMode('tscm')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span> TSCM</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="satellite" onclick="switchMode('satellite')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg></span> Sat</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="sstv" onclick="switchMode('sstv')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg></span> SSTV</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="listening" onclick="switchMode('listening')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span> Scanner</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="spystations" onclick="switchMode('spystations')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span> Spy</button>
|
|
||||||
<button class="mobile-nav-btn" data-mode="meshtastic" onclick="switchMode('meshtastic')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span> Mesh</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile Drawer Overlay -->
|
<!-- Mobile Drawer Overlay -->
|
||||||
<div class="drawer-overlay" id="drawerOverlay"></div>
|
<div class="drawer-overlay" id="drawerOverlay"></div>
|
||||||
@@ -1087,7 +989,7 @@
|
|||||||
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
|
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
|
||||||
PACKET LOG</h5>
|
PACKET LOG</h5>
|
||||||
<div id="aprsPacketLog"
|
<div id="aprsPacketLog"
|
||||||
style="flex: 1; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
|
style="flex: 1; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
|
||||||
<div style="color: var(--text-muted);">Waiting for packets...</div>
|
<div style="color: var(--text-muted);">Waiting for packets...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1107,7 +1009,7 @@
|
|||||||
STOPPED</div>
|
STOPPED</div>
|
||||||
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
|
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
|
||||||
<div class="freq-digits" id="mainScannerFreq"
|
<div class="freq-digits" id="mainScannerFreq"
|
||||||
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: 'JetBrains Mono', monospace; letter-spacing: 3px;">
|
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: var(--font-mono); letter-spacing: 3px;">
|
||||||
118.000</div>
|
118.000</div>
|
||||||
<span class="freq-unit"
|
<span class="freq-unit"
|
||||||
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
|
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
|
||||||
@@ -1153,6 +1055,10 @@
|
|||||||
Audio Output</div>
|
Audio Output</div>
|
||||||
</div>
|
</div>
|
||||||
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
|
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
|
||||||
|
<button id="audioUnlockBtn" type="button"
|
||||||
|
style="display: none; margin-top: 8px; width: 100%; padding: 6px 10px; font-size: 10px; letter-spacing: 1px; background: var(--accent-cyan); color: #000; border: 1px solid var(--accent-cyan); border-radius: 4px; cursor: pointer;">
|
||||||
|
CLICK TO ENABLE AUDIO
|
||||||
|
</button>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
|
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
|
||||||
<div
|
<div
|
||||||
@@ -1165,6 +1071,13 @@
|
|||||||
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
|
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
|
||||||
dB</span>
|
dB</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
|
||||||
|
<span style="font-size: 7px; color: var(--text-muted); letter-spacing: 1px;">SNR THRESH</span>
|
||||||
|
<input type="range" id="snrThresholdSlider" min="6" max="20" step="1" value="8"
|
||||||
|
style="flex: 1;" />
|
||||||
|
<span id="snrThresholdValue"
|
||||||
|
style="font-size: 8px; color: var(--text-muted); min-width: 26px; text-align: right;">8</span>
|
||||||
|
</div>
|
||||||
<!-- Signal Alert inline -->
|
<!-- Signal Alert inline -->
|
||||||
<div id="mainSignalAlert"
|
<div id="mainSignalAlert"
|
||||||
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
|
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
|
||||||
@@ -1259,10 +1172,10 @@
|
|||||||
<!-- SQL, Gain, Vol Knobs -->
|
<!-- SQL, Gain, Vol Knobs -->
|
||||||
<div style="display: flex; gap: 15px;">
|
<div style="display: flex; gap: 15px;">
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="radio-knob" id="radioSquelchKnob" data-value="30" data-min="0"
|
<div class="radio-knob" id="radioSquelchKnob" data-value="0" data-min="0"
|
||||||
data-max="100" data-step="1"></div>
|
data-max="100" data-step="1"></div>
|
||||||
<div class="knob-label">SQL</div>
|
<div class="knob-label">SQL</div>
|
||||||
<div class="knob-value" id="radioSquelchValue">30</div>
|
<div class="knob-value" id="radioSquelchValue">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
|
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
|
||||||
@@ -1335,7 +1248,7 @@
|
|||||||
START</div>
|
START</div>
|
||||||
<input type="number" id="radioScanStart" value="118" step="0.1"
|
<input type="number" id="radioScanStart" value="118" step="0.1"
|
||||||
class="radio-input"
|
class="radio-input"
|
||||||
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);">
|
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
|
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;">→</span>
|
||||||
@@ -1344,15 +1257,17 @@
|
|||||||
END</div>
|
END</div>
|
||||||
<input type="number" id="radioScanEnd" value="137" step="0.1"
|
<input type="number" id="radioScanEnd" value="137" step="0.1"
|
||||||
class="radio-input"
|
class="radio-input"
|
||||||
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: 'JetBrains Mono', monospace; font-weight: bold; color: var(--accent-cyan);">
|
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()"
|
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()">
|
||||||
style="padding: 12px; font-size: 13px; width: 100%; font-weight: bold;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN</button>
|
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN
|
||||||
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()"
|
</button>
|
||||||
style="padding: 10px; font-size: 12px; width: 100%; background: var(--accent-green); border: none; color: #fff;"><span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN</button>
|
<button class="radio-action-btn listen" id="radioListenBtn" onclick="toggleDirectListen()">
|
||||||
|
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1410,19 +1325,19 @@
|
|||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
|
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainSignalCount">0</span>
|
id="mainSignalCount">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
|
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainFreqsScanned">0</span>
|
id="mainFreqsScanned">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
|
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
|
||||||
<span
|
<span
|
||||||
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: 'JetBrains Mono', monospace;"
|
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
|
||||||
id="mainScanCycles">0</span>
|
id="mainScanCycles">0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2117,7 +2032,7 @@
|
|||||||
// After fade out, hide welcome and switch to mode
|
// After fade out, hide welcome and switch to mode
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
welcome.style.display = 'none';
|
welcome.style.display = 'none';
|
||||||
switchMode(mode);
|
switchMode(mode, { updateUrl: true });
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2126,6 +2041,35 @@
|
|||||||
document.getElementById('disclaimerModal').style.display = 'flex';
|
document.getElementById('disclaimerModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mode from query string (e.g., /?mode=wifi)
|
||||||
|
let pendingStartMode = null;
|
||||||
|
const validModes = new Set([
|
||||||
|
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
||||||
|
'spystations', 'meshtastic', 'wifi', 'bluetooth',
|
||||||
|
'tscm', 'satellite', 'sstv'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getModeFromQuery() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const mode = params.get('mode');
|
||||||
|
if (!mode || !validModes.has(mode)) return null;
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyModeFromQuery() {
|
||||||
|
const mode = getModeFromQuery();
|
||||||
|
if (!mode) return;
|
||||||
|
const accepted = localStorage.getItem('disclaimerAccepted') === 'true';
|
||||||
|
if (accepted) {
|
||||||
|
const welcome = document.getElementById('welcomePage');
|
||||||
|
if (welcome) welcome.style.display = 'none';
|
||||||
|
switchMode(mode, { updateUrl: false });
|
||||||
|
updateModeUrl(mode, true);
|
||||||
|
} else {
|
||||||
|
pendingStartMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function acceptDisclaimer() {
|
function acceptDisclaimer() {
|
||||||
localStorage.setItem('disclaimerAccepted', 'true');
|
localStorage.setItem('disclaimerAccepted', 'true');
|
||||||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||||||
@@ -2137,7 +2081,14 @@
|
|||||||
const gateStyle = document.getElementById('disclaimer-gate');
|
const gateStyle = document.getElementById('disclaimer-gate');
|
||||||
if (gateStyle) gateStyle.remove();
|
if (gateStyle) gateStyle.remove();
|
||||||
// Ensure welcome page is visible
|
// Ensure welcome page is visible
|
||||||
document.getElementById('welcomePage').style.display = '';
|
const welcome = document.getElementById('welcomePage');
|
||||||
|
if (welcome) welcome.style.display = '';
|
||||||
|
if (pendingStartMode) {
|
||||||
|
// Bypass welcome and jump to requested mode
|
||||||
|
welcome.style.display = 'none';
|
||||||
|
switchMode(pendingStartMode, { updateUrl: true });
|
||||||
|
pendingStartMode = null;
|
||||||
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2456,6 +2407,9 @@
|
|||||||
|
|
||||||
// Start SDR device status polling
|
// Start SDR device status polling
|
||||||
startSdrStatusPolling();
|
startSdrStatusPolling();
|
||||||
|
|
||||||
|
// Apply mode from URL query (e.g., /?mode=wifi)
|
||||||
|
applyModeFromQuery();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle section collapse
|
// Toggle section collapse
|
||||||
@@ -2511,8 +2465,20 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateModeUrl(mode, replace = false) {
|
||||||
|
if (!validModes.has(mode)) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('mode', mode);
|
||||||
|
if (replace) {
|
||||||
|
window.history.replaceState({ mode }, '', url);
|
||||||
|
} else {
|
||||||
|
window.history.pushState({ mode }, '', url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mode switching
|
// Mode switching
|
||||||
function switchMode(mode) {
|
function switchMode(mode, options = {}) {
|
||||||
|
const { updateUrl = true } = options;
|
||||||
// Only stop local scans if in local mode (not agent mode)
|
// Only stop local scans if in local mode (not agent mode)
|
||||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
if (!isAgentMode) {
|
if (!isAgentMode) {
|
||||||
@@ -2525,6 +2491,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
|
if (updateUrl) {
|
||||||
|
updateModeUrl(mode);
|
||||||
|
}
|
||||||
|
|
||||||
// Sync mode state with current agent/local after switching
|
// Sync mode state with current agent/local after switching
|
||||||
if (isAgentMode && typeof syncAgentModeStates === 'function') {
|
if (isAgentMode && typeof syncAgentModeStates === 'function') {
|
||||||
@@ -2757,6 +2726,13 @@
|
|||||||
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
|
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', function () {
|
||||||
|
const mode = getModeFromQuery();
|
||||||
|
if (mode && mode !== currentMode) {
|
||||||
|
switchMode(mode, { updateUrl: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Also handle orientation changes explicitly for mobile
|
// Also handle orientation changes explicitly for mobile
|
||||||
window.addEventListener('orientationchange', function () {
|
window.addEventListener('orientationchange', function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -12205,7 +12181,7 @@
|
|||||||
<textarea id="tleInput" placeholder="ISS (ZARYA)
|
<textarea id="tleInput" placeholder="ISS (ZARYA)
|
||||||
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
||||||
2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450"
|
2 25544 51.6400 208.9163 0006703 296.5855 63.4606 15.49995465478450"
|
||||||
style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px; resize: vertical;"></textarea>
|
style="width: 100%; height: 150px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: var(--font-mono); font-size: 11px; resize: vertical;"></textarea>
|
||||||
<button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add
|
<button class="preset-btn" onclick="addFromTLE()" style="margin-top: 10px; width: 100%;">Add
|
||||||
Satellite</button>
|
Satellite</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="{{ theme|default('dark') }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}iNTERCEPT{% endblock %} // iNTERCEPT</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
|
||||||
|
{# Fonts - Conditional CDN/Local loading #}
|
||||||
|
{% if offline_settings and offline_settings.fonts_source == 'local' %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
|
{% else %}
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Core CSS (Design System) #}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/base.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/components.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||||||
|
|
||||||
|
{# Responsive styles #}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
|
||||||
|
{# Page-specific CSS #}
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
|
|
||||||
|
{# Page-specific head content #}
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
{# Global Header #}
|
||||||
|
{% block header %}
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="app-header-left">
|
||||||
|
<button class="hamburger-btn" id="hamburgerBtn" aria-label="Toggle navigation menu">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
<a href="/" class="app-logo">
|
||||||
|
<svg class="app-logo-icon" width="40" height="40" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 30 Q5 50, 15 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<path d="M22 35 Q14 50, 22 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||||
|
<path d="M29 40 Q23 50, 29 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M85 30 Q95 50, 85 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<path d="M78 35 Q86 50, 78 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||||
|
<path d="M71 40 Q77 50, 71 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||||
|
<circle cx="50" cy="22" r="6" fill="var(--accent-green)"/>
|
||||||
|
<rect x="44" y="35" width="12" height="45" rx="2" fill="var(--accent-cyan)"/>
|
||||||
|
<rect x="38" y="35" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
|
||||||
|
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
|
||||||
|
</svg>
|
||||||
|
<span class="app-logo-text">
|
||||||
|
<span class="app-logo-title">iNTERCEPT</span>
|
||||||
|
<span class="app-logo-tagline">// See the Invisible</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% if version %}
|
||||||
|
<span class="badge badge-primary">v{{ version }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="app-header-right">
|
||||||
|
{% block header_right %}
|
||||||
|
<div class="header-clock">
|
||||||
|
<span class="header-clock-label">UTC</span>
|
||||||
|
<span id="headerUtcTime">--:--:--</span>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{# Global Navigation - opt-in for pages that need it #}
|
||||||
|
{# Override this block and include 'partials/nav.html' in child templates #}
|
||||||
|
{% block navigation %}{% endblock %}
|
||||||
|
|
||||||
|
{# Main Content Area #}
|
||||||
|
<main class="app-main">
|
||||||
|
{% block main %}
|
||||||
|
<div class="content-wrapper">
|
||||||
|
{# Optional Sidebar #}
|
||||||
|
{% block sidebar %}{% endblock %}
|
||||||
|
|
||||||
|
{# Page Content #}
|
||||||
|
<div class="app-content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{# Toast/Notification Container #}
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Core JavaScript #}
|
||||||
|
<script>
|
||||||
|
// UTC Clock
|
||||||
|
function updateUtcClock() {
|
||||||
|
const now = new Date();
|
||||||
|
const utc = now.toISOString().slice(11, 19);
|
||||||
|
const clockEl = document.getElementById('headerUtcTime');
|
||||||
|
if (clockEl) clockEl.textContent = utc;
|
||||||
|
}
|
||||||
|
setInterval(updateUtcClock, 1000);
|
||||||
|
updateUtcClock();
|
||||||
|
|
||||||
|
// Mobile menu toggle
|
||||||
|
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||||
|
const drawerOverlay = document.getElementById('drawerOverlay');
|
||||||
|
|
||||||
|
if (hamburgerBtn) {
|
||||||
|
hamburgerBtn.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('open');
|
||||||
|
document.querySelector('.app-sidebar')?.classList.toggle('open');
|
||||||
|
drawerOverlay?.classList.toggle('visible');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawerOverlay) {
|
||||||
|
drawerOverlay.addEventListener('click', function() {
|
||||||
|
hamburgerBtn?.classList.remove('open');
|
||||||
|
document.querySelector('.app-sidebar')?.classList.remove('open');
|
||||||
|
this.classList.remove('visible');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme toggle
|
||||||
|
function toggleTheme() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const currentTheme = html.getAttribute('data-theme') || 'dark';
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply saved theme
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nav dropdown handling
|
||||||
|
function toggleNavDropdown(groupName) {
|
||||||
|
const group = document.querySelector(`.nav-group[data-group="${groupName}"]`);
|
||||||
|
if (!group) return;
|
||||||
|
|
||||||
|
// Close other dropdowns
|
||||||
|
document.querySelectorAll('.nav-group.open').forEach(g => {
|
||||||
|
if (g !== group) g.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
group.classList.toggle('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('.nav-group')) {
|
||||||
|
document.querySelectorAll('.nav-group.open').forEach(g => g.classList.remove('open'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{# Page-specific JavaScript #}
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 %}
|
||||||
+1111
-1106
File diff suppressed because it is too large
Load Diff
@@ -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,283 @@
|
|||||||
|
{#
|
||||||
|
Global Navigation Partial
|
||||||
|
Single source of truth for app navigation
|
||||||
|
|
||||||
|
Compatible with:
|
||||||
|
- index.html (uses switchMode() for mode panels)
|
||||||
|
- Dashboard pages (uses navigation links)
|
||||||
|
|
||||||
|
Variables:
|
||||||
|
- active_mode: Current active mode (e.g., 'pager', 'adsb', 'wifi')
|
||||||
|
- is_index_page: If true, Satellite/SSTV use switchMode (panel mode)
|
||||||
|
If false (default), Satellite links to dashboard
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% set is_index_page = is_index_page|default(false) %}
|
||||||
|
|
||||||
|
{% macro mode_item(mode, label, icon_svg, href=None) -%}
|
||||||
|
{%- set is_active = 'active' if active_mode == mode else '' -%}
|
||||||
|
{%- if href %}
|
||||||
|
<a href="{{ href }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||||
|
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||||
|
<span class="nav-label">{{ label }}</span>
|
||||||
|
</a>
|
||||||
|
{%- elif is_index_page %}
|
||||||
|
<button class="mode-nav-btn {{ is_active }}" onclick="switchMode('{{ mode }}')">
|
||||||
|
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||||
|
<span class="nav-label">{{ label }}</span>
|
||||||
|
</button>
|
||||||
|
{%- else %}
|
||||||
|
<a href="/?mode={{ mode }}" class="mode-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||||
|
<span class="nav-icon icon">{{ icon_svg | safe }}</span>
|
||||||
|
<span class="nav-label">{{ label }}</span>
|
||||||
|
</a>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro mobile_item(mode, label, icon_svg, href=None) -%}
|
||||||
|
{%- set is_active = 'active' if active_mode == mode else '' -%}
|
||||||
|
{%- if href %}
|
||||||
|
<a href="{{ href }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||||
|
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||||
|
</a>
|
||||||
|
{%- elif is_index_page %}
|
||||||
|
<button class="mobile-nav-btn {{ is_active }}" data-mode="{{ mode }}" onclick="switchMode('{{ mode }}')">
|
||||||
|
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||||
|
</button>
|
||||||
|
{%- else %}
|
||||||
|
<a href="/?mode={{ mode }}" class="mobile-nav-btn {{ is_active }}" style="text-decoration: none;">
|
||||||
|
<span class="icon icon--sm">{{ icon_svg | safe }}</span> {{ label }}
|
||||||
|
</a>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{# Desktop Navigation - uses existing CSS class names for compatibility #}
|
||||||
|
<nav class="mode-nav" id="mainNav">
|
||||||
|
{# SDR / RF Group #}
|
||||||
|
<div class="mode-nav-dropdown" data-group="sdr">
|
||||||
|
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('sdr')"{% endif %}>
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
|
||||||
|
<span class="nav-label">SDR / RF</span>
|
||||||
|
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mode-nav-dropdown-menu">
|
||||||
|
{{ mode_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
|
||||||
|
{{ mode_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||||
|
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||||
|
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||||
|
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||||
|
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||||
|
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||||
|
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||||
|
{{ mode_item('meshtastic', 'Meshtastic', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Wireless Group #}
|
||||||
|
<div class="mode-nav-dropdown" data-group="wireless">
|
||||||
|
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('wireless')"{% endif %}>
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
||||||
|
<span class="nav-label">Wireless</span>
|
||||||
|
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mode-nav-dropdown-menu">
|
||||||
|
{{ mode_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg>') }}
|
||||||
|
{{ mode_item('bluetooth', 'Bluetooth', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Security Group #}
|
||||||
|
<div class="mode-nav-dropdown" data-group="security">
|
||||||
|
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('security')"{% endif %}>
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
||||||
|
<span class="nav-label">Security</span>
|
||||||
|
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mode-nav-dropdown-menu">
|
||||||
|
{{ mode_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Space Group #}
|
||||||
|
<div class="mode-nav-dropdown" data-group="space">
|
||||||
|
<button class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('space')"{% endif %}>
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
|
||||||
|
<span class="nav-label">Space</span>
|
||||||
|
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mode-nav-dropdown-menu">
|
||||||
|
{% if is_index_page %}
|
||||||
|
{{ mode_item('satellite', 'Satellite', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>') }}
|
||||||
|
{% else %}
|
||||||
|
{{ mode_item('satellite', 'Satellite', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>', '/satellite/dashboard') }}
|
||||||
|
{% endif %}
|
||||||
|
{{ mode_item('sstv', 'ISS SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg>') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Dynamic dashboard button (shown when in satellite mode) #}
|
||||||
|
<div class="mode-nav-actions">
|
||||||
|
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;">
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span>
|
||||||
|
<span class="nav-label">Full Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Nav Utilities (clock, theme, tools) #}
|
||||||
|
<div class="nav-utilities">
|
||||||
|
<div class="nav-clock">
|
||||||
|
<span class="utc-label">UTC</span>
|
||||||
|
<span class="utc-time" id="headerUtcTime">--:--:--</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<div class="nav-tools">
|
||||||
|
<button class="nav-tool-btn" onclick="toggleAnimations()" title="Toggle Animations">
|
||||||
|
<span class="icon-effects-on icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></span>
|
||||||
|
<span class="icon-effects-off icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/><line x1="2" y1="2" x2="22" y2="22"/></svg></span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-tool-btn" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
|
||||||
|
<span class="icon-moon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
|
||||||
|
<span class="icon-sun icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
|
||||||
|
</button>
|
||||||
|
<a href="/controller/monitor" class="nav-tool-btn" title="Network Monitor - Multi-Agent View" style="text-decoration: none;">
|
||||||
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
||||||
|
</a>
|
||||||
|
<a href="/controller/manage" class="nav-tool-btn" title="Manage Remote Agents" style="text-decoration: none;">
|
||||||
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span>
|
||||||
|
</a>
|
||||||
|
<button class="nav-tool-btn" onclick="showSettings()" title="Settings">
|
||||||
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
||||||
|
<button class="nav-tool-btn" onclick="logout(event)" title="Logout">
|
||||||
|
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{# Mobile Navigation Bar #}
|
||||||
|
<nav class="mobile-nav-bar" id="mobileNavBar">
|
||||||
|
{{ mobile_item('pager', 'Pager', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg>') }}
|
||||||
|
{{ mobile_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||||
|
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||||
|
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||||
|
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||||
|
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||||
|
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
|
||||||
|
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||||
|
{{ mobile_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||||
|
{% if is_index_page %}
|
||||||
|
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>') }}
|
||||||
|
{% else %}
|
||||||
|
{{ mobile_item('satellite', 'Sat', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg>', '/satellite/dashboard') }}
|
||||||
|
{% endif %}
|
||||||
|
{{ mobile_item('sstv', 'SSTV', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>') }}
|
||||||
|
{{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||||
|
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||||
|
{{ mobile_item('meshtastic', 'Mesh', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>') }}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||||
|
<script>
|
||||||
|
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
|
||||||
|
if (typeof switchMode === 'undefined') {
|
||||||
|
window.switchMode = function(mode) {
|
||||||
|
// On dashboard pages, navigate to main page with mode param
|
||||||
|
window.location.href = '/?mode=' + mode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof toggleNavDropdown === 'undefined') {
|
||||||
|
window.toggleNavDropdown = function(groupName) {
|
||||||
|
const dropdown = document.querySelector(`.mode-nav-dropdown[data-group="${groupName}"]`);
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
// Close other dropdowns
|
||||||
|
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => {
|
||||||
|
if (d !== dropdown) d.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.classList.toggle('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('.mode-nav-dropdown')) {
|
||||||
|
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => d.classList.remove('open'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof toggleAnimations === 'undefined') {
|
||||||
|
window.toggleAnimations = function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const current = html.getAttribute('data-animations') || 'on';
|
||||||
|
const next = current === 'on' ? 'off' : 'on';
|
||||||
|
html.setAttribute('data-animations', next);
|
||||||
|
localStorage.setItem('animations', next);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof toggleTheme === 'undefined') {
|
||||||
|
window.toggleTheme = function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const current = html.getAttribute('data-theme') || 'dark';
|
||||||
|
const next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('theme', next);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof showSettings === 'undefined') {
|
||||||
|
window.showSettings = function() {
|
||||||
|
// Navigate to main page settings
|
||||||
|
window.location.href = '/?settings=1';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof showHelp === 'undefined') {
|
||||||
|
window.showHelp = function() {
|
||||||
|
window.open('https://smittix.github.io/intercept', '_blank');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof logout === 'undefined') {
|
||||||
|
window.logout = function(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
if (confirm('Are you sure you want to logout?')) {
|
||||||
|
window.location.href = '/logout';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply saved preferences and start clock
|
||||||
|
(function() {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedAnimations = localStorage.getItem('animations');
|
||||||
|
if (savedAnimations) {
|
||||||
|
document.documentElement.setAttribute('data-animations', savedAnimations);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTC Clock update (if not already defined by parent page)
|
||||||
|
if (typeof window._navClockStarted === 'undefined') {
|
||||||
|
window._navClockStarted = true;
|
||||||
|
function updateNavUtcClock() {
|
||||||
|
const now = new Date();
|
||||||
|
const utc = now.toISOString().slice(11, 19);
|
||||||
|
const el = document.getElementById('headerUtcTime');
|
||||||
|
if (el) el.textContent = utc;
|
||||||
|
}
|
||||||
|
setInterval(updateNavUtcClock, 1000);
|
||||||
|
updateNavUtcClock();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -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">JetBrains Mono</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
|
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
|
||||||
<option value="cdn">Google Fonts (Online)</option>
|
<option value="cdn">Google Fonts (Online)</option>
|
||||||
<option value="local">Local</option>
|
<option value="local">Local</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Map Tiles</div>
|
<div class="settings-group-title">Map Tiles</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Tile Provider</span>
|
<span class="settings-label-text">Tile Provider</span>
|
||||||
<span class="settings-label-desc">Map background imagery</span>
|
<span class="settings-label-desc">Map background imagery</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
|
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
|
||||||
<option value="openstreetmap">OpenStreetMap</option>
|
<option value="openstreetmap">OpenStreetMap</option>
|
||||||
<option value="cartodb_dark">CartoDB Dark</option>
|
<option value="cartodb_dark">CartoDB Dark</option>
|
||||||
<option value="cartodb_light">CartoDB Positron</option>
|
<option value="cartodb_light">CartoDB Positron</option>
|
||||||
<option value="esri_world">ESRI World Imagery</option>
|
<option value="esri_world">ESRI World Imagery</option>
|
||||||
<option value="custom">Custom URL</option>
|
<option value="custom">Custom URL</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
|
<div class="settings-row custom-url-row" id="customTileUrlRow" style="display: none;">
|
||||||
<div class="settings-label" style="width: 100%;">
|
<div class="settings-label" style="width: 100%;">
|
||||||
<span class="settings-label-text">Custom Tile URL</span>
|
<span class="settings-label-text">Custom Tile URL</span>
|
||||||
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
|
<span class="settings-label-desc">e.g., http://localhost:8080/{z}/{x}/{y}.png</span>
|
||||||
<input type="text" id="customTileUrl" class="settings-input"
|
<input type="text" id="customTileUrl" class="settings-input"
|
||||||
placeholder="http://tile-server/{z}/{x}/{y}.png"
|
placeholder="http://tile-server/{z}/{x}/{y}.png"
|
||||||
onchange="Settings.setCustomTileUrl(this.value)">
|
onchange="Settings.setCustomTileUrl(this.value)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Local Asset Status</div>
|
<div class="settings-group-title">Local Asset Status</div>
|
||||||
<div class="asset-status" id="assetStatus">
|
<div class="asset-status" id="assetStatus">
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Leaflet JS/CSS</span>
|
<span class="asset-name">Leaflet JS/CSS</span>
|
||||||
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
|
<span class="asset-badge checking" id="statusLeaflet">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Chart.js</span>
|
<span class="asset-name">Chart.js</span>
|
||||||
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
|
<span class="asset-badge checking" id="statusChartjs">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">Inter Font</span>
|
<span class="asset-name">Inter Font</span>
|
||||||
<span class="asset-badge checking" id="statusInter">Checking...</span>
|
<span class="asset-badge checking" id="statusInter">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-status-row">
|
<div class="asset-status-row">
|
||||||
<span class="asset-name">JetBrains Mono</span>
|
<span class="asset-name">JetBrains Mono</span>
|
||||||
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
|
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="check-assets-btn" onclick="Settings.checkAssets()">
|
<button class="check-assets-btn" onclick="Settings.checkAssets()">
|
||||||
Check Assets
|
Check Assets
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
|
<strong>Note:</strong> Changes to asset sources require a page reload to take effect.
|
||||||
Local assets must be available in <code>/static/vendor/</code>.
|
Local assets must be available in <code>/static/vendor/</code>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location Section -->
|
<!-- Location Section -->
|
||||||
<div id="settings-location" class="settings-section">
|
<div id="settings-location" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Observer Location</div>
|
<div class="settings-group-title">Observer Location</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
Set your geographic coordinates for satellite pass predictions and ISS tracking.
|
Set your geographic coordinates for satellite pass predictions and ISS tracking.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Latitude</span>
|
<span class="settings-label-text">Latitude</span>
|
||||||
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
|
<span class="settings-label-desc">Decimal degrees (-90 to 90)</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="number" id="observerLatInput" class="settings-input"
|
<input type="number" id="observerLatInput" class="settings-input"
|
||||||
step="0.0001" min="-90" max="90" placeholder="51.5074"
|
step="0.0001" min="-90" max="90" placeholder="51.5074"
|
||||||
style="width: 120px; text-align: right;">
|
style="width: 120px; text-align: right;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Longitude</span>
|
<span class="settings-label-text">Longitude</span>
|
||||||
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
|
<span class="settings-label-desc">Decimal degrees (-180 to 180)</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="number" id="observerLonInput" class="settings-input"
|
<input type="number" id="observerLonInput" class="settings-input"
|
||||||
step="0.0001" min="-180" max="180" placeholder="-0.1278"
|
step="0.0001" min="-180" max="180" placeholder="-0.1278"
|
||||||
style="width: 120px; text-align: right;">
|
style="width: 120px; text-align: right;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||||||
<button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;">
|
<button class="check-assets-btn" onclick="detectLocationGPS(this)" style="flex: 1;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px;">
|
||||||
<circle cx="12" cy="12" r="10"/>
|
<circle cx="12" cy="12" r="10"/>
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<line x1="12" y1="2" x2="12" y2="6"/>
|
<line x1="12" y1="2" x2="12" y2="6"/>
|
||||||
<line x1="12" y1="18" x2="12" y2="22"/>
|
<line x1="12" y1="18" x2="12" y2="22"/>
|
||||||
<line x1="2" y1="12" x2="6" y2="12"/>
|
<line x1="2" y1="12" x2="6" y2="12"/>
|
||||||
<line x1="18" y1="12" x2="22" y2="12"/>
|
<line x1="18" y1="12" x2="22" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Use GPS
|
Use GPS
|
||||||
</button>
|
</button>
|
||||||
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
|
<button class="check-assets-btn" onclick="saveObserverLocation()" style="flex: 1; background: var(--accent-cyan); color: #000;">
|
||||||
Save Location
|
Save Location
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Current Location</div>
|
<div class="settings-group-title">Current Location</div>
|
||||||
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12px;">
|
<div id="currentLocationDisplay" style="padding: 12px; background: var(--bg-tertiary); border-radius: 6px; font-family: var(--font-mono); font-size: 12px;">
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||||
<span style="color: var(--text-dim);">Latitude</span>
|
<span style="color: var(--text-dim);">Latitude</span>
|
||||||
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
|
<span id="currentLatDisplay" style="color: var(--accent-cyan);">Not set</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<div style="display: flex; justify-content: space-between;">
|
||||||
<span style="color: var(--text-dim);">Longitude</span>
|
<span style="color: var(--text-dim);">Longitude</span>
|
||||||
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
|
<span id="currentLonDisplay" style="color: var(--accent-cyan);">Not set</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
|
<strong>Note:</strong> Location is used for ISS pass predictions in SSTV mode and satellite tracking.
|
||||||
Your location is stored locally and never sent to external servers.
|
Your location is stored locally and never sent to external servers.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display Section -->
|
<!-- Display Section -->
|
||||||
<div id="settings-display" class="settings-section">
|
<div id="settings-display" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Visual Preferences</div>
|
<div class="settings-group-title">Visual Preferences</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Theme</span>
|
<span class="settings-label-text">Theme</span>
|
||||||
<span class="settings-label-desc">Color scheme preference</span>
|
<span class="settings-label-desc">Color scheme preference</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
|
<select id="themeSelect" class="settings-select" onchange="setThemePreference(this.value)">
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Animations</span>
|
<span class="settings-label-text">Animations</span>
|
||||||
<span class="settings-label-desc">Enable visual effects and animations</span>
|
<span class="settings-label-desc">Enable visual effects and animations</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
|
<input type="checkbox" id="animationsEnabled" checked onchange="setAnimationsEnabled(this.checked)">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Updates Section -->
|
<!-- Updates Section -->
|
||||||
<div id="settings-updates" class="settings-section">
|
<div id="settings-updates" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Update Status</div>
|
<div class="settings-group-title">Update Status</div>
|
||||||
<div id="updateStatusContent" style="padding: 10px 0;">
|
<div id="updateStatusContent" style="padding: 10px 0;">
|
||||||
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
|
<div style="text-align: center; padding: 20px; color: var(--text-dim);">
|
||||||
Loading update status...
|
Loading update status...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
|
<button class="check-assets-btn" onclick="checkForUpdatesManual()" style="margin-top: 10px;">
|
||||||
Check Now
|
Check Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Update Settings</div>
|
<div class="settings-group-title">Update Settings</div>
|
||||||
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<div class="settings-label">
|
<div class="settings-label">
|
||||||
<span class="settings-label-text">Auto-Check for Updates</span>
|
<span class="settings-label-text">Auto-Check for Updates</span>
|
||||||
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
|
<span class="settings-label-desc">Periodically check GitHub for new releases</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
|
<input type="checkbox" id="updateCheckEnabled" checked onchange="toggleUpdateCheck(this.checked)">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-info">
|
<div class="settings-info">
|
||||||
<strong>Note:</strong> Updates are fetched from GitHub and applied via git pull.
|
<strong>Note:</strong> Updates are fetched from GitHub and applied via git pull.
|
||||||
Make sure you have git installed and the application is in a git repository.
|
Make sure you have git installed and the application is in a git repository.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tools Section -->
|
<!-- Tools Section -->
|
||||||
<div id="settings-tools" class="settings-section">
|
<div id="settings-tools" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Tool Dependencies</div>
|
<div class="settings-group-title">Tool Dependencies</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
Check which external tools are installed for each mode.
|
Check which external tools are installed for each mode.
|
||||||
<span style="color: var(--accent-green);">●</span> = Installed,
|
<span style="color: var(--accent-green);">●</span> = Installed,
|
||||||
<span style="color: var(--accent-red);">●</span> = Missing
|
<span style="color: var(--accent-red);">●</span> = Missing
|
||||||
</p>
|
</p>
|
||||||
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
|
<div id="settingsToolsContent" style="max-height: 45vh; overflow-y: auto;">
|
||||||
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
|
<div style="text-align: center; padding: 30px; color: var(--text-dim);">
|
||||||
Loading dependencies...
|
Loading dependencies...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group" style="margin-top: 15px;">
|
<div class="settings-group" style="margin-top: 15px;">
|
||||||
<div class="settings-group-title">Quick Install (Debian/Ubuntu)</div>
|
<div class="settings-group-title">Quick Install (Debian/Ubuntu)</div>
|
||||||
<div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;">
|
<div style="background: var(--bg-tertiary); padding: 10px; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; overflow-x: auto;">
|
||||||
<div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div>
|
<div>sudo apt install rtl-sdr multimon-ng rtl-433 aircrack-ng bluez dump1090-mutability hcxdumptool hcxtools</div>
|
||||||
<div style="margin-top: 5px;">pip install skyfield flask</div>
|
<div style="margin-top: 5px;">pip install skyfield flask</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);">
|
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim);">
|
||||||
<strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source.
|
<strong>Note:</strong> ACARS decoding requires <code>acarsdec</code> which must be built from source.
|
||||||
See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation.
|
See <a href="https://github.com/TLeconte/acarsdec" target="_blank" style="color: var(--accent-cyan);">github.com/TLeconte/acarsdec</a> or run <code>./setup.sh</code> for automated installation.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<div id="settings-about" class="settings-section">
|
<div id="settings-about" class="settings-section">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="about-info">
|
<div class="about-info">
|
||||||
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
|
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p>
|
||||||
<p>Version: <span class="about-version">{{ version }}</span></p>
|
<p>Version: <span class="about-version">{{ version }}</span></p>
|
||||||
<p>
|
<p>
|
||||||
A unified web interface for software-defined radio (SDR) tools,
|
A unified web interface for software-defined radio (SDR) tools,
|
||||||
supporting pager decoding, sensor monitoring, aircraft tracking,
|
supporting pager decoding, sensor monitoring, aircraft tracking,
|
||||||
WiFi/Bluetooth scanning, and more.
|
WiFi/Bluetooth scanning, and more.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
|
<a href="https://github.com/smittix/intercept" target="_blank">GitHub Repository</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-group-title">Support the Project</div>
|
<div class="settings-group-title">Support the Project</div>
|
||||||
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
|
||||||
If you find iNTERCEPT useful, consider supporting its development.
|
If you find iNTERCEPT useful, consider supporting its development.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
|
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
|
||||||
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
|
<path d="M17 8h1a4 4 0 1 1 0 8h-1"/>
|
||||||
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
|
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/>
|
||||||
<line x1="6" y1="2" x2="6" y2="4"/>
|
<line x1="6" y1="2" x2="6" y2="4"/>
|
||||||
<line x1="10" y1="2" x2="10" y2="4"/>
|
<line x1="10" y1="2" x2="10" y2="4"/>
|
||||||
<line x1="14" y1="2" x2="14" y2="4"/>
|
<line x1="14" y1="2" x2="14" y2="4"/>
|
||||||
</svg>
|
</svg>
|
||||||
Buy Me a Coffee
|
Buy Me a Coffee
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1006
-1001
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user