Compare commits

...

17 Commits

Author SHA1 Message Date
Smittix d0e8eaf397 fix: Add LISTEN button to listening post function strip and clarify controls
- Add dedicated LISTEN button (🎧) to function strip for direct audio playback
- Rename START to SCAN (📡) to clarify it scans for signals, not direct audio
- Update function strip status to show LISTENING (green) vs SCANNING (cyan)
- Update updateListeningStripRunning() to accept mode parameter
- Add listenFromStrip() function for direct audio from function strip
- Update stopListening() to handle both scanner and audio states
- Move .listening status dot to green color group (matches audio state)

This clarifies the confusion where users expected the START button to play audio,
but it actually started the frequency scanner which only plays audio when signals
are detected.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 21:20:17 +00:00
Smittix 4bf5bd2d37 fix: Initialize default mode and override nav.html fallback functions
- Call switchMode(currentMode) on page load to properly initialize the
  default pager mode and show the function strip
- Explicitly set window.switchMode and window.toggleNavDropdown after
  function definitions to override fallbacks defined in nav.html partial
- This fixes an issue where the nav.html fallback functions (which
  redirect to /?mode=X) would persist due to script execution order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:41:13 +00:00
Smittix 206e63944e fix: Reduce Open Notify API failure log level to debug
The Open Notify API is unreliable and has a fallback. Changed from
warning to debug to match satellite.py and reduce log noise.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:50:05 +00:00
Smittix 978e6cdaea fix: Resolve TSCM function strip visibility and clipping issues
- Fix function strip content being clipped by changing overflow to visible
- Add min-height and increased padding to function strip
- Add explicit colors for TSCM strip stat values and labels
- Fix output-panel overflow for TSCM mode using :has() selector
- Add CSS variables --bg-dark and --bg-panel aliases
- Clean up sidebar section margins for consistent spacing
- Add unique IDs to WiFi/Bluetooth export sections for restore function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:40:10 +00:00
Smittix 68d831dbe3 feat: Add collapsible sidebar with mode-specific section ordering
- Sidebar sections now rearrange based on active mode
- Mode info section (e.g., WiFi Scanning, Spectrum Analyzer) appears at top expanded
- Signal Source and SDR Device sections collapse and move below mode sections
- Other mode sections default to collapsed state
- Sections restore to original containers when switching modes
- Fix SCAN/LISTEN button styling consistency in Listening Post
- Reorder WiFi sections: WiFi Scanning, Signal Source, Monitor Mode, Scan Settings, Attack Options, Proximity Alerts, Export

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 13:25:54 +00:00
Smittix 623a0da056 fix: Add z-index to navbar to sit above radar grid overlay
Dashboard pages have a .radar-bg element with a grid pattern that
was showing through the navbar. Adding position: relative and z-index: 10
ensures the nav appears above the background effects.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:42:04 +00:00
Smittix d6f5127cd6 fix: Add direct link to layout.css in dashboard templates
CSS @import from within stylesheets may not load reliably.
Adding direct <link> tags ensures layout.css (with nav styles)
is loaded on all dashboard pages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:31:23 +00:00
Smittix 166963f2a1 fix: Force navbar background color with !important
Using !important to ensure the navbar background color (#151a23)
cannot be overridden by any cascade or specificity issues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:29:16 +00:00
Smittix eeca15c83e fix: Ensure consistent navbar background across all pages
Added explicit fallback color (#151a23) for .mode-nav background to
ensure consistency between main index and dashboard pages. CSS
variables may resolve differently based on cascade order.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:24:34 +00:00
Smittix ed1dfbb9b5 feat: Add UX polish - transitions, hover effects, accessibility
- Add smooth transitions for all interactive elements
- Button hover lift and active press feedback
- Card/panel hover effects with shadow
- Status dot glow animation
- Alert slide-in animation
- Enhanced input focus glow
- Better focus-visible states for keyboard navigation
- Reduced motion support (prefers-reduced-motion)
- High contrast mode support (prefers-contrast)
- Skip link for accessibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:07:02 +00:00
Smittix 29af551828 refactor: Import core components CSS in dashboards
- Add components.css import to ADSB, AIS, Satellite dashboards
- Provides base styles for panels, stats strips, status dots, etc.
- Dashboard-specific styles cascade over base styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:06:16 +00:00
Smittix e1e05523d2 refactor: Migrate index.html to use unified nav partial
- Replace 100+ lines of duplicated nav markup with partial include
- Add is_index_page variable to nav partial for conditional behavior
- Satellite/SSTV use switchMode on index, link to dashboard elsewhere
- Reduces code duplication, single source of truth for navigation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:04:48 +00:00
Smittix 94fcea0e99 fix: Hide welcome overlay when mode URL param is present
- Skip welcome screen when navigating with ?mode=X parameter
- Fixes nav from dashboards to panel modes (e.g., APRS, WiFi)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:52:49 +00:00
Smittix b0c323bb89 fix: Handle mode URL parameter on index page load
- Read ?mode=X parameter from URL when index.html loads
- Automatically switch to the specified mode (e.g., /?mode=aprs)
- Fixes navigation from dashboard pages to panel modes like APRS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:50:10 +00:00
Smittix 425572ac87 fix: Make Satellite nav link go to dashboard like ADSB/AIS
- Changed Satellite from switchMode button to dashboard link
- Consistent behavior: all dashboard modes (ADSB, AIS, Satellite) now
  link to their respective dashboards in the nav partial
- Only affects dashboard pages; index.html keeps its own navigation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:45:46 +00:00
Smittix db931c3806 fix: Add navigation styles to dashboards
- Add mode navigation CSS to core/layout.css so it's available on all pages
- Import layout.css in dashboard stylesheets (ADSB, AIS, Satellite)
- Fixes nav partial appearing unstyled on dashboard pages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:31:45 +00:00
Smittix a58e2f0d21 feat: UI refactor - unified navigation, design tokens, dashboard integration
- Add centralized design tokens in static/css/core/variables.css
- Create reusable base templates (base.html, base_dashboard.html)
- Add unified navigation partial (partials/nav.html) with all modules
- Create reusable UI components (card, loading, empty_state, status_badge, stats_strip)
- Integrate navigation into ADSB, AIS, and Satellite dashboards
- Consolidate CSS by importing shared variables in dashboard stylesheets
- Add comprehensive UI_GUIDE.md documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:27:39 +00:00
36 changed files with 6335 additions and 616 deletions
+608
View File
@@ -0,0 +1,608 @@
# iNTERCEPT UI Guide
This guide documents the UI design system, components, and patterns used in iNTERCEPT.
## Table of Contents
1. [Design Tokens](#design-tokens)
2. [Base Templates](#base-templates)
3. [Navigation](#navigation)
4. [Components](#components)
5. [Adding a New Module Page](#adding-a-new-module-page)
6. [Adding a New Dashboard](#adding-a-new-dashboard)
---
## Design Tokens
All design tokens are defined in `static/css/core/variables.css`. Import this file first in any stylesheet.
### Colors
```css
/* Backgrounds (layered depth) */
--bg-primary: #0a0c10; /* Darkest - page background */
--bg-secondary: #0f1218; /* Panels, sidebars */
--bg-tertiary: #151a23; /* Cards, elevated elements */
--bg-card: #121620; /* Card backgrounds */
--bg-elevated: #1a202c; /* Hover states, modals */
/* Accent Colors */
--accent-cyan: #4a9eff; /* Primary action color */
--accent-green: #22c55e; /* Success, online status */
--accent-red: #ef4444; /* Error, danger, stop */
--accent-orange: #f59e0b; /* Warning */
--accent-amber: #d4a853; /* Secondary highlight */
/* Text Hierarchy */
--text-primary: #e8eaed; /* Main content */
--text-secondary: #9ca3af; /* Secondary content */
--text-dim: #4b5563; /* Disabled, placeholder */
--text-muted: #374151; /* Barely visible */
/* Status Colors */
--status-online: #22c55e;
--status-warning: #f59e0b;
--status-error: #ef4444;
--status-offline: #6b7280;
```
### Spacing Scale
```css
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
```
### Typography
```css
/* Font Families */
--font-sans: 'Inter', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Font Sizes */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 20px;
--text-3xl: 24px;
--text-4xl: 30px;
```
### Border Radius
```css
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
```
### Light Theme
The design system supports light/dark themes via `data-theme` attribute:
```html
<html data-theme="dark"> <!-- or "light" -->
```
Toggle with JavaScript:
```javascript
document.documentElement.setAttribute('data-theme', 'light');
```
---
## Base Templates
### `templates/layout/base.html`
The main base template for standard pages. Use for pages with sidebar + content layout.
```html
{% extends 'layout/base.html' %}
{% block title %}My Page Title{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my-page.css') }}">
{% endblock %}
{% block navigation %}
{% set active_mode = 'mymode' %}
{% include 'partials/nav.html' %}
{% endblock %}
{% block sidebar %}
<div class="app-sidebar">
<!-- Sidebar content -->
</div>
{% endblock %}
{% block content %}
<div class="page-container">
<h1>Page Title</h1>
<!-- Page content -->
</div>
{% endblock %}
{% block scripts %}
<script>
// Page-specific JavaScript
</script>
{% endblock %}
```
### `templates/layout/base_dashboard.html`
Extended base for full-screen dashboards (maps, visualizations).
```html
{% extends 'layout/base_dashboard.html' %}
{% set active_mode = 'mydashboard' %}
{% block dashboard_title %}MY DASHBOARD{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_dashboard.css') }}">
{% endblock %}
{% block stats_strip %}
<div class="stats-strip">
<!-- Stats bar content -->
</div>
{% endblock %}
{% block dashboard_content %}
<div class="dashboard-map-container">
<!-- Main visualization -->
</div>
<div class="dashboard-sidebar">
<!-- Sidebar panels -->
</div>
{% endblock %}
```
---
## Navigation
### Including Navigation
```html
{% set active_mode = 'pager' %}
{% include 'partials/nav.html' %}
```
### Valid `active_mode` Values
| Mode | Description |
|------|-------------|
| `pager` | Pager decoding |
| `sensor` | 433MHz sensors |
| `rtlamr` | Utility meters |
| `adsb` | Aircraft tracking |
| `ais` | Vessel tracking |
| `aprs` | Amateur radio |
| `wifi` | WiFi scanning |
| `bluetooth` | Bluetooth scanning |
| `tscm` | Counter-surveillance |
| `satellite` | Satellite tracking |
| `sstv` | ISS SSTV |
| `listening` | Listening post |
| `spystations` | Spy stations |
| `meshtastic` | Mesh networking |
### Navigation Groups
The navigation is organized into groups:
- **SDR / RF**: Pager, 433MHz, Meters, Aircraft, Vessels, APRS, Listening Post, Spy Stations, Meshtastic
- **Wireless**: WiFi, Bluetooth
- **Security**: TSCM
- **Space**: Satellite, ISS SSTV
---
## Components
### Card / Panel
```html
{% call card(title='PANEL TITLE', indicator=true, indicator_active=false) %}
<p>Panel content here</p>
{% endcall %}
```
Or manually:
```html
<div class="panel">
<div class="panel-header">
<span>PANEL TITLE</span>
<div class="panel-indicator active"></div>
</div>
<div class="panel-content">
<p>Content here</p>
</div>
</div>
```
### Empty State
```html
{% include 'components/empty_state.html' with context %}
{# Or with variables: #}
{% with title='No data yet', description='Start scanning to see results', action_text='Start Scan', action_onclick='startScan()' %}
{% include 'components/empty_state.html' %}
{% endwith %}
```
### Loading State
```html
{# Inline spinner #}
{% include 'components/loading.html' %}
{# With text #}
{% with text='Loading data...', size='lg' %}
{% include 'components/loading.html' %}
{% endwith %}
{# Full overlay #}
{% with overlay=true, text='Please wait...' %}
{% include 'components/loading.html' %}
{% endwith %}
```
### Status Badge
```html
{% with status='online', text='Connected', id='connectionStatus' %}
{% include 'components/status_badge.html' %}
{% endwith %}
```
Status values: `online`, `offline`, `warning`, `error`, `inactive`
### Buttons
```html
<!-- Primary action -->
<button class="btn btn-primary">Start Tracking</button>
<!-- Secondary action -->
<button class="btn btn-secondary">Cancel</button>
<!-- Danger action -->
<button class="btn btn-danger">Stop</button>
<!-- Ghost/subtle -->
<button class="btn btn-ghost">Settings</button>
<!-- Sizes -->
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-lg">Large</button>
<!-- Icon button -->
<button class="btn btn-icon btn-secondary">
<span class="icon">...</span>
</button>
```
### Badges
```html
<span class="badge">Default</span>
<span class="badge badge-primary">Primary</span>
<span class="badge badge-success">Online</span>
<span class="badge badge-warning">Warning</span>
<span class="badge badge-danger">Error</span>
```
### Form Groups
```html
<div class="form-group">
<label for="frequency">Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350">
<span class="form-help">Enter frequency in MHz</span>
</div>
<div class="form-group">
<label for="gain">Gain</label>
<select id="gain">
<option value="auto">Auto</option>
<option value="30">30 dB</option>
</select>
</div>
<label class="form-check">
<input type="checkbox" id="alerts">
<span>Enable alerts</span>
</label>
```
### Stats Strip
Used in dashboards for horizontal statistics display:
```html
<div class="stats-strip">
<div class="stats-strip-inner">
<div class="strip-stat">
<span class="strip-value" id="count">0</span>
<span class="strip-label">COUNT</span>
</div>
<div class="strip-divider"></div>
<div class="strip-status">
<div class="status-dot active" id="statusDot"></div>
<span id="statusText">TRACKING</span>
</div>
<div class="strip-time" id="utcTime">--:--:-- UTC</div>
</div>
</div>
```
---
## Adding a New Module Page
### 1. Create the Route
In `routes/mymodule.py`:
```python
from flask import Blueprint, render_template
mymodule_bp = Blueprint('mymodule', __name__, url_prefix='/mymodule')
@mymodule_bp.route('/dashboard')
def dashboard():
return render_template('mymodule_dashboard.html',
offline_settings=get_offline_settings())
```
### 2. Register the Blueprint
In `routes/__init__.py`:
```python
from routes.mymodule import mymodule_bp
app.register_blueprint(mymodule_bp)
```
### 3. Create the Template
Option A: Simple page extending base.html
```html
{% extends 'layout/base.html' %}
{% set active_mode = 'mymodule' %}
{% block title %}My Module{% endblock %}
{% block navigation %}
{% include 'partials/nav.html' %}
{% endblock %}
{% block content %}
<!-- Your content -->
{% endblock %}
```
Option B: Full-screen dashboard
```html
{% extends 'layout/base_dashboard.html' %}
{% set active_mode = 'mymodule' %}
{% block dashboard_title %}MY MODULE{% endblock %}
{% block dashboard_content %}
<!-- Your dashboard content -->
{% endblock %}
```
### 4. Add to Navigation
In `templates/partials/nav.html`, add your module to the appropriate group:
```html
<button class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
onclick="switchMode('mymodule')">
<span class="nav-icon icon"><!-- SVG icon --></span>
<span class="nav-label">My Module</span>
</button>
```
Or if it's a dashboard link:
```html
<a href="/mymodule/dashboard"
class="mode-nav-btn {% if active_mode == 'mymodule' %}active{% endif %}"
style="text-decoration: none;">
<span class="nav-icon icon"><!-- SVG icon --></span>
<span class="nav-label">My Module</span>
</a>
```
### 5. Create Stylesheet
In `static/css/mymodule.css`:
```css
/**
* My Module Styles
*/
@import url('./core/variables.css');
/* Your styles using design tokens */
.mymodule-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-4);
}
```
---
## Adding a New Dashboard
For full-screen dashboards like ADSB, AIS, or Satellite:
### 1. Create the Template
```html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MY DASHBOARD // iNTERCEPT</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<!-- Design tokens (required) -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<!-- Fonts -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
{% endif %}
<!-- External libraries if needed -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Dashboard styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/mydashboard.css') }}">
</head>
<body>
<!-- Background effects -->
<div class="radar-bg"></div>
<div class="scanline"></div>
<!-- Header -->
<header class="header">
<div class="logo">
<a href="/" style="color: inherit; text-decoration: none;">
MY DASHBOARD
<span>// iNTERCEPT</span>
</a>
</div>
<div class="status-bar">
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
<a href="/" class="back-link">Main Dashboard</a>
</div>
</header>
<!-- Unified Navigation -->
{% set active_mode = 'mydashboard' %}
{% include 'partials/nav.html' %}
<!-- Stats Strip -->
<div class="stats-strip">
<!-- Stats content -->
</div>
<!-- Main Dashboard Content -->
<main class="dashboard">
<!-- Your dashboard layout -->
</main>
<script>
// Dashboard JavaScript
</script>
</body>
</html>
```
### 2. Create the Stylesheet
```css
/**
* My Dashboard Styles
*/
@import url('./core/variables.css');
:root {
/* Dashboard-specific aliases */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--bg-card: var(--bg-tertiary);
--grid-line: rgba(74, 158, 255, 0.08);
}
/* Your dashboard styles */
```
---
## Best Practices
### DO
- Use design tokens for all colors, spacing, and typography
- Include the nav partial on all pages for consistent navigation
- Set `active_mode` before including the nav partial
- Use semantic component classes (`btn`, `panel`, `badge`, etc.)
- Support both light and dark themes
- Test on mobile viewports
### DON'T
- Hardcode color values - use CSS variables
- Create new color variations without adding to tokens
- Duplicate navigation markup - use the partial
- Skip the favicon and design tokens imports
- Use inline styles for layout (use utility classes)
---
## File Structure
```
templates/
├── layout/
│ ├── base.html # Standard page base
│ └── base_dashboard.html # Dashboard page base
├── partials/
│ ├── nav.html # Unified navigation
│ ├── page_header.html # Page title component
│ └── settings-modal.html # Settings modal
├── components/
│ ├── card.html # Panel/card component
│ ├── empty_state.html # Empty state placeholder
│ ├── loading.html # Loading spinner
│ ├── stats_strip.html # Stats bar component
│ └── status_badge.html # Status indicator
├── index.html # Main dashboard
├── adsb_dashboard.html # Aircraft tracking
├── ais_dashboard.html # Vessel tracking
└── satellite_dashboard.html # Satellite tracking
static/css/
├── core/
│ ├── variables.css # Design tokens
│ ├── base.css # Reset & typography
│ ├── components.css # Component styles
│ └── layout.css # Layout styles
├── index.css # Main dashboard styles
├── adsb_dashboard.css # Aircraft dashboard
├── ais_dashboard.css # Vessel dashboard
├── satellite_dashboard.css # Satellite dashboard
└── responsive.css # Responsive breakpoints
```
+1 -1
View File
@@ -404,7 +404,7 @@ def iss_position():
return jsonify(result)
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:
+15 -16
View File
@@ -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;
padding: 0;
@@ -5,23 +13,14 @@
}
:root {
--bg-dark: #0a0c10;
--bg-panel: #0f1218;
--bg-card: #151a23;
--border-color: #1f2937;
--border-glow: #4a9eff;
--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;
/* Dashboard-specific aliases (for backward compatibility) */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--bg-card: var(--bg-tertiary);
/* Use tokens for shared values, keep custom values for dashboard-specific */
--grid-line: rgba(74, 158, 255, 0.08);
--radar-cyan: #4a9eff;
--radar-bg: #0f1218;
--radar-cyan: var(--accent-cyan);
--radar-bg: var(--bg-secondary);
}
body {
+14 -18
View File
@@ -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;
@@ -8,23 +13,14 @@
}
:root {
--bg-dark: #0a0c10;
--bg-panel: #0f1218;
--bg-card: #151a23;
--border-color: #1f2937;
--border-glow: #4a9eff;
--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;
/* Dashboard-specific aliases (for backward compatibility) */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--bg-card: var(--bg-tertiary);
/* Use tokens for shared values, keep custom values for dashboard-specific */
--grid-line: rgba(74, 158, 255, 0.08);
--radar-cyan: #4a9eff;
--radar-bg: #0f1218;
--radar-cyan: var(--accent-cyan);
--radar-bg: var(--bg-secondary);
}
body {
+371
View File
@@ -0,0 +1,371 @@
/* Function Strip (Action Bar) - Shared across modes
* Based on APRS strip pattern, reusable for Pager, Sensor, Bluetooth, WiFi, TSCM, etc.
*/
.function-strip {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 10px;
overflow: visible;
min-height: 44px;
}
.function-strip-inner {
display: flex;
align-items: center;
gap: 8px;
min-width: max-content;
}
/* Stats */
.function-strip .strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 10px;
background: rgba(74, 158, 255, 0.05);
border: 1px solid rgba(74, 158, 255, 0.15);
border-radius: 4px;
min-width: 55px;
}
.function-strip .strip-stat:hover {
background: rgba(74, 158, 255, 0.1);
border-color: rgba(74, 158, 255, 0.3);
}
.function-strip .strip-value {
font-family: '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); }
+420
View File
@@ -0,0 +1,420 @@
/**
* INTERCEPT Base Styles
* Reset, typography, and foundational element styles
* Requires: variables.css to be imported first
*/
/* ============================================
CSS RESET
============================================ */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-normal);
color: var(--text-primary);
background: var(--bg-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4, h5, h6 {
font-weight: var(--font-semibold);
line-height: var(--leading-tight);
color: var(--text-primary);
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
h5 { font-size: var(--text-lg); }
h6 { font-size: var(--text-base); }
p {
margin-bottom: var(--space-4);
}
a {
color: var(--accent-cyan);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--accent-cyan-hover);
}
a:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
strong, b {
font-weight: var(--font-semibold);
}
small {
font-size: var(--text-sm);
}
code, kbd, pre, samp {
font-family: var(--font-mono);
font-size: 0.9em;
}
code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
pre {
background: var(--bg-tertiary);
padding: var(--space-4);
border-radius: var(--radius-md);
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
}
/* ============================================
FORM ELEMENTS
============================================ */
button,
input,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
}
button {
cursor: pointer;
border: none;
background: none;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
input,
select,
textarea {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
color: var(--text-primary);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px var(--accent-cyan-dim);
}
input::placeholder,
textarea::placeholder {
color: var(--text-dim);
}
select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
}
input[type="checkbox"],
input[type="radio"] {
width: 16px;
height: 16px;
padding: 0;
cursor: pointer;
accent-color: var(--accent-cyan);
}
label {
display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-1);
}
/* ============================================
TABLES
============================================ */
table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
th,
td {
padding: var(--space-2) var(--space-3);
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
font-weight: var(--font-semibold);
color: var(--text-secondary);
background: var(--bg-secondary);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.05em;
}
tr:hover td {
background: var(--bg-tertiary);
}
/* ============================================
LISTS
============================================ */
ul, ol {
padding-left: var(--space-6);
margin-bottom: var(--space-4);
}
li {
margin-bottom: var(--space-1);
}
/* ============================================
UTILITY CLASSES
============================================ */
/* Text colors */
.text-primary { color: var(--text-primary); }
.text-secondary { color: var(--text-secondary); }
.text-muted { color: var(--text-muted); }
.text-cyan { color: var(--accent-cyan); }
.text-green { color: var(--accent-green); }
.text-red { color: var(--accent-red); }
.text-orange { color: var(--accent-orange); }
.text-amber { color: var(--accent-amber); }
/* Font utilities */
.font-mono { font-family: var(--font-mono); }
.font-medium { font-weight: var(--font-medium); }
.font-semibold { font-weight: var(--font-semibold); }
.font-bold { font-weight: var(--font-bold); }
/* Text sizes */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
/* Display */
.hidden { display: none !important; }
.block { display: block; }
.inline-block { display: inline-block; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
/* Flexbox */
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.flex-1 { flex: 1; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
/* Spacing */
.m-0 { margin: 0; }
.mt-2 { margin-top: var(--space-2); }
.mt-4 { margin-top: var(--space-4); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-4 { margin-bottom: var(--space-4); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
/* Borders */
.rounded { border-radius: var(--radius-md); }
.rounded-lg { border-radius: var(--radius-lg); }
.border { border: 1px solid var(--border-color); }
/* Truncate text */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ============================================
SCROLLBAR STYLING
============================================ */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-light);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-dim);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--border-light) var(--bg-secondary);
}
/* ============================================
SELECTION
============================================ */
::selection {
background: var(--accent-cyan-dim);
color: var(--text-primary);
}
/* ============================================
UX POLISH - TRANSITIONS & INTERACTIONS
============================================ */
/* Smooth page transitions */
html {
scroll-behavior: smooth;
}
/* Better focus ring for all interactive elements */
:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
}
/* Remove focus ring for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* Active state feedback */
button:active:not(:disabled),
a:active,
[role="button"]:active {
transform: scale(0.98);
}
/* Smooth transitions for all interactive elements */
button,
a,
input,
select,
textarea,
[role="button"] {
transition:
color var(--transition-fast),
background-color var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast),
opacity var(--transition-fast);
}
/* Subtle hover lift effect for cards and panels */
.card:hover,
.panel:hover {
box-shadow: var(--shadow-md);
}
/* Link underline on hover */
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Skip link for accessibility */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent-cyan);
color: var(--bg-primary);
padding: var(--space-2) var(--space-4);
z-index: 9999;
transition: top var(--transition-fast);
}
.skip-link:focus {
top: 0;
}
/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--border-color: #4b5563;
--text-secondary: #d1d5db;
}
}
+723
View File
@@ -0,0 +1,723 @@
/**
* INTERCEPT UI Components
* Reusable component styles for buttons, cards, badges, etc.
* Requires: variables.css and base.css
*/
/* ============================================
BUTTONS
============================================ */
/* Base button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-size: var(--text-sm);
font-weight: var(--font-medium);
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
text-decoration: none;
}
.btn:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Button variants */
.btn-primary {
background: var(--accent-cyan);
color: var(--text-inverse);
border-color: var(--accent-cyan);
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-cyan-hover);
border-color: var(--accent-cyan-hover);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-elevated);
border-color: var(--border-light);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-danger {
background: var(--accent-red);
color: white;
border-color: var(--accent-red);
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
border-color: #dc2626;
}
.btn-success {
background: var(--accent-green);
color: white;
border-color: var(--accent-green);
}
.btn-success:hover:not(:disabled) {
background: #16a34a;
border-color: #16a34a;
}
/* Button sizes */
.btn-sm {
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
}
.btn-lg {
padding: var(--space-3) var(--space-6);
font-size: var(--text-base);
}
/* Icon button */
.btn-icon {
padding: var(--space-2);
width: 36px;
height: 36px;
}
.btn-icon.btn-sm {
width: 28px;
height: 28px;
padding: var(--space-1);
}
/* ============================================
CARDS / PANELS
============================================ */
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.card-header-title {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.card-body {
padding: var(--space-4);
}
.card-footer {
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
}
/* Panel variant (used in dashboards) */
.panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-color);
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
}
.panel-indicator {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--status-offline);
}
.panel-indicator.active {
background: var(--status-online);
box-shadow: 0 0 8px var(--status-online);
}
.panel-content {
padding: var(--space-3);
}
/* ============================================
BADGES
============================================ */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px var(--space-2);
font-size: var(--text-xs);
font-weight: var(--font-medium);
border-radius: var(--radius-full);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.badge-primary {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.badge-success {
background: var(--accent-green-dim);
color: var(--accent-green);
}
.badge-warning {
background: var(--accent-orange-dim);
color: var(--accent-orange);
}
.badge-danger {
background: var(--accent-red-dim);
color: var(--accent-red);
}
/* ============================================
STATUS INDICATORS
============================================ */
.status-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--status-offline);
flex-shrink: 0;
}
.status-dot.online,
.status-dot.active {
background: var(--status-online);
box-shadow: 0 0 6px var(--status-online);
}
.status-dot.warning {
background: var(--status-warning);
box-shadow: 0 0 6px var(--status-warning);
}
.status-dot.error,
.status-dot.offline {
background: var(--status-error);
}
.status-dot.inactive {
background: var(--status-offline);
}
/* Pulse animation for active status */
.status-dot.pulse {
animation: statusPulse 2s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ============================================
EMPTY STATE
============================================ */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-8) var(--space-4);
text-align: center;
color: var(--text-muted);
}
.empty-state-icon {
width: 48px;
height: 48px;
margin-bottom: var(--space-4);
opacity: 0.5;
}
.empty-state-title {
font-size: var(--text-base);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.empty-state-description {
font-size: var(--text-sm);
color: var(--text-dim);
max-width: 300px;
}
.empty-state-action {
margin-top: var(--space-4);
}
/* ============================================
LOADING STATES
============================================ */
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-cyan);
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
}
.spinner-sm {
width: 14px;
height: 14px;
border-width: 2px;
}
.spinner-lg {
width: 32px;
height: 32px;
border-width: 3px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Loading overlay */
.loading-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-overlay);
z-index: var(--z-modal);
}
/* Skeleton loader */
.skeleton {
background: linear-gradient(
90deg,
var(--bg-tertiary) 25%,
var(--bg-elevated) 50%,
var(--bg-tertiary) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-sm);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ============================================
STATS STRIP
============================================ */
.stats-strip {
display: flex;
align-items: center;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 0 var(--space-4);
height: var(--stats-strip-height);
overflow-x: auto;
gap: var(--space-1);
}
.strip-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 var(--space-3);
min-width: fit-content;
}
.strip-value {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--accent-cyan);
line-height: 1;
}
.strip-label {
font-size: 9px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
margin-top: 2px;
}
.strip-divider {
width: 1px;
height: 20px;
background: var(--border-color);
margin: 0 var(--space-2);
}
/* ============================================
FORM GROUPS
============================================ */
.form-group {
margin-bottom: var(--space-4);
}
.form-group label {
display: block;
margin-bottom: var(--space-1);
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
}
.form-help {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--text-dim);
}
.form-error {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--accent-red);
}
/* Inline checkbox/radio */
.form-check {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
.form-check input {
width: auto;
}
.form-check label {
margin-bottom: 0;
cursor: pointer;
}
/* ============================================
ALERTS / TOASTS
============================================ */
.alert {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
border: 1px solid;
font-size: var(--text-sm);
}
.alert-info {
background: var(--accent-cyan-dim);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.alert-success {
background: var(--accent-green-dim);
border-color: var(--accent-green);
color: var(--accent-green);
}
.alert-warning {
background: var(--accent-orange-dim);
border-color: var(--accent-orange);
color: var(--accent-orange);
}
.alert-danger {
background: var(--accent-red-dim);
border-color: var(--accent-red);
color: var(--accent-red);
}
/* ============================================
TOOLTIPS
============================================ */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: var(--space-1) var(--space-2);
background: var(--bg-elevated);
color: var(--text-primary);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-fast), visibility var(--transition-fast);
z-index: var(--z-tooltip);
pointer-events: none;
margin-bottom: var(--space-1);
box-shadow: var(--shadow-md);
}
[data-tooltip]:hover::after {
opacity: 1;
visibility: visible;
}
/* ============================================
ICONS
============================================ */
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.icon svg {
width: 100%;
height: 100%;
}
.icon--sm {
width: 16px;
height: 16px;
}
.icon--lg {
width: 24px;
height: 24px;
}
/* ============================================
SECTION HEADERS
============================================ */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--border-color);
}
.section-title {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
/* ============================================
DIVIDERS
============================================ */
.divider {
height: 1px;
background: var(--border-color);
margin: var(--space-4) 0;
}
.divider-vertical {
width: 1px;
height: 100%;
background: var(--border-color);
margin: 0 var(--space-3);
}
/* ============================================
UX POLISH - ENHANCED INTERACTIONS
============================================ */
/* Button hover lift */
.btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
/* Card/Panel hover effects */
.card,
.panel {
transition:
box-shadow var(--transition-base),
border-color var(--transition-base),
transform var(--transition-base);
}
.card:hover,
.panel:hover {
border-color: var(--border-light);
}
/* Stats strip value highlight on hover */
.strip-stat {
transition: background-color var(--transition-fast);
border-radius: var(--radius-sm);
cursor: default;
}
.strip-stat:hover {
background: var(--bg-tertiary);
}
/* Status dot pulse animation */
.status-dot.online,
.status-dot.active {
animation: statusGlow 2s ease-in-out infinite;
}
@keyframes statusGlow {
0%, 100% {
box-shadow: 0 0 6px var(--status-online);
}
50% {
box-shadow: 0 0 12px var(--status-online), 0 0 20px var(--status-online);
}
}
/* Badge hover effect */
.badge {
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.badge:hover {
transform: scale(1.05);
}
/* Alert entrance animation */
.alert {
animation: alertSlideIn 0.3s ease-out;
}
@keyframes alertSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Loading spinner smooth appearance */
.spinner {
animation: spin 0.8s linear infinite, fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Input focus glow */
input:focus,
select:focus,
textarea:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px var(--accent-cyan-dim), 0 0 20px rgba(74, 158, 255, 0.1);
}
/* Nav item active indicator */
.mode-nav-btn.active::after,
.mobile-nav-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 2px;
background: currentColor;
border-radius: var(--radius-full);
}
/* Smooth tooltip appearance */
[data-tooltip]::after {
transition:
opacity var(--transition-fast),
visibility var(--transition-fast),
transform var(--transition-fast);
transform: translateX(-50%) translateY(-4px);
}
[data-tooltip]:hover::after {
transform: translateX(-50%) translateY(0);
}
/* Disabled state with better visual feedback */
:disabled,
.disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(30%);
}
+950
View File
@@ -0,0 +1,950 @@
/**
* INTERCEPT Layout Styles
* Global layout structure: header, navigation, sidebar, main content
* Requires: variables.css, base.css, components.css
*/
/* ============================================
APP SHELL
============================================ */
.app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-main {
flex: 1;
display: flex;
flex-direction: column;
}
/* ============================================
GLOBAL HEADER
============================================ */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-height);
padding: 0 var(--space-4);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.app-header-left {
display: flex;
align-items: center;
gap: var(--space-4);
}
.app-header-right {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* Logo */
.app-logo {
display: flex;
align-items: center;
gap: var(--space-3);
text-decoration: none;
color: var(--text-primary);
}
.app-logo-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
}
.app-logo-text {
display: flex;
flex-direction: column;
}
.app-logo-title {
font-size: var(--text-lg);
font-weight: var(--font-bold);
line-height: 1.2;
color: var(--text-primary);
}
.app-logo-tagline {
font-size: var(--text-xs);
color: var(--text-dim);
}
/* Page title in header */
.app-header-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
}
.app-header-subtitle {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-left: var(--space-2);
}
/* Header utilities */
.header-utilities {
display: flex;
align-items: center;
gap: var(--space-2);
}
.header-clock {
display: flex;
align-items: center;
gap: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.header-clock-label {
font-size: var(--text-xs);
color: var(--text-dim);
}
/* ============================================
GLOBAL NAVIGATION
============================================ */
.app-nav {
display: flex;
align-items: center;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: 0 var(--space-4);
height: var(--nav-height);
gap: var(--space-1);
overflow-x: auto;
}
.app-nav::-webkit-scrollbar {
height: 0;
}
/* Nav groups */
.nav-group {
display: flex;
align-items: center;
position: relative;
}
/* Dropdown trigger */
.nav-dropdown-trigger {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
background: transparent;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.nav-dropdown-trigger:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
.nav-dropdown-trigger.active {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.nav-dropdown-arrow {
width: 12px;
height: 12px;
transition: transform var(--transition-fast);
}
.nav-group.open .nav-dropdown-arrow {
transform: rotate(180deg);
}
/* Dropdown menu */
.nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
min-width: 180px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: var(--space-1);
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: all var(--transition-fast);
z-index: var(--z-dropdown);
}
.nav-group.open .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(4px);
}
/* Nav items */
.nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
border: none;
background: none;
width: 100%;
text-align: left;
}
.nav-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item.active {
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}
.nav-item-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Nav divider */
.nav-divider {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 var(--space-2);
}
/* Nav utilities (right side) */
.nav-utilities {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
padding-left: var(--space-4);
}
.nav-tool-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
}
.nav-tool-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
/* ============================================
MOBILE NAVIGATION
============================================ */
.mobile-nav {
display: none;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: var(--space-2) var(--space-3);
overflow-x: auto;
gap: var(--space-2);
}
.mobile-nav::-webkit-scrollbar {
height: 0;
}
.mobile-nav-btn {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--text-secondary);
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
white-space: nowrap;
text-decoration: none;
transition: all var(--transition-fast);
}
.mobile-nav-btn:hover,
.mobile-nav-btn.active {
background: var(--accent-cyan-dim);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* Hamburger button */
.hamburger-btn {
display: none;
flex-direction: column;
justify-content: center;
gap: 4px;
width: 32px;
height: 32px;
padding: 6px;
background: transparent;
border: none;
cursor: pointer;
}
.hamburger-btn span {
display: block;
width: 100%;
height: 2px;
background: var(--text-secondary);
border-radius: 1px;
transition: all var(--transition-fast);
}
.hamburger-btn.open span:nth-child(1) {
transform: rotate(45deg) translate(4px, 4px);
}
.hamburger-btn.open span:nth-child(2) {
opacity: 0;
}
.hamburger-btn.open span:nth-child(3) {
transform: rotate(-45deg) translate(4px, -4px);
}
/* ============================================
CONTENT LAYOUTS
============================================ */
/* Main content with optional sidebar */
.content-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.app-sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-section {
padding: var(--space-4);
border-bottom: 1px solid var(--border-color);
}
.sidebar-section:last-child {
border-bottom: none;
}
.sidebar-section-title {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
/* Main content area */
.app-content {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
.app-content-full {
flex: 1;
overflow: hidden;
position: relative;
}
/* ============================================
DASHBOARD LAYOUTS
============================================ */
/* Full-screen dashboard (maps, etc.) */
.dashboard-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-4);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.dashboard-header-logo {
font-size: var(--text-lg);
font-weight: var(--font-bold);
color: var(--text-primary);
}
.dashboard-header-logo span {
font-size: var(--text-sm);
font-weight: var(--font-normal);
color: var(--text-dim);
margin-left: var(--space-2);
}
.dashboard-main {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.dashboard-map {
flex: 1;
position: relative;
}
.dashboard-sidebar {
width: 320px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-3);
}
/* ============================================
PAGE LAYOUTS
============================================ */
.page-container {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--space-6);
}
.page-header {
margin-bottom: var(--space-6);
}
.page-title {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.page-description {
font-size: var(--text-base);
color: var(--text-secondary);
}
/* ============================================
RESPONSIVE BREAKPOINTS
============================================ */
@media (max-width: 1024px) {
.app-sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(-100%);
transition: transform var(--transition-base);
}
.app-sidebar.open {
transform: translateX(0);
}
.dashboard-sidebar {
width: 280px;
}
}
@media (max-width: 768px) {
.app-nav {
display: none;
}
.mobile-nav {
display: flex;
}
.hamburger-btn {
display: flex;
}
.app-header {
padding: 0 var(--space-3);
}
.app-logo-text {
display: none;
}
.header-utilities {
gap: var(--space-1);
}
.page-container {
padding: var(--space-4);
}
.dashboard-sidebar {
position: fixed;
right: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(100%);
transition: transform var(--transition-base);
}
.dashboard-sidebar.open {
transform: translateX(0);
}
}
/* ============================================
OVERLAY (for mobile drawers)
============================================ */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: calc(var(--z-fixed) - 1);
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
}
.drawer-overlay.visible {
opacity: 1;
visibility: visible;
}
/* ============================================
BACK LINK
============================================ */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--text-secondary);
text-decoration: none;
transition: color var(--transition-fast);
}
.back-link:hover {
color: var(--accent-cyan);
}
/* ============================================
MODE NAVIGATION (from index.css)
Used by nav.html partial across all pages
============================================ */
/* Mode Navigation Bar */
.mode-nav {
display: none;
background: #151a23 !important; /* Explicit color - forced to ensure consistency */
border-bottom: 1px solid var(--border-color);
padding: 0 20px;
position: relative;
z-index: 100;
}
@media (min-width: 1024px) {
.mode-nav {
display: flex;
align-items: center;
gap: 8px;
height: 44px;
}
}
.mode-nav-group {
display: flex;
align-items: center;
gap: 4px;
}
.mode-nav-label {
font-size: 9px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 8px;
font-weight: 500;
}
.mode-nav-divider {
width: 1px;
height: 24px;
background: var(--border-color);
margin: 0 12px;
}
.mode-nav-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--text-secondary);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-btn .nav-icon {
font-size: 14px;
}
.mode-nav-btn .nav-icon svg {
width: 14px;
height: 14px;
}
.mode-nav-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mode-nav-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-btn.active {
background: var(--accent-cyan);
color: var(--bg-primary);
border-color: var(--accent-cyan);
}
.mode-nav-btn.active .nav-icon {
filter: brightness(0);
}
.mode-nav-actions {
display: flex;
align-items: center;
gap: 16px;
}
.nav-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--bg-elevated);
border: 1px solid var(--accent-cyan);
border-radius: 4px;
color: var(--accent-cyan);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.nav-action-btn .nav-icon {
font-size: 12px;
}
.nav-action-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.nav-action-btn:hover {
background: var(--accent-cyan);
color: var(--bg-primary);
}
/* Dropdown Navigation */
.mode-nav-dropdown {
position: relative;
}
.mode-nav-dropdown-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--text-secondary);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-dropdown-btn:hover {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-dropdown-btn .nav-icon {
font-size: 14px;
}
.mode-nav-dropdown-btn .nav-icon svg {
width: 14px;
height: 14px;
}
.mode-nav-dropdown-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.mode-nav-dropdown-btn .dropdown-arrow {
font-size: 8px;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mode-nav-dropdown-btn .dropdown-arrow svg {
width: 10px;
height: 10px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--border-color);
}
.mode-nav-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: var(--accent-cyan);
color: var(--bg-primary);
border-color: var(--accent-cyan);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
filter: brightness(0);
}
.mode-nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 180px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.15s ease;
z-index: 1000;
padding: 6px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.mode-nav-dropdown-menu .mode-nav-btn {
width: 100%;
justify-content: flex-start;
padding: 10px 12px;
border-radius: 4px;
margin: 0;
}
.mode-nav-dropdown-menu .mode-nav-btn:hover {
background: var(--bg-elevated);
}
.mode-nav-dropdown-menu .mode-nav-btn.active {
background: var(--accent-cyan);
color: var(--bg-primary);
}
/* Nav Bar Utilities (clock, theme, tools) */
.nav-utilities {
display: none;
align-items: center;
gap: 12px;
margin-left: auto;
flex-shrink: 0;
}
@media (min-width: 1024px) {
.nav-utilities {
display: flex;
}
}
.nav-clock {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
flex-shrink: 0;
white-space: nowrap;
}
.nav-clock .utc-label {
font-size: 9px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-clock .utc-time {
color: var(--accent-cyan);
font-weight: 600;
}
.nav-divider {
width: 1px;
height: 20px;
background: var(--border-color);
}
.nav-tools {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.nav-tool-btn {
width: 28px;
height: 28px;
min-width: 28px;
border-radius: 4px;
background: transparent;
border: 1px solid transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.nav-tool-btn:hover {
background: var(--bg-elevated);
border-color: var(--border-color);
color: var(--accent-cyan);
}
.nav-tool-btn svg {
width: 14px;
height: 14px;
}
.nav-tool-btn .icon svg {
width: 14px;
height: 14px;
}
/* Theme toggle icon states in nav bar */
.nav-tool-btn .icon-sun,
.nav-tool-btn .icon-moon {
position: absolute;
transition: opacity 0.2s, transform 0.2s;
font-size: 14px;
}
.nav-tool-btn .icon-sun {
opacity: 0;
transform: rotate(-90deg);
}
.nav-tool-btn .icon-moon {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-sun {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-moon {
opacity: 0;
transform: rotate(90deg);
}
/* Effects toggle icon states */
.nav-tool-btn .icon-effects-off {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-on {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-off {
display: flex;
}
+198
View File
@@ -0,0 +1,198 @@
/**
* INTERCEPT Design Tokens
* Single source of truth for colors, spacing, typography, and effects
* Import this file FIRST in any stylesheet that needs design tokens
*/
:root {
/* ============================================
COLOR PALETTE - Dark Theme (Default)
============================================ */
/* Backgrounds - layered depth system */
--bg-primary: #0a0c10;
--bg-secondary: #0f1218;
--bg-tertiary: #151a23;
--bg-card: #121620;
--bg-elevated: #1a202c;
--bg-overlay: rgba(0, 0, 0, 0.7);
/* Background aliases for components */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
/* Accent colors */
--accent-cyan: #4a9eff;
--accent-cyan-dim: rgba(74, 158, 255, 0.15);
--accent-cyan-hover: #6bb3ff;
--accent-green: #22c55e;
--accent-green-dim: rgba(34, 197, 94, 0.15);
--accent-red: #ef4444;
--accent-red-dim: rgba(239, 68, 68, 0.15);
--accent-orange: #f59e0b;
--accent-orange-dim: rgba(245, 158, 11, 0.15);
--accent-amber: #d4a853;
--accent-amber-dim: rgba(212, 168, 83, 0.15);
--accent-yellow: #eab308;
--accent-purple: #a855f7;
/* Text hierarchy */
--text-primary: #e8eaed;
--text-secondary: #9ca3af;
--text-dim: #4b5563;
--text-muted: #374151;
--text-inverse: #0a0c10;
/* Borders */
--border-color: #1f2937;
--border-light: #374151;
--border-glow: rgba(74, 158, 255, 0.2);
--border-focus: var(--accent-cyan);
/* Status colors */
--status-online: #22c55e;
--status-warning: #f59e0b;
--status-error: #ef4444;
--status-offline: #6b7280;
--status-info: #3b82f6;
/* ============================================
SPACING SCALE
============================================ */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
/* ============================================
TYPOGRAPHY
============================================ */
--font-sans: '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
View File
@@ -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');
* {
@@ -6,64 +18,6 @@
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 {
background: var(--bg-primary);
}
@@ -686,9 +640,11 @@ header h1 {
/* Mode Navigation Bar */
.mode-nav {
display: none;
background: var(--bg-tertiary);
background: #151a23 !important; /* Explicit color - forced to ensure consistency */
border-bottom: 1px solid var(--border-color);
padding: 0 20px;
position: relative;
z-index: 10;
}
@media (min-width: 1024px) {
@@ -1458,6 +1414,7 @@ header h1 .tagline {
overflow: visible;
padding: 12px;
position: relative;
margin: 0; /* Reset any inherited margins - spacing handled by parent gap */
}
.section h3 {
@@ -1634,6 +1591,101 @@ header h1 .tagline {
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 {
width: 100%;
padding: 12px;
@@ -6035,13 +6087,15 @@ body::before {
cursor: not-allowed;
}
.radio-action-btn.scan {
.radio-action-btn.scan,
.radio-action-btn.listen {
background: var(--accent-green);
border-color: var(--accent-green);
color: #000;
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);
}
+7 -2
View File
@@ -32,12 +32,17 @@
.threat-card.low.active { background: rgba(0,255,136,0.2); }
/* TSCM Dashboard */
/* Ensure output-panel doesn't clip TSCM content */
.output-panel:has(.tscm-dashboard) {
overflow: visible;
}
.tscm-dashboard {
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
padding-bottom: 80px; /* Space for status bar */
overflow: visible;
padding: 20px 16px 80px 16px; /* Extra top padding for function strip visibility */
}
.tscm-threat-banner {
display: flex;
+17 -16
View File
@@ -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;
padding: 0;
@@ -5,20 +13,11 @@
}
:root {
--bg-dark: #0a0c10;
--bg-panel: #0f1218;
--bg-card: #151a23;
--border-color: #1f2937;
--border-glow: #4a9eff;
--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;
/* Dashboard-specific aliases (for backward compatibility) */
--bg-dark: var(--bg-primary);
--bg-panel: var(--bg-secondary);
--bg-card: var(--bg-tertiary);
/* Use tokens for shared values, keep custom values for dashboard-specific */
--grid-line: rgba(74, 158, 255, 0.08);
}
@@ -211,6 +210,7 @@ body {
}
/* Main dashboard grid */
/* Header ~60px + Controls bar ~55px = ~115px */
.dashboard {
position: relative;
z-index: 10;
@@ -218,7 +218,8 @@ body {
grid-template-columns: 1fr 1fr 340px;
grid-template-rows: 1fr auto;
gap: 0;
height: calc(100vh - 60px);
height: calc(100dvh - 115px);
height: calc(100vh - 115px); /* Fallback */
min-height: 500px;
}
@@ -696,7 +697,7 @@ body {
display: flex;
flex-direction: column;
height: auto;
min-height: calc(100vh - 60px);
min-height: calc(100dvh - 115px);
}
.polar-container,
+136
View File
@@ -227,6 +227,9 @@ function startScanner() {
isScannerPaused = false;
scannerSignalActive = false;
// Update listening strip
updateListeningStripRunning(true);
// Update controls (with null checks)
const startBtn = document.getElementById('scannerStartBtn');
if (startBtn) {
@@ -289,6 +292,9 @@ function stopScanner() {
scannerSignalActive = false;
currentSignalLevel = 0;
// Update listening strip
updateListeningStripRunning(false);
// Re-enable listen button (will be in local mode after stop)
updateListenButtonState(false);
@@ -572,6 +578,10 @@ function handleFrequencyUpdate(data) {
const mainFreq = document.getElementById('mainScannerFreq');
if (mainFreq) mainFreq.textContent = freqStr;
// Update function strip frequency
const stripFreq = document.getElementById('listeningStripFreq');
if (stripFreq) stripFreq.textContent = freqStr;
// Update progress bar
const progress = ((data.frequency - scannerStartFreq) / (scannerEndFreq - scannerStartFreq)) * 100;
const progressBar = document.getElementById('scannerProgressBar');
@@ -622,6 +632,10 @@ function handleSignalFound(data) {
const mainSignalCount = document.getElementById('mainSignalCount');
if (mainSignalCount) mainSignalCount.textContent = scannerSignalCount;
// Update function strip signal count
const stripSignals = document.getElementById('listeningStripSignals');
if (stripSignals) stripSignals.textContent = scannerSignalCount;
// Update sidebar
updateScannerDisplay('SIGNAL FOUND', 'var(--accent-green)');
const signalPanel = document.getElementById('scannerSignalPanel');
@@ -2233,6 +2247,15 @@ function updateDirectListenUI(isPlaying, freq) {
if (quickFreq && freq) {
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.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
View File
@@ -336,6 +336,9 @@ const WiFiMode = (function() {
if (elements.scanModeDeep) {
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
}
// Initialize button visibility (default to quick mode)
setScanMode('quick');
}
function setScanMode(mode) {
@@ -349,6 +352,21 @@ const WiFiMode = (function() {
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);
}
@@ -357,7 +375,10 @@ const WiFiMode = (function() {
// ==========================================================================
async function startQuickScan() {
if (isScanning) return;
if (isScanning) {
showInfo('Scan already in progress');
return;
}
// Check for agent mode conflicts
if (!checkAgentConflicts()) {
@@ -365,6 +386,7 @@ const WiFiMode = (function() {
}
console.log('[WiFiMode] Starting quick scan...');
showInfo('Starting quick scan...');
setScanning(true, 'quick');
try {
@@ -436,6 +458,9 @@ const WiFiMode = (function() {
// Process results
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
// But keep polling if user wants continuous updates
if (scanMode === 'quick') {
@@ -449,7 +474,10 @@ const WiFiMode = (function() {
}
async function startDeepScan() {
if (isScanning) return;
if (isScanning) {
showInfo('Scan already in progress');
return;
}
// Check for agent mode conflicts
if (!checkAgentConflicts()) {
@@ -457,6 +485,7 @@ const WiFiMode = (function() {
}
console.log('[WiFiMode] Starting deep scan...');
showInfo('Starting deep scan (requires monitor mode)...');
setScanning(true, 'deep');
try {
@@ -516,6 +545,7 @@ const WiFiMode = (function() {
async function stopScan() {
console.log('[WiFiMode] Stopping scan...');
showInfo('Stopping scan...');
// Stop polling
if (pollTimer) {
@@ -538,6 +568,7 @@ const WiFiMode = (function() {
} else if (scanMode === 'deep') {
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' });
}
showInfo('Scan stopped');
} catch (error) {
console.warn('[WiFiMode] Error stopping scan:', error);
}
@@ -549,15 +580,21 @@ const WiFiMode = (function() {
isScanning = scanning;
if (mode) scanMode = mode;
// Update buttons
if (elements.quickScanBtn) {
elements.quickScanBtn.style.display = scanning ? 'none' : 'inline-block';
}
if (elements.deepScanBtn) {
elements.deepScanBtn.style.display = scanning ? 'none' : 'inline-block';
}
if (elements.stopScanBtn) {
elements.stopScanBtn.style.display = scanning ? 'inline-block' : 'none';
// Update buttons based on scanning state and current mode
if (scanning) {
// Scanning: hide start buttons, show stop button
if (elements.quickScanBtn) elements.quickScanBtn.style.display = 'none';
if (elements.deepScanBtn) elements.deepScanBtn.style.display = 'none';
if (elements.stopScanBtn) elements.stopScanBtn.style.display = 'inline-block';
} else {
// Not scanning: show appropriate start button based on mode, hide stop
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
@@ -1426,3 +1463,390 @@ document.addEventListener('DOMContentLoaded', () => {
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;">&times;</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],
};
})();
+14 -6
View File
@@ -1,9 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<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 -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
@@ -27,8 +31,10 @@
<header class="header">
<div class="logo">
AIRCRAFT RADAR
<span>// INTERCEPT - See the Invisible</span>
<a href="/" style="color: inherit; text-decoration: none;">
AIRCRAFT RADAR
<span>// iNTERCEPT</span>
</a>
</div>
<div class="status-bar">
<!-- Agent Selector -->
@@ -41,11 +47,13 @@
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
</label>
</div>
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
<a href="/?mode=aircraft" class="back-link">Main Dashboard</a>
</div>
</header>
<!-- Unified Navigation -->
{% set active_mode = 'adsb' %}
{% include 'partials/nav.html' %}
<!-- Slim Statistics Bar -->
<div class="stats-strip">
<div class="stats-strip-inner">
-35
View File
@@ -223,26 +223,6 @@
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 {
position: fixed;
@@ -301,21 +281,6 @@
</header>
<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">
<h1>Remote Agents</h1>
+15 -7
View File
@@ -1,14 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<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 -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
@@ -28,8 +32,10 @@
<header class="header">
<div class="logo">
VESSEL RADAR
<span>// INTERCEPT - AIS Tracking</span>
<a href="/" style="color: inherit; text-decoration: none;">
VESSEL RADAR
<span>// iNTERCEPT</span>
</a>
</div>
<div class="status-bar">
<!-- Agent Selector -->
@@ -42,11 +48,13 @@
<input type="checkbox" id="showAllAgents" onchange="toggleShowAllAgents()"> All
</label>
</div>
<a href="#" onclick="history.back(); return false;" class="back-link">Back</a>
<a href="/" class="back-link">Main Dashboard</a>
</div>
</header>
<!-- Unified Navigation -->
{% set active_mode = 'ais' %}
{% include 'partials/nav.html' %}
<div class="stats-strip">
<div class="stats-strip-inner">
<div class="strip-stat">
+24
View File
@@ -0,0 +1,24 @@
{#
Card/Panel Component
Reusable container with optional header and footer
Variables:
- title: Optional card header title
- indicator: If true, shows status indicator dot in header
- indicator_active: If true, indicator is active/green
- no_padding: If true, removes body padding
#}
<div class="panel">
{% if title %}
<div class="panel-header">
<span>{{ title }}</span>
{% if indicator %}
<div class="panel-indicator {% if indicator_active %}active{% endif %}"></div>
{% endif %}
</div>
{% endif %}
<div class="panel-content{% if no_padding %}" style="padding: 0;{% else %}{% endif %}">
{{ caller() }}
</div>
</div>
+38
View File
@@ -0,0 +1,38 @@
{#
Empty State Component
Display when no data is available
Variables:
- icon: Optional SVG icon (default: generic empty icon)
- title: Main message (default: "No data")
- description: Optional helper text
- action_text: Optional button text
- action_onclick: Optional button onclick handler
- action_href: Optional button link
#}
<div class="empty-state">
<div class="empty-state-icon">
{% if icon %}
{{ icon|safe }}
{% else %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12h8"/>
</svg>
{% endif %}
</div>
<div class="empty-state-title">{{ title|default('No data') }}</div>
{% if description %}
<div class="empty-state-description">{{ description }}</div>
{% endif %}
{% if action_text %}
<div class="empty-state-action">
{% if action_href %}
<a href="{{ action_href }}" class="btn btn-primary btn-sm">{{ action_text }}</a>
{% elif action_onclick %}
<button class="btn btn-primary btn-sm" onclick="{{ action_onclick }}">{{ action_text }}</button>
{% endif %}
</div>
{% endif %}
</div>
+27
View File
@@ -0,0 +1,27 @@
{#
Loading State Component
Display while data is being fetched
Variables:
- text: Optional loading text (default: "Loading...")
- size: 'sm', 'md', or 'lg' (default: 'md')
- overlay: If true, renders as full overlay
#}
{% if overlay %}
<div class="loading-overlay">
<div class="loading-content">
<div class="spinner {% if size == 'sm' %}spinner-sm{% elif size == 'lg' %}spinner-lg{% endif %}"></div>
{% if text %}
<div class="loading-text mt-3 text-secondary text-sm">{{ text }}</div>
{% endif %}
</div>
</div>
{% else %}
<div class="loading-inline flex items-center gap-3">
<div class="spinner {% if size == 'sm' %}spinner-sm{% elif size == 'lg' %}spinner-lg{% endif %}"></div>
{% if text %}
<span class="text-secondary text-sm">{{ text }}</span>
{% endif %}
</div>
{% endif %}
+47
View File
@@ -0,0 +1,47 @@
{#
Stats Strip Component
Horizontal bar displaying key metrics
Variables:
- stats: List of stat objects with 'id', 'value', 'label', and optional 'title'
- show_divider: Show divider after stats (default: true)
- status_dot_id: Optional ID for status indicator dot
- status_text_id: Optional ID for status text
- time_id: Optional ID for time display
#}
<div class="stats-strip">
<div class="stats-strip-inner">
{% for stat in stats %}
<div class="strip-stat" {% if stat.title %}title="{{ stat.title }}"{% endif %}>
<span class="strip-value" id="{{ stat.id }}">{{ stat.value|default('0') }}</span>
<span class="strip-label">{{ stat.label }}</span>
</div>
{% endfor %}
{% if show_divider|default(true) %}
<div class="strip-divider"></div>
{% endif %}
{# Additional content from caller #}
{% if caller is defined %}
{{ caller() }}
{% endif %}
{% if status_dot_id or status_text_id %}
<div class="strip-divider"></div>
<div class="strip-status">
{% if status_dot_id %}
<div class="status-dot inactive" id="{{ status_dot_id }}"></div>
{% endif %}
{% if status_text_id %}
<span id="{{ status_text_id }}">STANDBY</span>
{% endif %}
</div>
{% endif %}
{% if time_id %}
<div class="strip-time" id="{{ time_id }}">--:--:-- UTC</div>
{% endif %}
</div>
</div>
+27
View File
@@ -0,0 +1,27 @@
{#
Status Badge Component
Compact status indicator with dot and text
Variables:
- status: 'online', 'offline', 'warning', 'error' (default: 'offline')
- text: Status text to display
- id: Optional ID for the text element (for JS updates)
- dot_id: Optional ID for the dot element (for JS updates)
- pulse: If true, adds pulse animation to dot
#}
{% set status_class = {
'online': 'online',
'active': 'online',
'offline': 'offline',
'warning': 'warning',
'error': 'error',
'inactive': 'inactive'
}.get(status|default('offline'), 'inactive') %}
<div class="status-badge flex items-center gap-2">
<div class="status-dot {{ status_class }}{% if pulse %} pulse{% endif %}"
{% if dot_id %}id="{{ dot_id }}"{% endif %}></div>
<span class="text-sm"
{% if id %}id="{{ id }}"{% endif %}>{{ text|default('Unknown') }}</span>
</div>
+1147 -149
View File
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en" data-theme="{{ theme|default('dark') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}iNTERCEPT{% endblock %} // iNTERCEPT</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
{# Fonts - Conditional CDN/Local loading #}
{% if offline_settings and offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=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>
+226
View File
@@ -0,0 +1,226 @@
{% extends 'layout/base.html' %}
{#
Dashboard Base Template
Extended layout for full-screen dashboard pages (ADSB, AIS, Satellite, etc.)
Features: Full-height layout, stats strip, sidebar overlay on mobile
Variables:
- active_mode: The current mode for nav highlighting (e.g., 'adsb', 'ais', 'satellite')
#}
{% block styles %}
{{ super() }}
<style>
/* Dashboard-specific overrides */
html, body {
height: 100%;
overflow: hidden;
}
.app-shell {
height: 100vh;
overflow: hidden;
}
.app-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Radar/Grid background effect */
.dashboard-bg {
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
}
.radar-bg {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at center, transparent 0%, var(--bg-primary) 70%),
repeating-linear-gradient(0deg, transparent, transparent 50px, var(--border-color) 50px, var(--border-color) 51px),
repeating-linear-gradient(90deg, transparent, transparent 50px, var(--border-color) 50px, var(--border-color) 51px);
opacity: 0.3;
}
.grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(var(--border-color) 1px, transparent 1px),
linear-gradient(90deg, var(--border-color) 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.15;
}
.scanline {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.5;
animation: scanline 8s linear infinite;
}
@keyframes scanline {
0% { top: 0; }
100% { top: 100%; }
}
/* Animations toggle */
[data-animations="off"] .scanline,
[data-animations="off"] .radar-bg,
[data-animations="off"] .grid-bg {
display: none;
}
/* Dashboard main content */
.dashboard-content {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
}
.dashboard-map-container {
flex: 1;
position: relative;
}
.dashboard-sidebar {
width: 320px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-3);
}
@media (max-width: 1024px) {
.dashboard-sidebar {
width: 280px;
}
}
@media (max-width: 768px) {
.dashboard-sidebar {
position: fixed;
right: 0;
top: 0;
bottom: 0;
z-index: var(--z-fixed);
transform: translateX(100%);
transition: transform var(--transition-base);
}
.dashboard-sidebar.open {
transform: translateX(0);
}
}
</style>
{% endblock %}
{% block header %}
<header class="app-header" style="padding: 0 var(--space-3); height: 48px;">
<div class="app-header-left" style="gap: var(--space-3);">
<a href="/" class="app-logo" style="gap: var(--space-2);">
<svg class="app-logo-icon" width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 30 Q5 50, 15 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M22 35 Q14 50, 22 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M29 40 Q23 50, 29 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M85 30 Q95 50, 85 70" stroke="var(--accent-cyan)" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M78 35 Q86 50, 78 65" stroke="var(--accent-cyan)" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M71 40 Q77 50, 71 60" stroke="var(--accent-cyan)" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="22" r="6" fill="var(--accent-green)"/>
<rect x="44" y="35" width="12" height="45" rx="2" fill="var(--accent-cyan)"/>
<rect x="38" y="35" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
</svg>
</a>
<div class="dashboard-header-title">
<span style="font-size: var(--text-lg); font-weight: var(--font-bold); color: var(--text-primary);">
{% block dashboard_title %}DASHBOARD{% endblock %}
</span>
<span style="font-size: var(--text-sm); color: var(--text-dim); margin-left: var(--space-2);">
// iNTERCEPT
</span>
</div>
</div>
<div class="app-header-right">
{% block dashboard_header_center %}{% endblock %}
<div class="header-utilities" style="gap: var(--space-2);">
{% block agent_selector %}{% endblock %}
</div>
</div>
</header>
{% endblock %}
{% block navigation %}
{# Include the unified nav partial with active_mode set #}
{% include 'partials/nav.html' with context %}
{% endblock %}
{% block main %}
{# Background effects #}
<div class="dashboard-bg">
{% block dashboard_bg %}
<div class="radar-bg"></div>
{% endblock %}
<div class="scanline"></div>
</div>
{# Stats strip #}
{% block stats_strip %}{% endblock %}
{# Dashboard content #}
<div class="dashboard-content">
{% block dashboard_content %}{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// Dashboard-specific scripts
(function() {
// Mobile sidebar toggle
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.querySelector('.dashboard-sidebar');
const overlay = document.getElementById('drawerOverlay');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('visible');
});
}
if (overlay) {
overlay.addEventListener('click', function() {
sidebar?.classList.remove('open');
this.classList.remove('visible');
});
}
// UTC Clock update
function updateUtcClock() {
const now = new Date();
const utc = now.toISOString().slice(11, 19) + ' UTC';
document.querySelectorAll('[id$="utcTime"], [id$="UtcTime"]').forEach(el => {
el.textContent = utc;
});
}
setInterval(updateUtcClock, 1000);
updateUtcClock();
})();
</script>
{% endblock %}
-2
View File
@@ -514,8 +514,6 @@
<span>// MULTI-AGENT VIEW</span>
</div>
<nav class="header-nav">
<a href="#" onclick="history.back(); return false;">Back</a>
<a href="/">Dashboard</a>
<a href="/controller/manage">Manage Agents</a>
</nav>
</header>
+23 -23
View File
@@ -2,9 +2,20 @@
<div id="bluetoothMode" class="mode-content">
<!-- Capability Status -->
<div id="btCapabilityStatus" class="section" style="display: none;">
<span id="btCapabilityMarker" style="display: none;"></span>
<!-- Populated by JavaScript with capability warnings -->
</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) -->
<div id="btShowAllAgentsContainer" class="section" style="display: none; padding: 8px;">
<label class="inline-checkbox" style="font-size: 10px;">
@@ -15,12 +26,6 @@
<div class="section">
<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">
<label>Scan Mode</label>
<select id="btScanMode">
@@ -30,14 +35,6 @@
<option value="bluetoothctl">bluetoothctl (Linux)</option>
</select>
</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">
<label>Duration (seconds, 0 = continuous)</label>
<input type="number" id="btScanDuration" value="0" min="0" max="300" placeholder="0">
@@ -54,17 +51,10 @@
<!-- Message Container for status cards -->
<div id="btMessageContainer"></div>
<button class="run-btn" id="startBtBtn" onclick="btStartScan()">
Start Scanning
</button>
<button class="stop-btn" id="stopBtBtn" onclick="btStopScan()" style="display: none;">
Stop Scanning
</button>
<div class="section" style="margin-top: 10px;">
<div class="section" id="btExportSection" style="margin-top: 10px;">
<h3>Export</h3>
<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
</button>
<button class="preset-btn" onclick="btExport('json')" style="flex: 1;">
@@ -72,4 +62,14 @@
</button>
</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>
@@ -1,5 +1,15 @@
<!-- LISTENING POST MODE -->
<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">
<h3>Status</h3>
+10 -25
View File
@@ -1,20 +1,12 @@
<!-- PAGER MODE -->
<div id="pagerMode" class="mode-content active">
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
</div>
<div class="preset-buttons" id="presetButtons">
<!-- 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>
<h3>Pager Decoding</h3>
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
Decode POCSAG and FLEX pager messages from emergency services, hospitals, and other organizations.
</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>
@@ -29,11 +21,7 @@
</div>
<div class="section">
<h3>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>
<h3>Advanced Settings</h3>
<div class="form-group">
<label>Squelch Level</label>
<input type="text" id="squelch" value="0" placeholder="0 = off">
@@ -75,10 +63,7 @@
</div>
</div>
<button class="run-btn" id="startBtn" onclick="startDecoding()">
Start Decoding
</button>
<button class="stop-btn" id="stopBtn" onclick="stopDecoding()" style="display: none;">
Stop Decoding
</button>
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
<input type="hidden" id="frequency" value="153.350">
<input type="hidden" id="gain" value="0">
</div>
+20 -36
View File
@@ -1,40 +1,21 @@
<!-- RTLAMR UTILITY METER MODE -->
<div id="rtlamrMode" class="mode-content">
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="rtlamrFrequency" value="912.0" placeholder="e.g., 912.0">
</div>
<div class="preset-buttons">
<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>
<h3>Utility Meter Reading</h3>
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
Decode utility meter transmissions (water, gas, electric) using ERT protocol.
</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">
<h3>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>
<h3>Advanced Settings</h3>
<div class="form-group">
<label>PPM Correction</label>
<input type="text" id="rtlamrPpm" value="0" placeholder="Frequency correction">
</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">
<label>Filter by Meter ID (optional, comma-separated)</label>
<input type="text" id="rtlamrFilterId" placeholder="e.g., 12345678,87654321">
@@ -42,10 +23,7 @@
</div>
<div class="section">
<h3>Protocols</h3>
<div class="info-text" style="margin-bottom: 10px;">
rtlamr decodes utility meter transmissions (water, gas, electric) using ERT protocol.
</div>
<h3>Options</h3>
<div class="checkbox-group">
<label>
<input type="checkbox" id="rtlamrUnique" checked onchange="toggleRtlamrUnique()">
@@ -58,10 +36,16 @@
</div>
</div>
<button class="run-btn" id="startRtlamrBtn" onclick="startRtlamrDecoding()">
Start Listening
</button>
<button class="stop-btn" id="stopRtlamrBtn" onclick="stopRtlamrDecoding()" style="display: none;">
Stop Listening
</button>
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
<input type="hidden" id="rtlamrFrequency" value="912.0">
<input type="hidden" id="rtlamrGain" value="0">
<select id="rtlamrMsgType" style="display: none;">
<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>
+11 -22
View File
@@ -1,25 +1,17 @@
<!-- 433MHz SENSOR MODE -->
<div id="sensorMode" class="mode-content">
<div class="section">
<h3>Frequency</h3>
<div class="form-group">
<label>Frequency (MHz)</label>
<input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92">
</div>
<div class="preset-buttons">
<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>
<h3>433MHz Sensors</h3>
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
Decode signals from wireless sensors including weather stations, TPMS, doorbells, and 200+ other device protocols.
</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">
<h3>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>
<h3>Advanced Settings</h3>
<div class="form-group">
<label>PPM Correction</label>
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
@@ -27,7 +19,7 @@
</div>
<div class="section">
<h3>Protocols</h3>
<h3>Logging</h3>
<div class="info-text" style="margin-bottom: 10px;">
rtl_433 auto-detects 200+ device protocols including weather stations, TPMS, doorbells, and more.
</div>
@@ -39,10 +31,7 @@
</div>
</div>
<button class="run-btn" id="startSensorBtn" onclick="startSensorDecoding()">
Start Listening
</button>
<button class="stop-btn" id="stopSensorBtn" onclick="stopSensorDecoding()" style="display: none;">
Stop Listening
</button>
<!-- Hidden frequency/gain inputs for compatibility with existing JS -->
<input type="hidden" id="sensorFrequency" value="433.92">
<input type="hidden" id="sensorGain" value="0">
</div>
+82 -91
View File
@@ -1,78 +1,60 @@
<!-- TSCM MODE (Counter-Surveillance) -->
<div id="tscmMode" class="mode-content">
<!-- Configuration -->
<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>
<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">
<label>Sweep Type</label>
<select id="tscmSweepType">
<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>
<!-- Configuration -->
<div class="section">
<h3>Scan Sources</h3>
<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">
<label>Compare Against</label>
<select id="tscmBaselineSelect">
<option value="">No Baseline</option>
<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 class="section">
<h3>Options</h3>
<div class="form-group">
<label class="inline-checkbox">
<input type="checkbox" id="tscmVerboseResults">
Verbose results (store full device details)
</label>
</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>
<!-- 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 -->
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none; margin-top: 12px;">
<div class="scanner-ring">
@@ -115,46 +97,42 @@
</div>
<!-- Advanced -->
<div class="section" style="margin-top: 12px;">
<h3 style="margin-bottom: 12px;">Advanced</h3>
<div style="margin-bottom: 16px;">
<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 class="section">
<h3>Baseline Recording</h3>
<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 style="border-top: 1px solid var(--border-color); padding-top: 12px;">
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 6px; color: var(--text-secondary);">Meeting Window</label>
<div id="tscmMeetingStatus" style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
No active meeting
</div>
<div class="form-group">
<input type="text" id="tscmMeetingName" placeholder="Meeting name (optional)">
</div>
<button class="run-btn" id="tscmStartMeetingBtn" onclick="tscmStartMeeting()" style="width: 100%; padding: 8px;">
Start Meeting Window
</button>
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
End Meeting Window
</button>
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
Devices detected during meetings get flagged
</div>
<div class="section">
<h3>Meeting Window</h3>
<div id="tscmMeetingStatus" style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
No active meeting
</div>
<div class="form-group">
<input type="text" id="tscmMeetingName" placeholder="Meeting name (optional)">
</div>
<button class="run-btn" id="tscmStartMeetingBtn" onclick="tscmStartMeeting()" style="width: 100%; padding: 8px;">
Start Meeting Window
</button>
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
End Meeting Window
</button>
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
Devices detected during meetings get flagged
</div>
</div>
<!-- Tools -->
<div class="section" style="margin-top: 12px;">
<h3 style="margin-bottom: 10px;">Tools</h3>
<div class="section">
<h3>Tools</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px;">
<button class="preset-btn" onclick="tscmShowCapabilities()" style="font-size: 10px; padding: 8px;">
Capabilities
@@ -173,4 +151,17 @@
<!-- 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>
<!-- 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>
+65 -84
View File
@@ -1,49 +1,50 @@
<!-- WiFi MODE -->
<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">
<h3>WiFi Adapter</h3>
<div class="form-group">
<label>Select Device</label>
<select id="wifiInterfaceSelect" style="font-size: 12px;">
<option value="">Detecting interfaces...</option>
</select>
</div>
<button class="preset-btn" onclick="refreshWifiInterfaces()" style="width: 100%;">
<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
</button>
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="wifiToolStatus">
<span>airmon-ng:</span><span class="tool-status missing">Checking...</span>
<span>airodump-ng:</span><span class="tool-status missing">Checking...</span>
<h3>WiFi Scanning</h3>
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
Scan for nearby WiFi networks, analyze channels, and monitor wireless activity.
</p>
<!-- Scan Mode Tabs -->
<div style="margin-top: 12px;">
<div class="wifi-scan-mode-tabs" style="display: flex; gap: 4px; margin-bottom: 10px;">
<button id="wifiScanModeQuick" class="wifi-mode-tab active">
Quick Scan
</button>
<button id="wifiScanModeDeep" class="wifi-mode-tab">
Deep Scan
</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 class="section">
<h3>Monitor Mode</h3>
<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
</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
</button>
</div>
@@ -60,34 +61,12 @@
<div class="section">
<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">
<label>Channel (empty = hop)</label>
<input type="text" id="wifiChannel" placeholder="e.g., 6 or 36">
</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">
<h3>Attack Options</h3>
<div class="info-text" style="color: var(--accent-red); margin-bottom: 10px;">
@@ -105,11 +84,25 @@
<label>Deauth Count</label>
<input type="text" id="deauthCount" value="5" placeholder="5">
</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
</button>
</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 -->
<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>
@@ -131,10 +124,10 @@
<span id="captureStatus" style="font-weight: bold;">--</span>
</div>
<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
</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
</button>
</div>
@@ -153,32 +146,10 @@
</div>
</div>
<!-- v2 Scan Buttons -->
<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;">
<div class="section" id="wifiExportSection">
<h3>Export</h3>
<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
</button>
<button class="preset-btn" onclick="WiFiMode.exportData('json')" style="flex: 1;">
@@ -186,4 +157,14 @@
</button>
</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>
+317
View File
@@ -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>
+37
View File
@@ -0,0 +1,37 @@
{#
Page Header Partial
Consistent page title with optional description and actions
Variables:
- title: Page title (required)
- description: Optional description text
- back_url: Optional back link URL
- back_text: Optional back link text (default: "Back")
#}
<div class="page-header">
{% if back_url %}
<a href="{{ back_url }}" class="back-link mb-4">
<span class="icon icon--sm">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</span>
{{ back_text|default('Back') }}
</a>
{% endif %}
<div class="flex items-center justify-between">
<div>
<h1 class="page-title">{{ title }}</h1>
{% if description %}
<p class="page-description">{{ description }}</p>
{% endif %}
</div>
{% if caller is defined %}
<div class="page-actions">
{{ caller() }}
</div>
{% endif %}
</div>
</div>
+14 -9
View File
@@ -1,9 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<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 -->
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
@@ -27,8 +31,10 @@
<header class="header">
<div class="logo">
SATELLITE COMMAND
<span>// iNTERCEPT - See the Invisible</span>
<a href="/" style="color: inherit; text-decoration: none;">
SATELLITE COMMAND
<span>// iNTERCEPT</span>
</a>
</div>
<div class="stats-badges">
<div class="stat-badge">
@@ -62,10 +68,13 @@
<span id="trackingStatus">TRACKING</span>
</div>
<div class="datetime" id="utcTime">--:--:-- UTC</div>
<a href="/?mode=satellite" class="back-link">Main Dashboard</a>
</div>
</header>
<!-- Unified Navigation -->
{% set active_mode = 'satellite' %}
{% include 'partials/nav.html' %}
<main class="dashboard">
<!-- Polar Plot -->
<div class="panel polar-container">
@@ -294,10 +303,6 @@
function setupEmbeddedMode() {
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
document.body.classList.add('embedded');