mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 08:13:32 -07:00
Compare commits
17 Commits
v2.24.0
...
ui-testing
| Author | SHA1 | Date | |
|---|---|---|---|
| d0e8eaf397 | |||
| 4bf5bd2d37 | |||
| 206e63944e | |||
| 978e6cdaea | |||
| 68d831dbe3 | |||
| 623a0da056 | |||
| d6f5127cd6 | |||
| 166963f2a1 | |||
| eeca15c83e | |||
| ed1dfbb9b5 | |||
| 29af551828 | |||
| e1e05523d2 | |||
| 94fcea0e99 | |||
| b0c323bb89 | |||
| 425572ac87 | |||
| db931c3806 | |||
| a58e2f0d21 |
@@ -0,0 +1,608 @@
|
|||||||
|
# iNTERCEPT UI Guide
|
||||||
|
|
||||||
|
This guide documents the UI design system, components, and patterns used in iNTERCEPT.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Design Tokens](#design-tokens)
|
||||||
|
2. [Base Templates](#base-templates)
|
||||||
|
3. [Navigation](#navigation)
|
||||||
|
4. [Components](#components)
|
||||||
|
5. [Adding a New Module Page](#adding-a-new-module-page)
|
||||||
|
6. [Adding a New Dashboard](#adding-a-new-dashboard)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Tokens
|
||||||
|
|
||||||
|
All design tokens are defined in `static/css/core/variables.css`. Import this file first in any stylesheet.
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Backgrounds (layered depth) */
|
||||||
|
--bg-primary: #0a0c10; /* Darkest - page background */
|
||||||
|
--bg-secondary: #0f1218; /* Panels, sidebars */
|
||||||
|
--bg-tertiary: #151a23; /* Cards, elevated elements */
|
||||||
|
--bg-card: #121620; /* Card backgrounds */
|
||||||
|
--bg-elevated: #1a202c; /* Hover states, modals */
|
||||||
|
|
||||||
|
/* Accent Colors */
|
||||||
|
--accent-cyan: #4a9eff; /* Primary action color */
|
||||||
|
--accent-green: #22c55e; /* Success, online status */
|
||||||
|
--accent-red: #ef4444; /* Error, danger, stop */
|
||||||
|
--accent-orange: #f59e0b; /* Warning */
|
||||||
|
--accent-amber: #d4a853; /* Secondary highlight */
|
||||||
|
|
||||||
|
/* Text Hierarchy */
|
||||||
|
--text-primary: #e8eaed; /* Main content */
|
||||||
|
--text-secondary: #9ca3af; /* Secondary content */
|
||||||
|
--text-dim: #4b5563; /* Disabled, placeholder */
|
||||||
|
--text-muted: #374151; /* Barely visible */
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
--status-online: #22c55e;
|
||||||
|
--status-warning: #f59e0b;
|
||||||
|
--status-error: #ef4444;
|
||||||
|
--status-offline: #6b7280;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spacing Scale
|
||||||
|
|
||||||
|
```css
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 32px;
|
||||||
|
--space-10: 40px;
|
||||||
|
--space-12: 48px;
|
||||||
|
--space-16: 64px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Font Families */
|
||||||
|
--font-sans: 'Inter', -apple-system, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
|
||||||
|
/* Font Sizes */
|
||||||
|
--text-xs: 10px;
|
||||||
|
--text-sm: 12px;
|
||||||
|
--text-base: 14px;
|
||||||
|
--text-lg: 16px;
|
||||||
|
--text-xl: 18px;
|
||||||
|
--text-2xl: 20px;
|
||||||
|
--text-3xl: 24px;
|
||||||
|
--text-4xl: 30px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Border Radius
|
||||||
|
|
||||||
|
```css
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 6px;
|
||||||
|
--radius-lg: 8px;
|
||||||
|
--radius-xl: 12px;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Light Theme
|
||||||
|
|
||||||
|
The design system supports light/dark themes via `data-theme` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html data-theme="dark"> <!-- or "light" -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Toggle with JavaScript:
|
||||||
|
```javascript
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Templates
|
||||||
|
|
||||||
|
### `templates/layout/base.html`
|
||||||
|
|
||||||
|
The main base template for standard pages. Use for pages with sidebar + content layout.
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends 'layout/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}My Page Title{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/my-page.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block navigation %}
|
||||||
|
{% set active_mode = 'mymode' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block sidebar %}
|
||||||
|
<div class="app-sidebar">
|
||||||
|
<!-- Sidebar content -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-container">
|
||||||
|
<h1>Page Title</h1>
|
||||||
|
<!-- Page content -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Page-specific JavaScript
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `templates/layout/base_dashboard.html`
|
||||||
|
|
||||||
|
Extended base for full-screen dashboards (maps, visualizations).
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends 'layout/base_dashboard.html' %}
|
||||||
|
|
||||||
|
{% set active_mode = 'mydashboard' %}
|
||||||
|
|
||||||
|
{% block dashboard_title %}MY DASHBOARD{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_dashboard.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block stats_strip %}
|
||||||
|
<div class="stats-strip">
|
||||||
|
<!-- Stats bar content -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
<div class="dashboard-map-container">
|
||||||
|
<!-- Main visualization -->
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-sidebar">
|
||||||
|
<!-- Sidebar panels -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Including Navigation
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% set active_mode = 'pager' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Valid `active_mode` Values
|
||||||
|
|
||||||
|
| Mode | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `pager` | Pager decoding |
|
||||||
|
| `sensor` | 433MHz sensors |
|
||||||
|
| `rtlamr` | Utility meters |
|
||||||
|
| `adsb` | Aircraft tracking |
|
||||||
|
| `ais` | Vessel tracking |
|
||||||
|
| `aprs` | Amateur radio |
|
||||||
|
| `wifi` | WiFi scanning |
|
||||||
|
| `bluetooth` | Bluetooth scanning |
|
||||||
|
| `tscm` | Counter-surveillance |
|
||||||
|
| `satellite` | Satellite tracking |
|
||||||
|
| `sstv` | ISS SSTV |
|
||||||
|
| `listening` | Listening post |
|
||||||
|
| `spystations` | Spy stations |
|
||||||
|
| `meshtastic` | Mesh networking |
|
||||||
|
|
||||||
|
### Navigation Groups
|
||||||
|
|
||||||
|
The navigation is organized into groups:
|
||||||
|
- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic
|
||||||
|
- **Wireless**: WiFi, Bluetooth
|
||||||
|
- **Security**: TSCM
|
||||||
|
- **Space**: Satellite, ISS SSTV
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Card / Panel
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% call card(title='PANEL TITLE', indicator=true, indicator_active=false) %}
|
||||||
|
<p>Panel content here</p>
|
||||||
|
{% endcall %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
```html
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>PANEL TITLE</span>
|
||||||
|
<div class="panel-indicator active"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<p>Content here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty State
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% include 'components/empty_state.html' with context %}
|
||||||
|
{# Or with variables: #}
|
||||||
|
{% with title='No data yet', description='Start scanning to see results', action_text='Start Scan', action_onclick='startScan()' %}
|
||||||
|
{% include 'components/empty_state.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading State
|
||||||
|
|
||||||
|
```html
|
||||||
|
{# Inline spinner #}
|
||||||
|
{% include 'components/loading.html' %}
|
||||||
|
|
||||||
|
{# With text #}
|
||||||
|
{% with text='Loading data...', size='lg' %}
|
||||||
|
{% include 'components/loading.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{# Full overlay #}
|
||||||
|
{% with overlay=true, text='Please wait...' %}
|
||||||
|
{% include 'components/loading.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Badge
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% with status='online', text='Connected', id='connectionStatus' %}
|
||||||
|
{% include 'components/status_badge.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status values: `online`, `offline`, `warning`, `error`, `inactive`
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Primary action -->
|
||||||
|
<button class="btn btn-primary">Start Tracking</button>
|
||||||
|
|
||||||
|
<!-- Secondary action -->
|
||||||
|
<button class="btn btn-secondary">Cancel</button>
|
||||||
|
|
||||||
|
<!-- Danger action -->
|
||||||
|
<button class="btn btn-danger">Stop</button>
|
||||||
|
|
||||||
|
<!-- Ghost/subtle -->
|
||||||
|
<button class="btn btn-ghost">Settings</button>
|
||||||
|
|
||||||
|
<!-- Sizes -->
|
||||||
|
<button class="btn btn-primary btn-sm">Small</button>
|
||||||
|
<button class="btn btn-primary btn-lg">Large</button>
|
||||||
|
|
||||||
|
<!-- Icon button -->
|
||||||
|
<button class="btn btn-icon btn-secondary">
|
||||||
|
<span class="icon">...</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Badges
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="badge">Default</span>
|
||||||
|
<span class="badge badge-primary">Primary</span>
|
||||||
|
<span class="badge badge-success">Online</span>
|
||||||
|
<span class="badge badge-warning">Warning</span>
|
||||||
|
<span class="badge badge-danger">Error</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Groups
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="frequency">Frequency (MHz)</label>
|
||||||
|
<input type="text" id="frequency" value="153.350">
|
||||||
|
<span class="form-help">Enter frequency in MHz</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="gain">Gain</label>
|
||||||
|
<select id="gain">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="30">30 dB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="form-check">
|
||||||
|
<input type="checkbox" id="alerts">
|
||||||
|
<span>Enable alerts</span>
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats Strip
|
||||||
|
|
||||||
|
Used in dashboards for horizontal statistics display:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="stats-strip">
|
||||||
|
<div class="stats-strip-inner">
|
||||||
|
<div class="strip-stat">
|
||||||
|
<span class="strip-value" id="count">0</span>
|
||||||
|
<span class="strip-label">COUNT</span>
|
||||||
|
</div>
|
||||||
|
<div class="strip-divider"></div>
|
||||||
|
<div class="strip-status">
|
||||||
|
<div class="status-dot active" id="statusDot"></div>
|
||||||
|
<span id="statusText">TRACKING</span>
|
||||||
|
</div>
|
||||||
|
<div class="strip-time" id="utcTime">--:--:-- UTC</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a New Module Page
|
||||||
|
|
||||||
|
### 1. Create the Route
|
||||||
|
|
||||||
|
In `routes/mymodule.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
mymodule_bp = Blueprint('mymodule', __name__, url_prefix='/mymodule')
|
||||||
|
|
||||||
|
@mymodule_bp.route('/dashboard')
|
||||||
|
def dashboard():
|
||||||
|
return render_template('mymodule_dashboard.html',
|
||||||
|
offline_settings=get_offline_settings())
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register the Blueprint
|
||||||
|
|
||||||
|
In `routes/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from routes.mymodule import mymodule_bp
|
||||||
|
app.register_blueprint(mymodule_bp)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create the Template
|
||||||
|
|
||||||
|
Option A: Simple page extending base.html
|
||||||
|
```html
|
||||||
|
{% extends 'layout/base.html' %}
|
||||||
|
{% set active_mode = 'mymodule' %}
|
||||||
|
|
||||||
|
{% block title %}My Module{% endblock %}
|
||||||
|
|
||||||
|
{% block navigation %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Your content -->
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Option B: Full-screen dashboard
|
||||||
|
```html
|
||||||
|
{% extends 'layout/base_dashboard.html' %}
|
||||||
|
{% set active_mode = 'mymodule' %}
|
||||||
|
|
||||||
|
{% block dashboard_title %}MY MODULE{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
<!-- Your dashboard content -->
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Add to Navigation
|
||||||
|
|
||||||
|
In `templates/partials/nav.html`, add your module to the appropriate group:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
|
||||||
|
onclick="switchMode('mymodule')">
|
||||||
|
<span class="nav-icon icon"><!-- SVG icon --></span>
|
||||||
|
<span class="nav-label">My Module</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if it's a dashboard link:
|
||||||
|
```html
|
||||||
|
<a href="/mymodule/dashboard"
|
||||||
|
class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
|
||||||
|
style="text-decoration: none;">
|
||||||
|
<span class="nav-icon icon"><!-- SVG icon --></span>
|
||||||
|
<span class="nav-label">My Module</span>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Stylesheet
|
||||||
|
|
||||||
|
In `static/css/mymodule.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* My Module Styles
|
||||||
|
*/
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
|
||||||
|
/* Your styles using design tokens */
|
||||||
|
.mymodule-container {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a New Dashboard
|
||||||
|
|
||||||
|
For full-screen dashboards like ADSB, AIS, or Satellite:
|
||||||
|
|
||||||
|
### 1. Create the Template
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MY DASHBOARD // iNTERCEPT</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
|
||||||
|
<!-- Design tokens (required) -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
{% if offline_settings.fonts_source == 'local' %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||||
|
{% else %}
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- External libraries if needed -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
|
<!-- Dashboard styles -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/mydashboard.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Background effects -->
|
||||||
|
<div class="radar-bg"></div>
|
||||||
|
<div class="scanline"></div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="/" style="color: inherit; text-decoration: none;">
|
||||||
|
MY DASHBOARD
|
||||||
|
<span>// iNTERCEPT</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="status-bar">
|
||||||
|
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
||||||
|
<a href="/" class="back-link">Main Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Unified Navigation -->
|
||||||
|
{% set active_mode = 'mydashboard' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
|
||||||
|
<!-- Stats Strip -->
|
||||||
|
<div class="stats-strip">
|
||||||
|
<!-- Stats content -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Dashboard Content -->
|
||||||
|
<main class="dashboard">
|
||||||
|
<!-- Your dashboard layout -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Dashboard JavaScript
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the Stylesheet
|
||||||
|
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* My Dashboard Styles
|
||||||
|
*/
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Dashboard-specific aliases */
|
||||||
|
--bg-dark: var(--bg-primary);
|
||||||
|
--bg-panel: var(--bg-secondary);
|
||||||
|
--bg-card: var(--bg-tertiary);
|
||||||
|
--grid-line: rgba(74, 158, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Your dashboard styles */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### DO
|
||||||
|
|
||||||
|
- Use design tokens for all colors, spacing, and typography
|
||||||
|
- Include the nav partial on all pages for consistent navigation
|
||||||
|
- Set `active_mode` before including the nav partial
|
||||||
|
- Use semantic component classes (`btn`, `panel`, `badge`, etc.)
|
||||||
|
- Support both light and dark themes
|
||||||
|
- Test on mobile viewports
|
||||||
|
|
||||||
|
### DON'T
|
||||||
|
|
||||||
|
- Hardcode color values - use CSS variables
|
||||||
|
- Create new color variations without adding to tokens
|
||||||
|
- Duplicate navigation markup - use the partial
|
||||||
|
- Skip the favicon and design tokens imports
|
||||||
|
- Use inline styles for layout (use utility classes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
templates/
|
||||||
|
├── layout/
|
||||||
|
│ ├── base.html # Standard page base
|
||||||
|
│ └── base_dashboard.html # Dashboard page base
|
||||||
|
├── partials/
|
||||||
|
│ ├── nav.html # Unified navigation
|
||||||
|
│ ├── page_header.html # Page title component
|
||||||
|
│ └── settings-modal.html # Settings modal
|
||||||
|
├── components/
|
||||||
|
│ ├── card.html # Panel/card component
|
||||||
|
│ ├── empty_state.html # Empty state placeholder
|
||||||
|
│ ├── loading.html # Loading spinner
|
||||||
|
│ ├── stats_strip.html # Stats bar component
|
||||||
|
│ └── status_badge.html # Status indicator
|
||||||
|
├── index.html # Main dashboard
|
||||||
|
├── adsb_dashboard.html # Aircraft tracking
|
||||||
|
├── ais_dashboard.html # Vessel tracking
|
||||||
|
└── satellite_dashboard.html # Satellite tracking
|
||||||
|
|
||||||
|
static/css/
|
||||||
|
├── core/
|
||||||
|
│ ├── variables.css # Design tokens
|
||||||
|
│ ├── base.css # Reset & typography
|
||||||
|
│ ├── components.css # Component styles
|
||||||
|
│ └── layout.css # Layout styles
|
||||||
|
├── index.css # Main dashboard styles
|
||||||
|
├── adsb_dashboard.css # Aircraft dashboard
|
||||||
|
├── ais_dashboard.css # Vessel dashboard
|
||||||
|
├── satellite_dashboard.css # Satellite dashboard
|
||||||
|
└── responsive.css # Responsive breakpoints
|
||||||
|
```
|
||||||
+1
-1
@@ -404,7 +404,7 @@ def iss_position():
|
|||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Open Notify API failed: {e}")
|
logger.debug(f"Open Notify API failed: {e}")
|
||||||
|
|
||||||
# Try fallback API: Where The ISS At
|
# Try fallback API: Where The ISS At
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* ADSB Dashboard Styles
|
||||||
|
* Imports design tokens for consistency, with dashboard-specific overrides
|
||||||
|
*/
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
@import url('./core/layout.css');
|
||||||
|
@import url('./core/components.css');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -5,23 +13,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0a0c10;
|
/* Dashboard-specific aliases (for backward compatibility) */
|
||||||
--bg-panel: #0f1218;
|
--bg-dark: var(--bg-primary);
|
||||||
--bg-card: #151a23;
|
--bg-panel: var(--bg-secondary);
|
||||||
--border-color: #1f2937;
|
--bg-card: var(--bg-tertiary);
|
||||||
--border-glow: #4a9eff;
|
/* Use tokens for shared values, keep custom values for dashboard-specific */
|
||||||
--text-primary: #e8eaed;
|
|
||||||
--text-secondary: #9ca3af;
|
|
||||||
--text-dim: #4b5563;
|
|
||||||
--accent-green: #22c55e;
|
|
||||||
--accent-cyan: #4a9eff;
|
|
||||||
--accent-orange: #f59e0b;
|
|
||||||
--accent-red: #ef4444;
|
|
||||||
--accent-yellow: #eab308;
|
|
||||||
--accent-amber: #d4a853;
|
|
||||||
--grid-line: rgba(74, 158, 255, 0.08);
|
--grid-line: rgba(74, 158, 255, 0.08);
|
||||||
--radar-cyan: #4a9eff;
|
--radar-cyan: var(--accent-cyan);
|
||||||
--radar-bg: #0f1218;
|
--radar-bg: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
/* AIS Dashboard - Vessel Tracking Interface */
|
/**
|
||||||
/* Styled to match ADSB Dashboard */
|
* AIS Dashboard Styles - Vessel Tracking Interface
|
||||||
|
* Imports design tokens for consistency, with dashboard-specific overrides
|
||||||
|
*/
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
@import url('./core/layout.css');
|
||||||
|
@import url('./core/components.css');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -8,23 +13,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0a0c10;
|
/* Dashboard-specific aliases (for backward compatibility) */
|
||||||
--bg-panel: #0f1218;
|
--bg-dark: var(--bg-primary);
|
||||||
--bg-card: #151a23;
|
--bg-panel: var(--bg-secondary);
|
||||||
--border-color: #1f2937;
|
--bg-card: var(--bg-tertiary);
|
||||||
--border-glow: #4a9eff;
|
/* Use tokens for shared values, keep custom values for dashboard-specific */
|
||||||
--text-primary: #e8eaed;
|
|
||||||
--text-secondary: #9ca3af;
|
|
||||||
--text-dim: #4b5563;
|
|
||||||
--accent-green: #22c55e;
|
|
||||||
--accent-cyan: #4a9eff;
|
|
||||||
--accent-orange: #f59e0b;
|
|
||||||
--accent-red: #ef4444;
|
|
||||||
--accent-yellow: #eab308;
|
|
||||||
--accent-amber: #d4a853;
|
|
||||||
--grid-line: rgba(74, 158, 255, 0.08);
|
--grid-line: rgba(74, 158, 255, 0.08);
|
||||||
--radar-cyan: #4a9eff;
|
--radar-cyan: var(--accent-cyan);
|
||||||
--radar-bg: #0f1218;
|
--radar-bg: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -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: 'JetBrains Mono', monospace;
|
||||||
|
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: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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); }
|
||||||
@@ -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: '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
-62
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* INTERCEPT Main Styles
|
||||||
|
* Legacy styles for index.html - will be incrementally refactored
|
||||||
|
*
|
||||||
|
* Note: Design tokens are now imported from core/variables.css
|
||||||
|
* New pages should import core CSS directly in templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Import design tokens (provides CSS variables) */
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
|
||||||
|
/* Font import - kept for backward compatibility with pages not using base template */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -6,64 +18,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Tactical dark palette */
|
|
||||||
--bg-primary: #0a0c10;
|
|
||||||
--bg-secondary: #0f1218;
|
|
||||||
--bg-tertiary: #151a23;
|
|
||||||
--bg-card: #121620;
|
|
||||||
--bg-elevated: #1a202c;
|
|
||||||
|
|
||||||
/* Accent colors - sophisticated blue/amber */
|
|
||||||
--accent-cyan: #4a9eff;
|
|
||||||
--accent-cyan-dim: rgba(74, 158, 255, 0.15);
|
|
||||||
--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-amber: #d4a853;
|
|
||||||
--accent-amber-dim: rgba(212, 168, 83, 0.15);
|
|
||||||
|
|
||||||
/* Text hierarchy */
|
|
||||||
--text-primary: #e8eaed;
|
|
||||||
--text-secondary: #9ca3af;
|
|
||||||
--text-dim: #4b5563;
|
|
||||||
--text-muted: #374151;
|
|
||||||
|
|
||||||
/* Borders */
|
|
||||||
--border-color: #1f2937;
|
|
||||||
--border-light: #374151;
|
|
||||||
--border-glow: rgba(74, 158, 255, 0.2);
|
|
||||||
|
|
||||||
/* Status colors */
|
|
||||||
--status-online: #22c55e;
|
|
||||||
--status-warning: #f59e0b;
|
|
||||||
--status-error: #ef4444;
|
|
||||||
--status-offline: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] {
|
|
||||||
--bg-primary: #f8fafc;
|
|
||||||
--bg-secondary: #f1f5f9;
|
|
||||||
--bg-tertiary: #e2e8f0;
|
|
||||||
--bg-card: #ffffff;
|
|
||||||
--bg-elevated: #f8fafc;
|
|
||||||
--accent-cyan: #2563eb;
|
|
||||||
--accent-cyan-dim: rgba(37, 99, 235, 0.1);
|
|
||||||
--accent-green: #16a34a;
|
|
||||||
--accent-red: #dc2626;
|
|
||||||
--accent-orange: #d97706;
|
|
||||||
--accent-amber: #b45309;
|
|
||||||
--text-primary: #0f172a;
|
|
||||||
--text-secondary: #475569;
|
|
||||||
--text-dim: #94a3b8;
|
|
||||||
--text-muted: #cbd5e1;
|
|
||||||
--border-color: #e2e8f0;
|
|
||||||
--border-light: #cbd5e1;
|
|
||||||
--border-glow: rgba(37, 99, 235, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] body {
|
[data-theme="light"] body {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
@@ -686,9 +640,11 @@ header h1 {
|
|||||||
/* Mode Navigation Bar */
|
/* Mode Navigation Bar */
|
||||||
.mode-nav {
|
.mode-nav {
|
||||||
display: none;
|
display: none;
|
||||||
background: var(--bg-tertiary);
|
background: #151a23 !important; /* Explicit color - forced to ensure consistency */
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
@@ -1458,6 +1414,7 @@ header h1 .tagline {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0; /* Reset any inherited margins - spacing handled by parent gap */
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h3 {
|
.section h3 {
|
||||||
@@ -1634,6 +1591,101 @@ header h1 .tagline {
|
|||||||
border-color: var(--accent-cyan);
|
border-color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* WiFi Mode Tab Buttons */
|
||||||
|
.wifi-mode-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-mode-tab:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-mode-tab.active {
|
||||||
|
background: var(--accent-green);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-mode-tab.active:hover {
|
||||||
|
background: #1db954;
|
||||||
|
border-color: #1db954;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi Start/Stop Buttons */
|
||||||
|
.wifi-start-btn {
|
||||||
|
background: var(--accent-green) !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border-color: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-start-btn:hover {
|
||||||
|
background: #1db954 !important;
|
||||||
|
border-color: #1db954 !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-stop-btn {
|
||||||
|
background: var(--accent-red) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--accent-red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-stop-btn:hover {
|
||||||
|
background: #e62e50 !important;
|
||||||
|
border-color: #e62e50 !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 51, 102, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi Monitor Mode Buttons */
|
||||||
|
.wifi-monitor-btn {
|
||||||
|
background: var(--accent-green) !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border-color: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-monitor-btn:hover {
|
||||||
|
background: #1db954 !important;
|
||||||
|
border-color: #1db954 !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-monitor-stop-btn {
|
||||||
|
background: var(--accent-orange) !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border-color: var(--accent-orange) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-monitor-stop-btn:hover {
|
||||||
|
background: #e68a00 !important;
|
||||||
|
border-color: #e68a00 !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 159, 28, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi Danger/Attack Buttons */
|
||||||
|
.wifi-danger-btn {
|
||||||
|
border-color: var(--accent-red) !important;
|
||||||
|
color: var(--accent-red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-danger-btn:hover {
|
||||||
|
background: var(--accent-red) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 51, 102, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.run-btn {
|
.run-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -6035,13 +6087,15 @@ body::before {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-action-btn.scan {
|
.radio-action-btn.scan,
|
||||||
|
.radio-action-btn.listen {
|
||||||
background: var(--accent-green);
|
background: var(--accent-green);
|
||||||
border-color: var(--accent-green);
|
border-color: var(--accent-green);
|
||||||
color: #000;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-action-btn.scan:hover:not(:disabled) {
|
.radio-action-btn.scan:hover:not(:disabled),
|
||||||
|
.radio-action-btn.listen:hover:not(:disabled) {
|
||||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
box-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,17 @@
|
|||||||
.threat-card.low.active { background: rgba(0,255,136,0.2); }
|
.threat-card.low.active { background: rgba(0,255,136,0.2); }
|
||||||
|
|
||||||
/* TSCM Dashboard */
|
/* TSCM Dashboard */
|
||||||
|
/* Ensure output-panel doesn't clip TSCM content */
|
||||||
|
.output-panel:has(.tscm-dashboard) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.tscm-dashboard {
|
.tscm-dashboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
overflow-y: auto;
|
overflow: visible;
|
||||||
padding-bottom: 80px; /* Space for status bar */
|
padding: 20px 16px 80px 16px; /* Extra top padding for function strip visibility */
|
||||||
}
|
}
|
||||||
.tscm-threat-banner {
|
.tscm-threat-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Satellite Dashboard Styles
|
||||||
|
* Imports design tokens for consistency, with dashboard-specific overrides
|
||||||
|
*/
|
||||||
|
@import url('./core/variables.css');
|
||||||
|
@import url('./core/layout.css');
|
||||||
|
@import url('./core/components.css');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -5,20 +13,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0a0c10;
|
/* Dashboard-specific aliases (for backward compatibility) */
|
||||||
--bg-panel: #0f1218;
|
--bg-dark: var(--bg-primary);
|
||||||
--bg-card: #151a23;
|
--bg-panel: var(--bg-secondary);
|
||||||
--border-color: #1f2937;
|
--bg-card: var(--bg-tertiary);
|
||||||
--border-glow: #4a9eff;
|
/* Use tokens for shared values, keep custom values for dashboard-specific */
|
||||||
--text-primary: #e8eaed;
|
|
||||||
--text-secondary: #9ca3af;
|
|
||||||
--text-dim: #4b5563;
|
|
||||||
--accent-cyan: #4a9eff;
|
|
||||||
--accent-green: #22c55e;
|
|
||||||
--accent-orange: #f59e0b;
|
|
||||||
--accent-red: #ef4444;
|
|
||||||
--accent-purple: #a855f7;
|
|
||||||
--accent-amber: #d4a853;
|
|
||||||
--grid-line: rgba(74, 158, 255, 0.08);
|
--grid-line: rgba(74, 158, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +210,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main dashboard grid */
|
/* Main dashboard grid */
|
||||||
|
/* Header ~60px + Controls bar ~55px = ~115px */
|
||||||
.dashboard {
|
.dashboard {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -218,7 +218,8 @@ body {
|
|||||||
grid-template-columns: 1fr 1fr 340px;
|
grid-template-columns: 1fr 1fr 340px;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: calc(100vh - 60px);
|
height: calc(100dvh - 115px);
|
||||||
|
height: calc(100vh - 115px); /* Fallback */
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +697,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100dvh - 115px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.polar-container,
|
.polar-container,
|
||||||
|
|||||||
@@ -227,6 +227,9 @@ function startScanner() {
|
|||||||
isScannerPaused = false;
|
isScannerPaused = false;
|
||||||
scannerSignalActive = false;
|
scannerSignalActive = false;
|
||||||
|
|
||||||
|
// Update listening strip
|
||||||
|
updateListeningStripRunning(true);
|
||||||
|
|
||||||
// Update controls (with null checks)
|
// Update controls (with null checks)
|
||||||
const startBtn = document.getElementById('scannerStartBtn');
|
const startBtn = document.getElementById('scannerStartBtn');
|
||||||
if (startBtn) {
|
if (startBtn) {
|
||||||
@@ -289,6 +292,9 @@ function stopScanner() {
|
|||||||
scannerSignalActive = false;
|
scannerSignalActive = false;
|
||||||
currentSignalLevel = 0;
|
currentSignalLevel = 0;
|
||||||
|
|
||||||
|
// Update listening strip
|
||||||
|
updateListeningStripRunning(false);
|
||||||
|
|
||||||
// Re-enable listen button (will be in local mode after stop)
|
// Re-enable listen button (will be in local mode after stop)
|
||||||
updateListenButtonState(false);
|
updateListenButtonState(false);
|
||||||
|
|
||||||
@@ -572,6 +578,10 @@ function handleFrequencyUpdate(data) {
|
|||||||
const mainFreq = document.getElementById('mainScannerFreq');
|
const mainFreq = document.getElementById('mainScannerFreq');
|
||||||
if (mainFreq) mainFreq.textContent = freqStr;
|
if (mainFreq) mainFreq.textContent = freqStr;
|
||||||
|
|
||||||
|
// Update function strip frequency
|
||||||
|
const stripFreq = document.getElementById('listeningStripFreq');
|
||||||
|
if (stripFreq) stripFreq.textContent = freqStr;
|
||||||
|
|
||||||
// Update progress bar
|
// Update progress bar
|
||||||
const progress = ((data.frequency - scannerStartFreq) / (scannerEndFreq - scannerStartFreq)) * 100;
|
const progress = ((data.frequency - scannerStartFreq) / (scannerEndFreq - scannerStartFreq)) * 100;
|
||||||
const progressBar = document.getElementById('scannerProgressBar');
|
const progressBar = document.getElementById('scannerProgressBar');
|
||||||
@@ -622,6 +632,10 @@ function handleSignalFound(data) {
|
|||||||
const mainSignalCount = document.getElementById('mainSignalCount');
|
const mainSignalCount = document.getElementById('mainSignalCount');
|
||||||
if (mainSignalCount) mainSignalCount.textContent = scannerSignalCount;
|
if (mainSignalCount) mainSignalCount.textContent = scannerSignalCount;
|
||||||
|
|
||||||
|
// Update function strip signal count
|
||||||
|
const stripSignals = document.getElementById('listeningStripSignals');
|
||||||
|
if (stripSignals) stripSignals.textContent = scannerSignalCount;
|
||||||
|
|
||||||
// Update sidebar
|
// Update sidebar
|
||||||
updateScannerDisplay('SIGNAL FOUND', 'var(--accent-green)');
|
updateScannerDisplay('SIGNAL FOUND', 'var(--accent-green)');
|
||||||
const signalPanel = document.getElementById('scannerSignalPanel');
|
const signalPanel = document.getElementById('scannerSignalPanel');
|
||||||
@@ -2233,6 +2247,15 @@ function updateDirectListenUI(isPlaying, freq) {
|
|||||||
if (quickFreq && freq) {
|
if (quickFreq && freq) {
|
||||||
quickFreq.textContent = freq.toFixed(3) + ' MHz';
|
quickFreq.textContent = freq.toFixed(3) + ' MHz';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update function strip
|
||||||
|
updateListeningStripRunning(isPlaying, 'listen');
|
||||||
|
|
||||||
|
// Update strip frequency display
|
||||||
|
if (freq) {
|
||||||
|
const stripFreq = document.getElementById('listeningStripFreq');
|
||||||
|
if (stripFreq) stripFreq.textContent = freq.toFixed(3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2597,3 +2620,116 @@ window.tuneToFrequency = tuneToFrequency;
|
|||||||
window.clearScannerLog = clearScannerLog;
|
window.clearScannerLog = clearScannerLog;
|
||||||
window.exportScannerLog = exportScannerLog;
|
window.exportScannerLog = exportScannerLog;
|
||||||
|
|
||||||
|
// ============== FUNCTION STRIP SUPPORT ==============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the listening post function strip running state
|
||||||
|
*/
|
||||||
|
function updateListeningStripRunning(running, mode = 'scan') {
|
||||||
|
const listeningStripDot = document.getElementById('listeningStripDot');
|
||||||
|
const listeningStripStatus = document.getElementById('listeningStripStatus');
|
||||||
|
const listeningStripListenBtn = document.getElementById('listeningStripListenBtn');
|
||||||
|
const listeningStripScanBtn = document.getElementById('listeningStripScanBtn');
|
||||||
|
const listeningStripStopBtn = document.getElementById('listeningStripStopBtn');
|
||||||
|
const listeningStripFreqInput = document.getElementById('listeningStripFreqInput');
|
||||||
|
const listeningStripMode = document.getElementById('listeningStripMode');
|
||||||
|
const listeningStripGain = document.getElementById('listeningStripGain');
|
||||||
|
|
||||||
|
if (listeningStripDot) listeningStripDot.className = 'status-dot ' + (running ? (mode === 'listen' ? 'listening' : 'scanning') : 'inactive');
|
||||||
|
if (listeningStripStatus) {
|
||||||
|
listeningStripStatus.textContent = running ? (mode === 'listen' ? 'LISTENING' : 'SCANNING') : 'STANDBY';
|
||||||
|
listeningStripStatus.style.color = running ? (mode === 'listen' ? 'var(--accent-green)' : 'var(--accent-cyan)') : '';
|
||||||
|
}
|
||||||
|
if (listeningStripListenBtn) listeningStripListenBtn.style.display = running ? 'none' : 'inline-block';
|
||||||
|
if (listeningStripScanBtn) listeningStripScanBtn.style.display = running ? 'none' : 'inline-block';
|
||||||
|
if (listeningStripStopBtn) listeningStripStopBtn.style.display = running ? 'inline-block' : 'none';
|
||||||
|
if (listeningStripFreqInput) listeningStripFreqInput.disabled = running;
|
||||||
|
if (listeningStripMode) listeningStripMode.disabled = running;
|
||||||
|
if (listeningStripGain) listeningStripGain.disabled = running;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update listening strip stats
|
||||||
|
*/
|
||||||
|
function updateListeningStrip(freq, bandwidth, signalCount) {
|
||||||
|
const freqEl = document.getElementById('listeningStripFreq');
|
||||||
|
const bwEl = document.getElementById('listeningStripBW');
|
||||||
|
const signalsEl = document.getElementById('listeningStripSignals');
|
||||||
|
|
||||||
|
if (freqEl && freq !== undefined) freqEl.textContent = freq;
|
||||||
|
if (bwEl && bandwidth !== undefined) bwEl.textContent = bandwidth;
|
||||||
|
if (signalsEl && signalCount !== undefined) signalsEl.textContent = signalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening from the function strip
|
||||||
|
*/
|
||||||
|
function startListeningFromStrip() {
|
||||||
|
// Get values from strip
|
||||||
|
const freq = document.getElementById('listeningStripFreqInput')?.value;
|
||||||
|
const mode = document.getElementById('listeningStripMode')?.value;
|
||||||
|
const gain = document.getElementById('listeningStripGain')?.value;
|
||||||
|
|
||||||
|
// Update the main controls if they exist
|
||||||
|
if (freq) {
|
||||||
|
const mainFreqInput = document.getElementById('radioScanStart');
|
||||||
|
if (mainFreqInput) mainFreqInput.value = freq;
|
||||||
|
}
|
||||||
|
if (mode) {
|
||||||
|
currentModulation = mode.toLowerCase();
|
||||||
|
}
|
||||||
|
if (gain) {
|
||||||
|
const gainValueEl = document.getElementById('radioGainValue');
|
||||||
|
if (gainValueEl) gainValueEl.textContent = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the scanner
|
||||||
|
startScanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen directly from the function strip (audio only, no scanning)
|
||||||
|
*/
|
||||||
|
function listenFromStrip() {
|
||||||
|
// Get values from strip
|
||||||
|
const freq = document.getElementById('listeningStripFreqInput')?.value;
|
||||||
|
const mode = document.getElementById('listeningStripMode')?.value;
|
||||||
|
const gain = document.getElementById('listeningStripGain')?.value;
|
||||||
|
|
||||||
|
// Update the main controls if they exist
|
||||||
|
if (freq) {
|
||||||
|
const mainFreqInput = document.getElementById('radioScanStart');
|
||||||
|
if (mainFreqInput) mainFreqInput.value = freq;
|
||||||
|
}
|
||||||
|
if (mode) {
|
||||||
|
currentModulation = mode.toLowerCase();
|
||||||
|
}
|
||||||
|
if (gain) {
|
||||||
|
const gainValueEl = document.getElementById('radioGainValue');
|
||||||
|
if (gainValueEl) gainValueEl.textContent = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start direct audio listening
|
||||||
|
toggleDirectListen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop listening/scanning from the function strip
|
||||||
|
*/
|
||||||
|
function stopListening() {
|
||||||
|
// Stop both scanner and audio
|
||||||
|
if (isScannerRunning) {
|
||||||
|
stopScanner();
|
||||||
|
}
|
||||||
|
if (isDirectListening) {
|
||||||
|
stopDirectListen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export strip functions
|
||||||
|
window.updateListeningStripRunning = updateListeningStripRunning;
|
||||||
|
window.updateListeningStrip = updateListeningStrip;
|
||||||
|
window.startListeningFromStrip = startListeningFromStrip;
|
||||||
|
window.listenFromStrip = listenFromStrip;
|
||||||
|
window.stopListening = stopListening;
|
||||||
|
|
||||||
|
|||||||
+435
-11
@@ -336,6 +336,9 @@ const WiFiMode = (function() {
|
|||||||
if (elements.scanModeDeep) {
|
if (elements.scanModeDeep) {
|
||||||
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize button visibility (default to quick mode)
|
||||||
|
setScanMode('quick');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setScanMode(mode) {
|
function setScanMode(mode) {
|
||||||
@@ -349,6 +352,21 @@ const WiFiMode = (function() {
|
|||||||
elements.scanModeDeep.classList.toggle('active', mode === 'deep');
|
elements.scanModeDeep.classList.toggle('active', mode === 'deep');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update button visibility based on mode
|
||||||
|
if (!isScanning) {
|
||||||
|
if (elements.quickScanBtn) {
|
||||||
|
elements.quickScanBtn.style.display = mode === 'quick' ? 'inline-block' : 'none';
|
||||||
|
elements.quickScanBtn.textContent = 'Start Quick Scan';
|
||||||
|
}
|
||||||
|
if (elements.deepScanBtn) {
|
||||||
|
elements.deepScanBtn.style.display = mode === 'deep' ? 'inline-block' : 'none';
|
||||||
|
elements.deepScanBtn.textContent = 'Start Deep Scan';
|
||||||
|
}
|
||||||
|
if (elements.stopScanBtn) {
|
||||||
|
elements.stopScanBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Scan mode set to:', mode);
|
console.log('[WiFiMode] Scan mode set to:', mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +375,10 @@ const WiFiMode = (function() {
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
async function startQuickScan() {
|
async function startQuickScan() {
|
||||||
if (isScanning) return;
|
if (isScanning) {
|
||||||
|
showInfo('Scan already in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for agent mode conflicts
|
// Check for agent mode conflicts
|
||||||
if (!checkAgentConflicts()) {
|
if (!checkAgentConflicts()) {
|
||||||
@@ -365,6 +386,7 @@ const WiFiMode = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Starting quick scan...');
|
console.log('[WiFiMode] Starting quick scan...');
|
||||||
|
showInfo('Starting quick scan...');
|
||||||
setScanning(true, 'quick');
|
setScanning(true, 'quick');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -436,6 +458,9 @@ const WiFiMode = (function() {
|
|||||||
// Process results
|
// Process results
|
||||||
processQuickScanResult({ ...scanResult, access_points: accessPoints });
|
processQuickScanResult({ ...scanResult, access_points: accessPoints });
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
showInfo(`Found ${accessPoints.length} network${accessPoints.length !== 1 ? 's' : ''}`);
|
||||||
|
|
||||||
// For quick scan, we're done after one scan
|
// For quick scan, we're done after one scan
|
||||||
// But keep polling if user wants continuous updates
|
// But keep polling if user wants continuous updates
|
||||||
if (scanMode === 'quick') {
|
if (scanMode === 'quick') {
|
||||||
@@ -449,7 +474,10 @@ const WiFiMode = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startDeepScan() {
|
async function startDeepScan() {
|
||||||
if (isScanning) return;
|
if (isScanning) {
|
||||||
|
showInfo('Scan already in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for agent mode conflicts
|
// Check for agent mode conflicts
|
||||||
if (!checkAgentConflicts()) {
|
if (!checkAgentConflicts()) {
|
||||||
@@ -457,6 +485,7 @@ const WiFiMode = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WiFiMode] Starting deep scan...');
|
console.log('[WiFiMode] Starting deep scan...');
|
||||||
|
showInfo('Starting deep scan (requires monitor mode)...');
|
||||||
setScanning(true, 'deep');
|
setScanning(true, 'deep');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -516,6 +545,7 @@ const WiFiMode = (function() {
|
|||||||
|
|
||||||
async function stopScan() {
|
async function stopScan() {
|
||||||
console.log('[WiFiMode] Stopping scan...');
|
console.log('[WiFiMode] Stopping scan...');
|
||||||
|
showInfo('Stopping scan...');
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
if (pollTimer) {
|
if (pollTimer) {
|
||||||
@@ -538,6 +568,7 @@ const WiFiMode = (function() {
|
|||||||
} else if (scanMode === 'deep') {
|
} else if (scanMode === 'deep') {
|
||||||
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' });
|
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' });
|
||||||
}
|
}
|
||||||
|
showInfo('Scan stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[WiFiMode] Error stopping scan:', error);
|
console.warn('[WiFiMode] Error stopping scan:', error);
|
||||||
}
|
}
|
||||||
@@ -549,15 +580,21 @@ const WiFiMode = (function() {
|
|||||||
isScanning = scanning;
|
isScanning = scanning;
|
||||||
if (mode) scanMode = mode;
|
if (mode) scanMode = mode;
|
||||||
|
|
||||||
// Update buttons
|
// Update buttons based on scanning state and current mode
|
||||||
if (elements.quickScanBtn) {
|
if (scanning) {
|
||||||
elements.quickScanBtn.style.display = scanning ? 'none' : 'inline-block';
|
// Scanning: hide start buttons, show stop button
|
||||||
}
|
if (elements.quickScanBtn) elements.quickScanBtn.style.display = 'none';
|
||||||
if (elements.deepScanBtn) {
|
if (elements.deepScanBtn) elements.deepScanBtn.style.display = 'none';
|
||||||
elements.deepScanBtn.style.display = scanning ? 'none' : 'inline-block';
|
if (elements.stopScanBtn) elements.stopScanBtn.style.display = 'inline-block';
|
||||||
}
|
} else {
|
||||||
if (elements.stopScanBtn) {
|
// Not scanning: show appropriate start button based on mode, hide stop
|
||||||
elements.stopScanBtn.style.display = scanning ? 'inline-block' : 'none';
|
if (elements.quickScanBtn) {
|
||||||
|
elements.quickScanBtn.style.display = scanMode === 'quick' ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
if (elements.deepScanBtn) {
|
||||||
|
elements.deepScanBtn.style.display = scanMode === 'deep' ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
if (elements.stopScanBtn) elements.stopScanBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status
|
// Update status
|
||||||
@@ -1426,3 +1463,390 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
WiFiMode.init();
|
WiFiMode.init();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// WiFi Helper Functions
|
||||||
|
// Implements UI helper functions for Monitor Mode, Deauth, Watch List, etc.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const WiFiHelpers = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Note: Monitor mode and attack endpoints are in /wifi/ (v1), not /wifi/v2/
|
||||||
|
const CONFIG = {
|
||||||
|
apiBase: '/wifi/v2',
|
||||||
|
apiV1Base: '/wifi',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch list state
|
||||||
|
let watchList = [];
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Monitor Mode
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
async function enableMonitorMode() {
|
||||||
|
const iface = document.getElementById('wifiInterfaceSelect')?.value;
|
||||||
|
const killProcesses = document.getElementById('killProcesses')?.checked || false;
|
||||||
|
const startBtn = document.getElementById('monitorStartBtn');
|
||||||
|
const stopBtn = document.getElementById('monitorStopBtn');
|
||||||
|
const statusEl = document.getElementById('monitorStatus');
|
||||||
|
|
||||||
|
// Provide immediate feedback
|
||||||
|
console.log('[WiFiHelpers] Enable monitor mode clicked');
|
||||||
|
showNotification('Monitor Mode', 'Enabling monitor mode...', 'info');
|
||||||
|
|
||||||
|
if (!iface) {
|
||||||
|
showNotification('Monitor Mode', 'No interface selected. Please wait for interface detection.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI to show processing
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.disabled = true;
|
||||||
|
startBtn.textContent = 'Enabling...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use v1 API which has monitor mode support
|
||||||
|
const response = await fetch(`${CONFIG.apiV1Base}/monitor`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
interface: iface,
|
||||||
|
action: 'enable',
|
||||||
|
kill_processes: killProcesses
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || result.error) {
|
||||||
|
throw new Error(result.error || result.message || 'Failed to enable monitor mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - update UI
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.style.display = 'none';
|
||||||
|
startBtn.disabled = false;
|
||||||
|
startBtn.textContent = 'Enable Monitor';
|
||||||
|
}
|
||||||
|
if (stopBtn) stopBtn.style.display = 'inline-block';
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `Monitor mode: <span style="color: var(--accent-green);">Active</span> (${result.monitor_interface || iface})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Monitor Mode', `Enabled on ${result.monitor_interface || iface}`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WiFiHelpers] Enable monitor mode error:', error);
|
||||||
|
showNotification('Monitor Mode', error.message, 'error');
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
if (startBtn) {
|
||||||
|
startBtn.disabled = false;
|
||||||
|
startBtn.textContent = 'Enable Monitor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableMonitorMode() {
|
||||||
|
const iface = document.getElementById('wifiInterfaceSelect')?.value;
|
||||||
|
const startBtn = document.getElementById('monitorStartBtn');
|
||||||
|
const stopBtn = document.getElementById('monitorStopBtn');
|
||||||
|
const statusEl = document.getElementById('monitorStatus');
|
||||||
|
|
||||||
|
console.log('[WiFiHelpers] Disable monitor mode clicked');
|
||||||
|
showNotification('Monitor Mode', 'Disabling monitor mode...', 'info');
|
||||||
|
|
||||||
|
if (stopBtn) {
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
stopBtn.textContent = 'Disabling...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use v1 API which has monitor mode support
|
||||||
|
const response = await fetch(`${CONFIG.apiV1Base}/monitor`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
interface: iface,
|
||||||
|
action: 'disable'
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || result.error) {
|
||||||
|
throw new Error(result.error || result.message || 'Failed to disable monitor mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - update UI
|
||||||
|
if (stopBtn) {
|
||||||
|
stopBtn.style.display = 'none';
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.textContent = 'Disable Monitor';
|
||||||
|
}
|
||||||
|
if (startBtn) startBtn.style.display = 'inline-block';
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `Monitor mode: <span style="color: var(--accent-red);">Inactive</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Monitor Mode', 'Disabled successfully', 'info');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WiFiHelpers] Disable monitor mode error:', error);
|
||||||
|
showNotification('Monitor Mode', error.message, 'error');
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
if (stopBtn) {
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.textContent = 'Disable Monitor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Deauth Attack
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
async function sendDeauth() {
|
||||||
|
const bssid = document.getElementById('targetBssid')?.value?.trim();
|
||||||
|
const client = document.getElementById('targetClient')?.value?.trim() || 'FF:FF:FF:FF:FF:FF';
|
||||||
|
const count = parseInt(document.getElementById('deauthCount')?.value) || 5;
|
||||||
|
const iface = document.getElementById('wifiInterfaceSelect')?.value;
|
||||||
|
|
||||||
|
console.log('[WiFiHelpers] Send deauth clicked');
|
||||||
|
|
||||||
|
if (!bssid || !/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(bssid)) {
|
||||||
|
showNotification('Deauth Attack', 'Please enter a valid target BSSID (e.g., AA:BB:CC:DD:EE:FF)', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm action
|
||||||
|
if (!confirm(`Send ${count} deauth frames to ${bssid}?\n\nWARNING: Only use on networks you are authorized to test.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Deauth Attack', `Sending ${count} deauth frames...`, 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use v1 API which has deauth support
|
||||||
|
const response = await fetch(`${CONFIG.apiV1Base}/deauth`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
bssid: bssid,
|
||||||
|
client: client,
|
||||||
|
count: count,
|
||||||
|
interface: iface
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || result.error) {
|
||||||
|
throw new Error(result.error || result.message || 'Deauth attack failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Deauth Attack', `Sent ${count} deauth frames to ${bssid}`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WiFiHelpers] Deauth error:', error);
|
||||||
|
showNotification('Deauth Attack', error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Watch List (Proximity Alerts)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
function addWatchMac() {
|
||||||
|
const input = document.getElementById('watchMacInput');
|
||||||
|
const mac = input?.value?.trim().toUpperCase();
|
||||||
|
|
||||||
|
console.log('[WiFiHelpers] Add watch MAC clicked, value:', mac);
|
||||||
|
|
||||||
|
if (!mac) {
|
||||||
|
showNotification('Watch List', 'Please enter a MAC address', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(mac)) {
|
||||||
|
showNotification('Watch List', 'Invalid format. Use XX:XX:XX:XX:XX:XX', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchList.includes(mac)) {
|
||||||
|
showNotification('Watch List', 'MAC address already in watch list', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchList.push(mac);
|
||||||
|
input.value = '';
|
||||||
|
updateWatchListDisplay();
|
||||||
|
showNotification('Watch List', `Added ${mac} to watch list`, 'success');
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('wifiWatchList', JSON.stringify(watchList));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WiFiHelpers] Could not save watch list to localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWatchMac(mac) {
|
||||||
|
watchList = watchList.filter(m => m !== mac);
|
||||||
|
updateWatchListDisplay();
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('wifiWatchList', JSON.stringify(watchList));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WiFiHelpers] Could not save watch list to localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWatchListDisplay() {
|
||||||
|
const container = document.getElementById('watchList');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (watchList.length === 0) {
|
||||||
|
container.innerHTML = '<span style="color: var(--text-dim);">No MAC addresses in watch list</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = watchList.map(mac => `
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid var(--border-color);">
|
||||||
|
<code style="font-size: 10px;">${mac}</code>
|
||||||
|
<button onclick="WiFiHelpers.removeWatchMac('${mac}')" style="background: none; border: none; color: var(--accent-red); cursor: pointer; font-size: 12px; padding: 2px 6px;">×</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWatchList() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('wifiWatchList');
|
||||||
|
if (saved) {
|
||||||
|
watchList = JSON.parse(saved);
|
||||||
|
updateWatchListDisplay();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WiFiHelpers] Could not load watch list from localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Handshake Capture
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
async function checkCaptureStatus() {
|
||||||
|
const statusEl = document.getElementById('captureStatus');
|
||||||
|
const bssid = document.getElementById('captureTargetBssid')?.textContent;
|
||||||
|
|
||||||
|
console.log('[WiFiHelpers] Check capture status clicked');
|
||||||
|
showNotification('Handshake Capture', 'Checking capture status...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use v1 API which has handshake capture support
|
||||||
|
const response = await fetch(`${CONFIG.apiV1Base}/handshake/status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ bssid: bssid }),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (statusEl) {
|
||||||
|
if (result.capturing || result.running) {
|
||||||
|
statusEl.textContent = result.handshake_found ? 'Handshake captured!' : 'Capturing...';
|
||||||
|
statusEl.style.color = result.handshake_found ? 'var(--accent-green)' : 'var(--accent-orange)';
|
||||||
|
} else {
|
||||||
|
statusEl.textContent = 'Not capturing';
|
||||||
|
statusEl.style.color = 'var(--text-secondary)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.handshake_found) {
|
||||||
|
showNotification('Handshake Capture', `Handshake captured! File: ${result.capture_file || 'check captures folder'}`, 'success');
|
||||||
|
} else if (result.capturing || result.running) {
|
||||||
|
showNotification('Handshake Capture', 'Still capturing, no handshake yet...', 'info');
|
||||||
|
} else {
|
||||||
|
showNotification('Handshake Capture', 'No active capture', 'info');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WiFiHelpers] Check capture status error:', error);
|
||||||
|
showNotification('Handshake Capture', error.message, 'error');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Error checking status';
|
||||||
|
statusEl.style.color = 'var(--accent-red)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopHandshakeCapture() {
|
||||||
|
const panel = document.getElementById('captureStatusPanel');
|
||||||
|
const statusEl = document.getElementById('captureStatus');
|
||||||
|
|
||||||
|
console.log('[WiFiHelpers] Stop handshake capture clicked');
|
||||||
|
showNotification('Handshake Capture', 'Stopping capture...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use v1 API - handshake stop endpoint
|
||||||
|
const response = await fetch(`${CONFIG.apiV1Base}/scan/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || result.error) {
|
||||||
|
throw new Error(result.error || result.message || 'Failed to stop capture');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Stopped';
|
||||||
|
statusEl.style.color = 'var(--text-secondary)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide panel after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (panel) panel.style.display = 'none';
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
showNotification('Handshake Capture', 'Capture stopped', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WiFiHelpers] Stop capture error:', error);
|
||||||
|
showNotification('Handshake Capture', error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Utility
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
function showNotification(title, message, type) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(title, message, type);
|
||||||
|
} else {
|
||||||
|
console.log(`[${type.toUpperCase()}] ${title}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Initialize
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
// Load watch list on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', loadWatchList);
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Public API
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableMonitorMode,
|
||||||
|
disableMonitorMode,
|
||||||
|
sendDeauth,
|
||||||
|
addWatchMac,
|
||||||
|
removeWatchMac,
|
||||||
|
checkCaptureStatus,
|
||||||
|
stopHandshakeCapture,
|
||||||
|
getWatchList: () => [...watchList],
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<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>AIRCRAFT RADAR // INTERCEPT - See the Invisible</title>
|
<title>AIRCRAFT RADAR // iNTERCEPT</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
<!-- Design tokens and shared styles -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||||||
<!-- Fonts - Conditional CDN/Local loading -->
|
<!-- Fonts - Conditional CDN/Local loading -->
|
||||||
{% 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') }}">
|
||||||
@@ -27,8 +31,10 @@
|
|||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
AIRCRAFT RADAR
|
<a href="/" style="color: inherit; text-decoration: none;">
|
||||||
<span>// INTERCEPT - See the Invisible</span>
|
AIRCRAFT RADAR
|
||||||
|
<span>// iNTERCEPT</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<!-- Agent Selector -->
|
<!-- Agent Selector -->
|
||||||
@@ -41,11 +47,13 @@
|
|||||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
|
||||||
<a href="/?mode=aircraft" class="back-link">Main Dashboard</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Unified Navigation -->
|
||||||
|
{% set active_mode = 'adsb' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
|
||||||
<!-- Slim Statistics Bar -->
|
<!-- Slim Statistics Bar -->
|
||||||
<div class="stats-strip">
|
<div class="stats-strip">
|
||||||
<div class="stats-strip-inner">
|
<div class="stats-strip-inner">
|
||||||
|
|||||||
@@ -223,26 +223,6 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation links */
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast notifications */
|
/* Toast notifications */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -301,21 +281,6 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="agents-container">
|
<div class="agents-container">
|
||||||
<div class="nav-links">
|
|
||||||
<a href="#" onclick="history.back(); return false;" class="back-link">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
||||||
</svg>
|
|
||||||
Back
|
|
||||||
</a>
|
|
||||||
<a href="/" class="back-link">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
||||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
|
||||||
</svg>
|
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="agents-header">
|
<div class="agents-header">
|
||||||
<h1>Remote Agents</h1>
|
<h1>Remote Agents</h1>
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<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>VESSEL RADAR // INTERCEPT - See the Invisible</title>
|
<title>VESSEL RADAR // iNTERCEPT</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
<!-- Design tokens and shared styles -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||||||
<!-- Fonts - Conditional CDN/Local loading -->
|
<!-- Fonts - Conditional CDN/Local loading -->
|
||||||
{% 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=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&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' %}
|
||||||
@@ -28,8 +32,10 @@
|
|||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
VESSEL RADAR
|
<a href="/" style="color: inherit; text-decoration: none;">
|
||||||
<span>// INTERCEPT - AIS Tracking</span>
|
VESSEL RADAR
|
||||||
|
<span>// iNTERCEPT</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<!-- Agent Selector -->
|
<!-- Agent Selector -->
|
||||||
@@ -42,11 +48,13 @@
|
|||||||
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
|
|
||||||
<a href="/" class="back-link">Main Dashboard</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Unified Navigation -->
|
||||||
|
{% set active_mode = 'ais' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -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>
|
||||||
+1148
-150
File diff suppressed because it is too large
Load Diff
@@ -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=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&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 %}
|
||||||
@@ -514,8 +514,6 @@
|
|||||||
<span>// MULTI-AGENT VIEW</span>
|
<span>// MULTI-AGENT VIEW</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
<a href="#" onclick="history.back(); return false;">Back</a>
|
|
||||||
<a href="/">Dashboard</a>
|
|
||||||
<a href="/controller/manage">Manage Agents</a>
|
<a href="/controller/manage">Manage Agents</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -2,9 +2,20 @@
|
|||||||
<div id="bluetoothMode" class="mode-content">
|
<div id="bluetoothMode" class="mode-content">
|
||||||
<!-- Capability Status -->
|
<!-- Capability Status -->
|
||||||
<div id="btCapabilityStatus" class="section" style="display: none;">
|
<div id="btCapabilityStatus" class="section" style="display: none;">
|
||||||
|
<span id="btCapabilityMarker" style="display: none;"></span>
|
||||||
<!-- Populated by JavaScript with capability warnings -->
|
<!-- Populated by JavaScript with capability warnings -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Bluetooth Scanning</h3>
|
||||||
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
|
Scan for nearby Bluetooth devices including trackers, phones, and other wireless devices.
|
||||||
|
</p>
|
||||||
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Show All Agents option (visible when agents are available) -->
|
<!-- Show All Agents option (visible when agents are available) -->
|
||||||
<div id="btShowAllAgentsContainer" class="section" style="display: none; padding: 8px;">
|
<div id="btShowAllAgentsContainer" class="section" style="display: none; padding: 8px;">
|
||||||
<label class="inline-checkbox" style="font-size: 10px;">
|
<label class="inline-checkbox" style="font-size: 10px;">
|
||||||
@@ -15,12 +26,6 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Scanner Configuration</h3>
|
<h3>Scanner Configuration</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label>Adapter</label>
|
|
||||||
<select id="btAdapterSelect">
|
|
||||||
<option value="">Detecting adapters...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Scan Mode</label>
|
<label>Scan Mode</label>
|
||||||
<select id="btScanMode">
|
<select id="btScanMode">
|
||||||
@@ -30,14 +35,6 @@
|
|||||||
<option value="bluetoothctl">bluetoothctl (Linux)</option>
|
<option value="bluetoothctl">bluetoothctl (Linux)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>Transport</label>
|
|
||||||
<select id="btTransport">
|
|
||||||
<option value="auto">Auto (BLE + Classic)</option>
|
|
||||||
<option value="le">BLE Only</option>
|
|
||||||
<option value="br_edr">Classic Only</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Duration (seconds, 0 = continuous)</label>
|
<label>Duration (seconds, 0 = continuous)</label>
|
||||||
<input type="number" id="btScanDuration" value="0" min="0" max="300" placeholder="0">
|
<input type="number" id="btScanDuration" value="0" min="0" max="300" placeholder="0">
|
||||||
@@ -54,17 +51,10 @@
|
|||||||
<!-- Message Container for status cards -->
|
<!-- Message Container for status cards -->
|
||||||
<div id="btMessageContainer"></div>
|
<div id="btMessageContainer"></div>
|
||||||
|
|
||||||
<button class="run-btn" id="startBtBtn" onclick="btStartScan()">
|
<div class="section" id="btExportSection" style="margin-top: 10px;">
|
||||||
Start Scanning
|
|
||||||
</button>
|
|
||||||
<button class="stop-btn" id="stopBtBtn" onclick="btStopScan()" style="display: none;">
|
|
||||||
Stop Scanning
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="section" style="margin-top: 10px;">
|
|
||||||
<h3>Export</h3>
|
<h3>Export</h3>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button class="preset-btn" onclick="btExport('csv')" style="flex: 1;">
|
<button class="preset-btn" id="btExportCsvBtn" onclick="btExport('csv')" style="flex: 1;">
|
||||||
Export CSV
|
Export CSV
|
||||||
</button>
|
</button>
|
||||||
<button class="preset-btn" onclick="btExport('json')" style="flex: 1;">
|
<button class="preset-btn" onclick="btExport('json')" style="flex: 1;">
|
||||||
@@ -72,4 +62,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden inputs for action bar sync -->
|
||||||
|
<select id="btAdapterSelect" style="display: none;">
|
||||||
|
<option value="">Detecting adapters...</option>
|
||||||
|
</select>
|
||||||
|
<select id="btTransport" style="display: none;">
|
||||||
|
<option value="auto">Auto (BLE + Classic)</option>
|
||||||
|
<option value="le">BLE Only</option>
|
||||||
|
<option value="br_edr">Classic Only</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<!-- LISTENING POST MODE -->
|
<!-- LISTENING POST MODE -->
|
||||||
<div id="listeningPostMode" class="mode-content">
|
<div id="listeningPostMode" class="mode-content">
|
||||||
|
<div class="section">
|
||||||
|
<h3>Spectrum Analyzer</h3>
|
||||||
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
|
Scan radio frequencies to discover and listen to signals across the RF spectrum.
|
||||||
|
</p>
|
||||||
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Status</h3>
|
<h3>Status</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
<!-- PAGER MODE -->
|
<!-- PAGER MODE -->
|
||||||
<div id="pagerMode" class="mode-content active">
|
<div id="pagerMode" class="mode-content active">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Frequency</h3>
|
<h3>Pager Decoding</h3>
|
||||||
<div class="form-group">
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
<label>Frequency (MHz)</label>
|
Decode POCSAG and FLEX pager messages from emergency services, hospitals, and other organizations.
|
||||||
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
|
</p>
|
||||||
</div>
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
<div class="preset-buttons" id="presetButtons">
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
<!-- Populated by JavaScript -->
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 8px; display: flex; gap: 5px;">
|
|
||||||
<input type="text" id="newPresetFreq" placeholder="New freq (MHz)" style="flex: 1; padding: 6px; background: #0f3460; border: 1px solid #1a1a2e; color: #fff; border-radius: 4px; font-size: 12px;">
|
|
||||||
<button class="preset-btn" onclick="addPreset()" style="background: #2ecc71;">Add</button>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 5px;">
|
|
||||||
<button class="preset-btn" onclick="resetPresets()" style="font-size: 11px;">Reset to Defaults</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,11 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Settings</h3>
|
<h3>Advanced Settings</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label>Gain (dB, 0 = auto)</label>
|
|
||||||
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Squelch Level</label>
|
<label>Squelch Level</label>
|
||||||
<input type="text" id="squelch" value="0" placeholder="0 = off">
|
<input type="text" id="squelch" value="0" placeholder="0 = off">
|
||||||
@@ -75,10 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="run-btn" id="startBtn" onclick="startDecoding()">
|
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
|
||||||
Start Decoding
|
<input type="hidden" id="frequency" value="153.350">
|
||||||
</button>
|
<input type="hidden" id="gain" value="0">
|
||||||
<button class="stop-btn" id="stopBtn" onclick="stopDecoding()" style="display: none;">
|
|
||||||
Stop Decoding
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,40 +1,21 @@
|
|||||||
<!-- RTLAMR UTILITY METER MODE -->
|
<!-- RTLAMR UTILITY METER MODE -->
|
||||||
<div id="rtlamrMode" class="mode-content">
|
<div id="rtlamrMode" class="mode-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Frequency</h3>
|
<h3>Utility Meter Reading</h3>
|
||||||
<div class="form-group">
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
<label>Frequency (MHz)</label>
|
Decode utility meter transmissions (water, gas, electric) using ERT protocol.
|
||||||
<input type="text" id="rtlamrFrequency" value="912.0" placeholder="e.g., 912.0">
|
</p>
|
||||||
</div>
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
<div class="preset-buttons">
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
<button class="preset-btn" onclick="setRtlamrFreq('912.0')">912.0 (NA)</button>
|
|
||||||
<button class="preset-btn" onclick="setRtlamrFreq('868.0')">868.0 (EU)</button>
|
|
||||||
<button class="preset-btn" onclick="setRtlamrFreq('915.0')">915.0</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Settings</h3>
|
<h3>Advanced Settings</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label>Gain (dB, 0 = auto)</label>
|
|
||||||
<input type="text" id="rtlamrGain" value="0" placeholder="0-49 or 0 for auto">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>PPM Correction</label>
|
<label>PPM Correction</label>
|
||||||
<input type="text" id="rtlamrPpm" value="0" placeholder="Frequency correction">
|
<input type="text" id="rtlamrPpm" value="0" placeholder="Frequency correction">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>Message Type</label>
|
|
||||||
<select id="rtlamrMsgType">
|
|
||||||
<option value="scm">SCM (Standard Consumption Message)</option>
|
|
||||||
<option value="scm+">SCM+ (Enhanced)</option>
|
|
||||||
<option value="idm">IDM (Interval Data Message)</option>
|
|
||||||
<option value="netidm">NetIDM (Network IDM)</option>
|
|
||||||
<option value="r900">R900 (Neptune)</option>
|
|
||||||
<option value="r900bcd">R900 BCD</option>
|
|
||||||
<option value="all">All Types</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Filter by Meter ID (optional, comma-separated)</label>
|
<label>Filter by Meter ID (optional, comma-separated)</label>
|
||||||
<input type="text" id="rtlamrFilterId" placeholder="e.g., 12345678,87654321">
|
<input type="text" id="rtlamrFilterId" placeholder="e.g., 12345678,87654321">
|
||||||
@@ -42,10 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Protocols</h3>
|
<h3>Options</h3>
|
||||||
<div class="info-text" style="margin-bottom: 10px;">
|
|
||||||
rtlamr decodes utility meter transmissions (water, gas, electric) using ERT protocol.
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="rtlamrUnique" checked onchange="toggleRtlamrUnique()">
|
<input type="checkbox" id="rtlamrUnique" checked onchange="toggleRtlamrUnique()">
|
||||||
@@ -58,10 +36,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="run-btn" id="startRtlamrBtn" onclick="startRtlamrDecoding()">
|
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
|
||||||
Start Listening
|
<input type="hidden" id="rtlamrFrequency" value="912.0">
|
||||||
</button>
|
<input type="hidden" id="rtlamrGain" value="0">
|
||||||
<button class="stop-btn" id="stopRtlamrBtn" onclick="stopRtlamrDecoding()" style="display: none;">
|
<select id="rtlamrMsgType" style="display: none;">
|
||||||
Stop Listening
|
<option value="scm">SCM (Standard Consumption Message)</option>
|
||||||
</button>
|
<option value="scm+">SCM+ (Enhanced)</option>
|
||||||
|
<option value="idm">IDM (Interval Data Message)</option>
|
||||||
|
<option value="netidm">NetIDM (Network IDM)</option>
|
||||||
|
<option value="r900">R900 (Neptune)</option>
|
||||||
|
<option value="r900bcd">R900 BCD</option>
|
||||||
|
<option value="all">All Types</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
<!-- 433MHz SENSOR MODE -->
|
<!-- 433MHz SENSOR MODE -->
|
||||||
<div id="sensorMode" class="mode-content">
|
<div id="sensorMode" class="mode-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Frequency</h3>
|
<h3>433MHz Sensors</h3>
|
||||||
<div class="form-group">
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
<label>Frequency (MHz)</label>
|
Decode signals from wireless sensors including weather stations, TPMS, doorbells, and 200+ other device protocols.
|
||||||
<input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92">
|
</p>
|
||||||
</div>
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
<div class="preset-buttons">
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
<button class="preset-btn" onclick="setSensorFreq('433.92')">433.92</button>
|
|
||||||
<button class="preset-btn" onclick="setSensorFreq('315.00')">315.00</button>
|
|
||||||
<button class="preset-btn" onclick="setSensorFreq('868.00')">868.00</button>
|
|
||||||
<button class="preset-btn" onclick="setSensorFreq('915.00')">915.00</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Settings</h3>
|
<h3>Advanced Settings</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label>Gain (dB, 0 = auto)</label>
|
|
||||||
<input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>PPM Correction</label>
|
<label>PPM Correction</label>
|
||||||
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
|
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
|
||||||
@@ -27,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Protocols</h3>
|
<h3>Logging</h3>
|
||||||
<div class="info-text" style="margin-bottom: 10px;">
|
<div class="info-text" style="margin-bottom: 10px;">
|
||||||
rtl_433 auto-detects 200+ device protocols including weather stations, TPMS, doorbells, and more.
|
rtl_433 auto-detects 200+ device protocols including weather stations, TPMS, doorbells, and more.
|
||||||
</div>
|
</div>
|
||||||
@@ -39,10 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="run-btn" id="startSensorBtn" onclick="startSensorDecoding()">
|
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
|
||||||
Start Listening
|
<input type="hidden" id="sensorFrequency" value="433.92">
|
||||||
</button>
|
<input type="hidden" id="sensorGain" value="0">
|
||||||
<button class="stop-btn" id="stopSensorBtn" onclick="stopSensorDecoding()" style="display: none;">
|
|
||||||
Stop Listening
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,78 +1,60 @@
|
|||||||
<!-- TSCM MODE (Counter-Surveillance) -->
|
<!-- TSCM MODE (Counter-Surveillance) -->
|
||||||
<div id="tscmMode" class="mode-content">
|
<div id="tscmMode" class="mode-content">
|
||||||
<!-- Configuration -->
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">TSCM Sweep <span style="font-size: 9px; font-weight: normal; background: var(--accent-orange); color: #000; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px;">Alpha</span></h3>
|
<h3 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">TSCM Sweep <span style="font-size: 9px; font-weight: normal; background: var(--accent-orange); color: #000; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px;">Alpha</span></h3>
|
||||||
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
|
Technical surveillance countermeasures to detect wireless surveillance devices, hidden cameras, and GPS trackers.
|
||||||
|
</p>
|
||||||
|
<div style="background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; padding: 8px; font-size: 10px;">
|
||||||
|
<span style="color: var(--accent-cyan);">Controls in function bar above</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Configuration -->
|
||||||
<label>Sweep Type</label>
|
<div class="section">
|
||||||
<select id="tscmSweepType">
|
<h3>Scan Sources</h3>
|
||||||
<option value="quick">Quick Scan (2 min)</option>
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
<option value="standard" selected>Standard (5 min)</option>
|
<label class="inline-checkbox">
|
||||||
<option value="full">Full Sweep (15 min)</option>
|
<input type="checkbox" id="tscmWifiEnabled" checked>
|
||||||
<option value="wireless_cameras">Wireless Cameras</option>
|
WiFi
|
||||||
<option value="body_worn">Body-Worn Devices</option>
|
</label>
|
||||||
<option value="gps_trackers">GPS Trackers</option>
|
<select id="tscmWifiInterface" style="margin-top: 4px;">
|
||||||
|
<option value="">Select WiFi interface...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
<div class="form-group">
|
<label class="inline-checkbox">
|
||||||
<label>Compare Against</label>
|
<input type="checkbox" id="tscmBtEnabled" checked>
|
||||||
<select id="tscmBaselineSelect">
|
Bluetooth
|
||||||
<option value="">No Baseline</option>
|
</label>
|
||||||
|
<select id="tscmBtInterface" style="margin-top: 4px;">
|
||||||
|
<option value="">Select Bluetooth adapter...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
|
<label class="inline-checkbox">
|
||||||
|
<input type="checkbox" id="tscmRfEnabled" checked>
|
||||||
|
RF/SDR
|
||||||
|
</label>
|
||||||
|
<select id="tscmSdrDevice" style="margin-top: 4px;">
|
||||||
|
<option value="">Select SDR device...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="preset-btn" onclick="refreshTscmDevices()" style="width: 100%; margin-top: 8px; font-size: 10px;">
|
||||||
|
<span class="icon icon-refresh icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></span>Refresh Devices
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Options</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="inline-checkbox">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" id="tscmVerboseResults">
|
<input type="checkbox" id="tscmVerboseResults">
|
||||||
Verbose results (store full device details)
|
Verbose results (store full device details)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
|
||||||
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary);">Scan Sources</label>
|
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
|
||||||
<label class="inline-checkbox">
|
|
||||||
<input type="checkbox" id="tscmWifiEnabled" checked>
|
|
||||||
WiFi
|
|
||||||
</label>
|
|
||||||
<select id="tscmWifiInterface" style="margin-top: 4px;">
|
|
||||||
<option value="">Select WiFi interface...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
|
||||||
<label class="inline-checkbox">
|
|
||||||
<input type="checkbox" id="tscmBtEnabled" checked>
|
|
||||||
Bluetooth
|
|
||||||
</label>
|
|
||||||
<select id="tscmBtInterface" style="margin-top: 4px;">
|
|
||||||
<option value="">Select Bluetooth adapter...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
|
||||||
<label class="inline-checkbox">
|
|
||||||
<input type="checkbox" id="tscmRfEnabled" checked>
|
|
||||||
RF/SDR
|
|
||||||
</label>
|
|
||||||
<select id="tscmSdrDevice" style="margin-top: 4px;">
|
|
||||||
<option value="">Select SDR device...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="preset-btn" onclick="refreshTscmDevices()" style="width: 100%; margin-top: 8px; font-size: 10px;">
|
|
||||||
<span class="icon icon-refresh icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></span>Refresh Devices
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<button class="run-btn" id="startTscmBtn" onclick="startTscmSweep()" style="margin-top: 12px;">
|
|
||||||
Start Sweep
|
|
||||||
</button>
|
|
||||||
<button class="stop-btn" id="stopTscmBtn" onclick="stopTscmSweep()" style="display: none; margin-top: 12px;">
|
|
||||||
Stop Sweep
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Futuristic Scanner Progress -->
|
<!-- Futuristic Scanner Progress -->
|
||||||
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none; margin-top: 12px;">
|
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none; margin-top: 12px;">
|
||||||
<div class="scanner-ring">
|
<div class="scanner-ring">
|
||||||
@@ -115,46 +97,42 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Advanced -->
|
<!-- Advanced -->
|
||||||
<div class="section" style="margin-top: 12px;">
|
<div class="section">
|
||||||
<h3 style="margin-bottom: 12px;">Advanced</h3>
|
<h3>Baseline Recording</h3>
|
||||||
|
<div class="form-group">
|
||||||
<div style="margin-bottom: 16px;">
|
<input type="text" id="tscmBaselineName" placeholder="Baseline name...">
|
||||||
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary);">Baseline Recording</label>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="tscmBaselineName" placeholder="Baseline name...">
|
|
||||||
</div>
|
|
||||||
<button class="run-btn" id="tscmRecordBaselineBtn" onclick="tscmRecordBaseline()" style="width: 100%; padding: 8px;">
|
|
||||||
Record New Baseline
|
|
||||||
</button>
|
|
||||||
<button class="stop-btn" id="tscmStopBaselineBtn" onclick="tscmStopBaseline()" style="width: 100%; padding: 8px; display: none;">
|
|
||||||
Stop Recording
|
|
||||||
</button>
|
|
||||||
<div id="tscmBaselineStatus" style="margin-top: 8px; font-size: 11px; color: var(--text-muted);"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="run-btn" id="tscmRecordBaselineBtn" onclick="tscmRecordBaseline()" style="width: 100%; padding: 8px;">
|
||||||
|
Record New Baseline
|
||||||
|
</button>
|
||||||
|
<button class="stop-btn" id="tscmStopBaselineBtn" onclick="tscmStopBaseline()" style="width: 100%; padding: 8px; display: none;">
|
||||||
|
Stop Recording
|
||||||
|
</button>
|
||||||
|
<div id="tscmBaselineStatus" style="margin-top: 8px; font-size: 11px; color: var(--text-muted);"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px;">
|
<div class="section">
|
||||||
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary);">Meeting Window</label>
|
<h3>Meeting Window</h3>
|
||||||
<div id="tscmMeetingStatus" style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
|
<div id="tscmMeetingStatus" style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
|
||||||
No active meeting
|
No active meeting
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="tscmMeetingName" placeholder="Meeting name (optional)">
|
<input type="text" id="tscmMeetingName" placeholder="Meeting name (optional)">
|
||||||
</div>
|
</div>
|
||||||
<button class="run-btn" id="tscmStartMeetingBtn" onclick="tscmStartMeeting()" style="width: 100%; padding: 8px;">
|
<button class="run-btn" id="tscmStartMeetingBtn" onclick="tscmStartMeeting()" style="width: 100%; padding: 8px;">
|
||||||
Start Meeting Window
|
Start Meeting Window
|
||||||
</button>
|
</button>
|
||||||
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
|
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
|
||||||
End Meeting Window
|
End Meeting Window
|
||||||
</button>
|
</button>
|
||||||
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
|
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
|
||||||
Devices detected during meetings get flagged
|
Devices detected during meetings get flagged
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tools -->
|
<!-- Tools -->
|
||||||
<div class="section" style="margin-top: 12px;">
|
<div class="section">
|
||||||
<h3 style="margin-bottom: 10px;">Tools</h3>
|
<h3>Tools</h3>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px;">
|
||||||
<button class="preset-btn" onclick="tscmShowCapabilities()" style="font-size: 10px; padding: 8px;">
|
<button class="preset-btn" onclick="tscmShowCapabilities()" style="font-size: 10px; padding: 8px;">
|
||||||
Capabilities
|
Capabilities
|
||||||
@@ -173,4 +151,17 @@
|
|||||||
|
|
||||||
<!-- Device Warnings -->
|
<!-- Device Warnings -->
|
||||||
<div id="tscmDeviceWarnings" style="display: none; margin-top: 8px; padding: 8px; background: rgba(255,153,51,0.1); border: 1px solid rgba(255,153,51,0.3); border-radius: 4px;"></div>
|
<div id="tscmDeviceWarnings" style="display: none; margin-top: 8px; padding: 8px; background: rgba(255,153,51,0.1); border: 1px solid rgba(255,153,51,0.3); border-radius: 4px;"></div>
|
||||||
|
|
||||||
|
<!-- Hidden inputs for action bar sync -->
|
||||||
|
<select id="tscmSweepType" style="display: none;">
|
||||||
|
<option value="quick">Quick Scan (2 min)</option>
|
||||||
|
<option value="standard" selected>Standard (5 min)</option>
|
||||||
|
<option value="full">Full Sweep (15 min)</option>
|
||||||
|
<option value="wireless_cameras">Wireless Cameras</option>
|
||||||
|
<option value="body_worn">Body-Worn Devices</option>
|
||||||
|
<option value="gps_trackers">GPS Trackers</option>
|
||||||
|
</select>
|
||||||
|
<select id="tscmBaselineSelect" style="display: none;">
|
||||||
|
<option value="">No Baseline</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
<!-- WiFi MODE -->
|
<!-- WiFi MODE -->
|
||||||
<div id="wifiMode" class="mode-content">
|
<div id="wifiMode" class="mode-content">
|
||||||
<!-- Scan Mode Tabs -->
|
|
||||||
<div class="section" style="padding: 8px;">
|
|
||||||
<div class="wifi-scan-mode-tabs" style="display: flex; gap: 4px;">
|
|
||||||
<button id="wifiScanModeQuick" class="wifi-mode-tab active" style="flex: 1; padding: 8px; font-size: 11px; background: var(--accent-green); color: #000; border: none; border-radius: 4px; cursor: pointer;">
|
|
||||||
Quick Scan
|
|
||||||
</button>
|
|
||||||
<button id="wifiScanModeDeep" class="wifi-mode-tab" style="flex: 1; padding: 8px; font-size: 11px; background: var(--bg-tertiary); color: #888; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
|
|
||||||
Deep Scan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="wifiCapabilityStatus" class="info-text" style="margin-top: 8px; font-size: 10px;"></div>
|
|
||||||
<!-- Show All Agents option (visible when agents are available) -->
|
|
||||||
<div id="wifiShowAllAgentsContainer" style="margin-top: 8px; display: none;">
|
|
||||||
<label class="inline-checkbox" style="font-size: 10px;">
|
|
||||||
<input type="checkbox" id="wifiShowAllAgents" onchange="if(typeof WiFiMode !== 'undefined') WiFiMode.toggleShowAllAgents(this.checked)">
|
|
||||||
Show networks from all agents
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>WiFi Adapter</h3>
|
<h3>WiFi Scanning</h3>
|
||||||
<div class="form-group">
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
<label>Select Device</label>
|
Scan for nearby WiFi networks, analyze channels, and monitor wireless activity.
|
||||||
<select id="wifiInterfaceSelect" style="font-size: 12px;">
|
</p>
|
||||||
<option value="">Detecting interfaces...</option>
|
<!-- Scan Mode Tabs -->
|
||||||
</select>
|
<div style="margin-top: 12px;">
|
||||||
</div>
|
<div class="wifi-scan-mode-tabs" style="display: flex; gap: 4px; margin-bottom: 10px;">
|
||||||
<button class="preset-btn" onclick="refreshWifiInterfaces()" style="width: 100%;">
|
<button id="wifiScanModeQuick" class="wifi-mode-tab active">
|
||||||
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></span> Refresh Devices
|
Quick Scan
|
||||||
</button>
|
</button>
|
||||||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="wifiToolStatus">
|
<button id="wifiScanModeDeep" class="wifi-mode-tab">
|
||||||
<span>airmon-ng:</span><span class="tool-status missing">Checking...</span>
|
Deep Scan
|
||||||
<span>airodump-ng:</span><span class="tool-status missing">Checking...</span>
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Scan Control Buttons -->
|
||||||
|
<div id="wifiScanControls" style="display: flex; gap: 8px;">
|
||||||
|
<button id="wifiQuickScanBtn" class="preset-btn wifi-start-btn" onclick="if(typeof WiFiMode !== 'undefined') WiFiMode.startQuickScan()" style="flex: 1;">
|
||||||
|
Start Quick Scan
|
||||||
|
</button>
|
||||||
|
<button id="wifiDeepScanBtn" class="preset-btn wifi-start-btn" onclick="if(typeof WiFiMode !== 'undefined') WiFiMode.startDeepScan()" style="flex: 1; display: none;">
|
||||||
|
Start Deep Scan
|
||||||
|
</button>
|
||||||
|
<button id="wifiStopScanBtn" class="preset-btn wifi-stop-btn" onclick="if(typeof WiFiMode !== 'undefined') WiFiMode.stopScan()" style="flex: 1; display: none;">
|
||||||
|
Stop Scan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="wifiCapabilityStatus" class="info-text" style="margin-top: 8px; font-size: 10px;"></div>
|
||||||
|
<!-- Show All Agents option (visible when agents are available) -->
|
||||||
|
<div id="wifiShowAllAgentsContainer" style="margin-top: 8px; display: none;">
|
||||||
|
<label class="inline-checkbox" style="font-size: 10px;">
|
||||||
|
<input type="checkbox" id="wifiShowAllAgents" onchange="if(typeof WiFiMode !== 'undefined') WiFiMode.toggleShowAllAgents(this.checked)">
|
||||||
|
Show networks from all agents
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Monitor Mode</h3>
|
<h3>Monitor Mode</h3>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button class="preset-btn" id="monitorStartBtn" onclick="enableMonitorMode()" style="flex: 1; background: var(--accent-green); color: #000;">
|
<button class="preset-btn wifi-monitor-btn" id="monitorStartBtn" onclick="WiFiHelpers.enableMonitorMode()" style="flex: 1;">
|
||||||
Enable Monitor
|
Enable Monitor
|
||||||
</button>
|
</button>
|
||||||
<button class="preset-btn" id="monitorStopBtn" onclick="disableMonitorMode()" style="flex: 1; display: none;">
|
<button class="preset-btn wifi-monitor-stop-btn" id="monitorStopBtn" onclick="WiFiHelpers.disableMonitorMode()" style="flex: 1; display: none;">
|
||||||
Disable Monitor
|
Disable Monitor
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,34 +61,12 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Scan Settings</h3>
|
<h3>Scan Settings</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label>Band</label>
|
|
||||||
<select id="wifiBand">
|
|
||||||
<option value="abg">All (2.4 + 5 GHz)</option>
|
|
||||||
<option value="bg">2.4 GHz only</option>
|
|
||||||
<option value="a">5 GHz only</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Channel (empty = hop)</label>
|
<label>Channel (empty = hop)</label>
|
||||||
<input type="text" id="wifiChannel" placeholder="e.g., 6 or 36">
|
<input type="text" id="wifiChannel" placeholder="e.g., 6 or 36">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h3>Proximity Alerts</h3>
|
|
||||||
<div class="info-text" style="margin-bottom: 8px;">
|
|
||||||
Alert when specific MAC addresses appear
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="watchMacInput" placeholder="AA:BB:CC:DD:EE:FF">
|
|
||||||
</div>
|
|
||||||
<button class="preset-btn" onclick="addWatchMac()" style="width: 100%; margin-bottom: 8px;">
|
|
||||||
Add to Watch List
|
|
||||||
</button>
|
|
||||||
<div id="watchList" style="max-height: 80px; overflow-y: auto; font-size: 10px; color: var(--text-dim);"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Attack Options</h3>
|
<h3>Attack Options</h3>
|
||||||
<div class="info-text" style="color: var(--accent-red); margin-bottom: 10px;">
|
<div class="info-text" style="color: var(--accent-red); margin-bottom: 10px;">
|
||||||
@@ -105,11 +84,25 @@
|
|||||||
<label>Deauth Count</label>
|
<label>Deauth Count</label>
|
||||||
<input type="text" id="deauthCount" value="5" placeholder="5">
|
<input type="text" id="deauthCount" value="5" placeholder="5">
|
||||||
</div>
|
</div>
|
||||||
<button class="preset-btn" onclick="sendDeauth()" style="width: 100%; border-color: var(--accent-red); color: var(--accent-red);">
|
<button class="preset-btn wifi-danger-btn" onclick="WiFiHelpers.sendDeauth()" style="width: 100%;">
|
||||||
Send Deauth
|
Send Deauth
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Proximity Alerts</h3>
|
||||||
|
<div class="info-text" style="margin-bottom: 8px;">
|
||||||
|
Alert when specific MAC addresses appear
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="watchMacInput" placeholder="AA:BB:CC:DD:EE:FF">
|
||||||
|
</div>
|
||||||
|
<button class="preset-btn" onclick="WiFiHelpers.addWatchMac()" style="width: 100%; margin-bottom: 8px;">
|
||||||
|
Add to Watch List
|
||||||
|
</button>
|
||||||
|
<div id="watchList" style="max-height: 80px; overflow-y: auto; font-size: 10px; color: var(--text-dim);"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Handshake Capture Status Panel -->
|
<!-- Handshake Capture Status Panel -->
|
||||||
<div class="section" id="captureStatusPanel" style="display: none; border: 1px solid var(--accent-orange); border-radius: 4px; padding: 10px; background: rgba(255, 165, 0, 0.1);">
|
<div class="section" id="captureStatusPanel" style="display: none; border: 1px solid var(--accent-orange); border-radius: 4px; padding: 10px; background: rgba(255, 165, 0, 0.1);">
|
||||||
<h3 style="color: var(--accent-orange); margin: 0 0 8px 0;"><span class="icon icon--sm"><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="6"/><circle cx="12" cy="12" r="2"/></svg></span> Handshake Capture</h3>
|
<h3 style="color: var(--accent-orange); margin: 0 0 8px 0;"><span class="icon icon--sm"><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="6"/><circle cx="12" cy="12" r="2"/></svg></span> Handshake Capture</h3>
|
||||||
@@ -131,10 +124,10 @@
|
|||||||
<span id="captureStatus" style="font-weight: bold;">--</span>
|
<span id="captureStatus" style="font-weight: bold;">--</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button class="preset-btn" onclick="checkCaptureStatus()" style="flex: 1; font-size: 10px; padding: 4px;">
|
<button class="preset-btn" onclick="WiFiHelpers.checkCaptureStatus()" style="flex: 1; font-size: 10px; padding: 4px;">
|
||||||
Check Status
|
Check Status
|
||||||
</button>
|
</button>
|
||||||
<button class="preset-btn" onclick="stopHandshakeCapture()" style="flex: 1; font-size: 10px; padding: 4px; background: var(--accent-red); border: none; color: #fff;">
|
<button class="preset-btn wifi-stop-btn" onclick="WiFiHelpers.stopHandshakeCapture()" style="flex: 1; font-size: 10px; padding: 4px;">
|
||||||
Stop Capture
|
Stop Capture
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,32 +146,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- v2 Scan Buttons -->
|
<div class="section" id="wifiExportSection">
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button class="run-btn" id="wifiQuickScanBtn" onclick="WiFiMode.startQuickScan()" style="flex: 1;">
|
|
||||||
Quick Scan
|
|
||||||
</button>
|
|
||||||
<button class="run-btn" id="wifiDeepScanBtn" onclick="WiFiMode.startDeepScan()" style="flex: 1; background: var(--accent-orange);">
|
|
||||||
Deep Scan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button class="stop-btn" id="wifiStopScanBtn" onclick="WiFiMode.stopScan()" style="display: none; width: 100%;">
|
|
||||||
Stop Scanning
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Legacy Scan Buttons (hidden, for backwards compatibility) -->
|
|
||||||
<button class="run-btn" id="startWifiBtn" onclick="startWifiScan()" style="display: none;">
|
|
||||||
Start Scanning (Legacy)
|
|
||||||
</button>
|
|
||||||
<button class="stop-btn" id="stopWifiBtn" onclick="stopWifiScan()" style="display: none;">
|
|
||||||
Stop Scanning (Legacy)
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Export Section -->
|
|
||||||
<div class="section" style="margin-top: 10px;">
|
|
||||||
<h3>Export</h3>
|
<h3>Export</h3>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<button class="preset-btn" onclick="WiFiMode.exportData('csv')" style="flex: 1;">
|
<button class="preset-btn" id="wifiExportCsvBtn" onclick="WiFiMode.exportData('csv')" style="flex: 1;">
|
||||||
Export CSV
|
Export CSV
|
||||||
</button>
|
</button>
|
||||||
<button class="preset-btn" onclick="WiFiMode.exportData('json')" style="flex: 1;">
|
<button class="preset-btn" onclick="WiFiMode.exportData('json')" style="flex: 1;">
|
||||||
@@ -186,4 +157,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden inputs for action bar sync -->
|
||||||
|
<select id="wifiInterfaceSelect" style="display: none;">
|
||||||
|
<option value="">Detecting interfaces...</option>
|
||||||
|
</select>
|
||||||
|
<select id="wifiBand" style="display: none;">
|
||||||
|
<option value="abg">All (2.4 + 5 GHz)</option>
|
||||||
|
<option value="bg">2.4 GHz only</option>
|
||||||
|
<option value="a">5 GHz only</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
{#
|
||||||
|
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) %}
|
||||||
|
|
||||||
|
{# 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" onclick="toggleNavDropdown('sdr')">
|
||||||
|
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>
|
||||||
|
<span class="nav-label">SDR / RF</span>
|
||||||
|
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||||
|
</button>
|
||||||
|
<div class="mode-nav-dropdown-menu">
|
||||||
|
<button class="mode-nav-btn {% if active_mode == 'pager' %}active{% endif %}" 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 {% if active_mode == 'sensor' %}active{% endif %}" 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.49"/></svg></span>
|
||||||
|
<span class="nav-label">433MHz</span>
|
||||||
|
</button>
|
||||||
|
<button class="mode-nav-btn {% if active_mode == 'rtlamr' %}active{% endif %}" 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 {% if active_mode == 'adsb' %}active{% endif %}" 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 {% if active_mode == 'ais' %}active{% endif %}" 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 {% if active_mode == 'aprs' %}active{% endif %}" 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 {% if active_mode == 'listening' %}active{% endif %}" 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 {% if active_mode == 'spystations' %}active{% endif %}" 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 {% if active_mode == 'meshtastic' %}active{% endif %}" 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>
|
||||||
|
|
||||||
|
{# Wireless Group #}
|
||||||
|
<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 {% if active_mode == 'wifi' %}active{% endif %}" 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 {% if active_mode == 'bluetooth' %}active{% endif %}" 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>
|
||||||
|
|
||||||
|
{# Security Group #}
|
||||||
|
<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 {% if active_mode == 'tscm' %}active{% endif %}" 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>
|
||||||
|
|
||||||
|
{# Space Group #}
|
||||||
|
<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">
|
||||||
|
{% if is_index_page %}
|
||||||
|
<button class="mode-nav-btn {% if active_mode == 'satellite' %}active{% endif %}" 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>
|
||||||
|
{% else %}
|
||||||
|
<a href="/satellite/dashboard" class="mode-nav-btn {% if active_mode == 'satellite' %}active{% endif %}" 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="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>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<button class="mode-nav-btn {% if active_mode == 'sstv' %}active{% endif %}" 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>
|
||||||
|
|
||||||
|
{# 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">
|
||||||
|
<button class="mobile-nav-btn {% if active_mode == 'pager' %}active{% endif %}" 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 {% if active_mode == 'sensor' %}active{% endif %}" 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 {% if active_mode == 'rtlamr' %}active{% endif %}" 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 {% if active_mode == 'adsb' %}active{% endif %}" 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 {% if active_mode == 'ais' %}active{% endif %}" 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 {% if active_mode == 'aprs' %}active{% endif %}" 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 {% if active_mode == 'wifi' %}active{% endif %}" 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 {% if active_mode == 'bluetooth' %}active{% endif %}" 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 {% if active_mode == 'tscm' %}active{% endif %}" 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>
|
||||||
|
{% if is_index_page %}
|
||||||
|
<button class="mobile-nav-btn {% if active_mode == 'satellite' %}active{% endif %}" 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>
|
||||||
|
{% else %}
|
||||||
|
<a href="/satellite/dashboard" class="mobile-nav-btn {% if active_mode == 'satellite' %}active{% endif %}" style="text-decoration: none;">
|
||||||
|
<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
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<button class="mobile-nav-btn {% if active_mode == 'sstv' %}active{% endif %}" 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 {% if active_mode == 'listening' %}active{% endif %}" 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 {% if active_mode == 'spystations' %}active{% endif %}" 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 {% if active_mode == 'meshtastic' %}active{% endif %}" 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>
|
||||||
|
|
||||||
|
{# 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,9 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<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>SATELLITE COMMAND // iNTERCEPT - See the Invisible</title>
|
<title>SATELLITE COMMAND // iNTERCEPT</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
<!-- Design tokens and shared styles -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||||||
<!-- Fonts - Conditional CDN/Local loading -->
|
<!-- Fonts - Conditional CDN/Local loading -->
|
||||||
{% 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') }}">
|
||||||
@@ -27,8 +31,10 @@
|
|||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
SATELLITE COMMAND
|
<a href="/" style="color: inherit; text-decoration: none;">
|
||||||
<span>// iNTERCEPT - See the Invisible</span>
|
SATELLITE COMMAND
|
||||||
|
<span>// iNTERCEPT</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-badges">
|
<div class="stats-badges">
|
||||||
<div class="stat-badge">
|
<div class="stat-badge">
|
||||||
@@ -62,10 +68,13 @@
|
|||||||
<span id="trackingStatus">TRACKING</span>
|
<span id="trackingStatus">TRACKING</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="datetime" id="utcTime">--:--:-- UTC</div>
|
<div class="datetime" id="utcTime">--:--:-- UTC</div>
|
||||||
<a href="/?mode=satellite" class="back-link">Main Dashboard</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Unified Navigation -->
|
||||||
|
{% set active_mode = 'satellite' %}
|
||||||
|
{% include 'partials/nav.html' %}
|
||||||
|
|
||||||
<main class="dashboard">
|
<main class="dashboard">
|
||||||
<!-- Polar Plot -->
|
<!-- Polar Plot -->
|
||||||
<div class="panel polar-container">
|
<div class="panel polar-container">
|
||||||
@@ -294,10 +303,6 @@
|
|||||||
|
|
||||||
function setupEmbeddedMode() {
|
function setupEmbeddedMode() {
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
// Hide back link when embedded
|
|
||||||
const backLink = document.querySelector('.back-link');
|
|
||||||
if (backLink) backLink.style.display = 'none';
|
|
||||||
|
|
||||||
// Add embedded class to body for CSS adjustments
|
// Add embedded class to body for CSS adjustments
|
||||||
document.body.classList.add('embedded');
|
document.body.classList.add('embedded');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user