mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Refine UI to clean professional style
This commit is contained in:
24
templates/components/card.html
Normal file
24
templates/components/card.html
Normal 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
templates/components/empty_state.html
Normal file
38
templates/components/empty_state.html
Normal 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
templates/components/loading.html
Normal file
27
templates/components/loading.html
Normal 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
templates/components/stats_strip.html
Normal file
47
templates/components/stats_strip.html
Normal 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
templates/components/status_badge.html
Normal file
27
templates/components/status_badge.html
Normal 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>
|
||||
169
templates/layout/base.html
Normal file
169
templates/layout/base.html
Normal 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
templates/layout/base_dashboard.html
Normal file
226
templates/layout/base_dashboard.html
Normal 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 %}
|
||||
317
templates/partials/nav.html
Normal file
317
templates/partials/nav.html
Normal 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
templates/partials/page_header.html
Normal file
37
templates/partials/page_header.html
Normal 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>
|
||||
Reference in New Issue
Block a user