feat: UI/UX overhaul — CSS cleanup, accessibility, error handling, inline style extraction

Phase 0 — CSS-only fixes:
- Fix --font-mono to use real monospace stack (JetBrains Mono, Fira Code, etc.)
- Replace hardcoded hex colors with CSS variables across 16+ files
- Merge global-nav.css (507 lines) into layout.css, delete original
- Reduce !important in responsive.css from 71 to 8 via .app-shell specificity
- Standardize breakpoints to 480/768/1024/1280px

Phase 1 — Loading states & SSE connection feedback:
- Add centralized SSEManager (sse-manager.js) with exponential backoff
- Add SSE status indicator dot in nav bar
- Add withLoadingButton() + .btn-loading CSS spinner
- Add mode section crossfade transitions

Phase 2 — Accessibility:
- Add aria-labels to icon-only buttons across mode partials
- Add for/id associations to 42 form labels in 5 mode partials
- Add aria-live on toast stack, enableListKeyNav() utility

Phase 3 — Destructive action guards & list overflow:
- Add confirmAction() styled modal, replace all 25 native confirm() calls
- Add toast cap at 5 simultaneous toasts
- Add list overflow indicator CSS

Phase 4 — Inline style extraction:
- Refactor switchMode() in app.js and index.html to use classList.toggle()
- Add CSS toggle rules for all switchMode-controlled elements
- Remove inline style="display:none" from 7+ HTML elements
- Add utility classes (.hidden, .d-flex, .d-grid, etc.)

Phase 5 — Mobile UX polish:
- pre/code overflow handling already in place
- Touch target sizing via --touch-min variable

Phase 6 — Error handling consistency:
- Add reportActionableError() to user-facing catch blocks in 5 mode JS files
- 28 error toast additions alongside existing console.error calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-12 13:04:36 +00:00
parent 05412fbfc3
commit e687862043
56 changed files with 2660 additions and 2238 deletions

View File

@@ -2449,7 +2449,7 @@ body {
font-size: 10px; font-size: 10px;
} }
@media (max-width: 600px) { @media (max-width: 480px) {
.squawk-item { .squawk-item {
grid-template-columns: 45px 80px 1fr; grid-template-columns: 45px 80px 1fr;
gap: 8px; gap: 8px;

View File

@@ -684,7 +684,7 @@ body {
} }
} }
@media (max-width: 720px) { @media (max-width: 768px) {
.controls { .controls {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;

View File

@@ -522,7 +522,7 @@
/* ============================================ /* ============================================
RESPONSIVE ADJUSTMENTS RESPONSIVE ADJUSTMENTS
============================================ */ ============================================ */
@media (max-width: 600px) { @media (max-width: 480px) {
.device-signal-row { .device-signal-row {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -841,7 +841,7 @@
/* ============================================ /* ============================================
RESPONSIVE MODAL RESPONSIVE MODAL
============================================ */ ============================================ */
@media (max-width: 600px) { @media (max-width: 480px) {
.modal-signal-stats { .modal-signal-stats {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }

View File

@@ -1128,7 +1128,7 @@
} }
/* Responsive adjustments for aggregated meters */ /* Responsive adjustments for aggregated meters */
@media (max-width: 500px) { @media (max-width: 480px) {
.meter-aggregated-grid { .meter-aggregated-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto; grid-template-rows: auto auto;
@@ -1922,7 +1922,7 @@
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 500px) { @media (max-width: 480px) {
.signal-details-modal-content { .signal-details-modal-content {
width: 95%; width: 95%;
max-height: 90vh; max-height: 90vh;

View File

@@ -429,7 +429,7 @@
border-color: rgba(31, 95, 168, 0.45); border-color: rgba(31, 95, 168, 0.45);
} }
@media (max-width: 920px) { @media (max-width: 1023px) {
.run-state-strip { .run-state-strip {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -440,7 +440,7 @@
} }
} }
@media (max-width: 640px) { @media (max-width: 768px) {
.command-palette-overlay { .command-palette-overlay {
padding: 8vh 10px 0; padding: 8vh 10px 0;
} }

View File

@@ -21,36 +21,36 @@ html {
tab-size: 4; tab-size: 4;
} }
body { body {
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: var(--text-base); font-size: var(--text-base);
line-height: var(--leading-normal); line-height: var(--leading-normal);
color: var(--text-primary); color: var(--text-primary);
background-color: var(--bg-primary); background-color: var(--bg-primary);
background-image: background-image:
radial-gradient(1200px 620px at 8% -12%, var(--ambient-top-left), transparent 62%), radial-gradient(1200px 620px at 8% -12%, var(--ambient-top-left), transparent 62%),
radial-gradient(980px 560px at 92% -16%, var(--ambient-top-right), transparent 64%), radial-gradient(980px 560px at 92% -16%, var(--ambient-top-right), transparent 64%),
radial-gradient(900px 520px at 50% 126%, var(--ambient-bottom), transparent 68%), radial-gradient(900px 520px at 50% 126%, var(--ambient-bottom), transparent 68%),
var(--noise-image), var(--noise-image),
linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: auto, auto, auto, 40px 40px, 48px 48px, 48px 48px; background-size: auto, auto, auto, 40px 40px, 48px 48px, 48px 48px;
background-attachment: fixed; background-attachment: fixed;
min-height: 100vh; min-height: 100vh;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* ============================================ /* ============================================
TYPOGRAPHY TYPOGRAPHY
============================================ */ ============================================ */
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
line-height: var(--leading-tight); line-height: var(--leading-tight);
color: var(--text-primary); color: var(--text-primary);
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
h1 { font-size: var(--text-4xl); } h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); } h2 { font-size: var(--text-3xl); }
@@ -91,20 +91,23 @@ code, kbd, pre, samp {
font-size: 0.9em; font-size: 0.9em;
} }
code { code {
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 2px 6px; padding: 2px 6px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} overflow-x: auto;
max-width: 100%;
}
pre { pre {
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: var(--space-4); padding: var(--space-4);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow-x: auto; overflow-x: auto;
} max-width: 100%;
}
pre code { pre code {
background: none; background: none;
@@ -135,38 +138,38 @@ button:disabled {
opacity: 0.5; opacity: 0.5;
} }
input, input,
select, select,
textarea { textarea {
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
color: var(--text-primary); color: var(--text-primary);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast); transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
} }
input:focus, input:focus,
select:focus, select:focus,
textarea:focus { textarea:focus {
outline: none; outline: none;
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
box-shadow: 0 0 0 2px var(--accent-cyan-dim); box-shadow: 0 0 0 2px var(--accent-cyan-dim);
} }
input::placeholder, input::placeholder,
textarea::placeholder { textarea::placeholder {
color: var(--text-dim); color: var(--text-dim);
} }
select { select {
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239fb0c7' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239fb0c7' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 8px center; background-position: right 8px center;
padding-right: 28px; padding-right: 28px;
} }
input[type="checkbox"], input[type="checkbox"],
input[type="radio"] { input[type="radio"] {
@@ -201,18 +204,18 @@ td {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
th { th {
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
color: var(--text-secondary); color: var(--text-secondary);
background: var(--bg-tertiary); background: var(--bg-tertiary);
text-transform: uppercase; text-transform: uppercase;
font-size: var(--text-xs); font-size: var(--text-xs);
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
tr:hover td { tr:hover td {
background: var(--bg-elevated); background: var(--bg-elevated);
} }
/* ============================================ /* ============================================
LISTS LISTS

View File

@@ -80,8 +80,8 @@
} }
.btn-danger:hover:not(:disabled) { .btn-danger:hover:not(:disabled) {
background: #dc2626; background: var(--accent-red-hover);
border-color: #dc2626; border-color: var(--accent-red-hover);
} }
.btn-success { .btn-success {
@@ -91,8 +91,8 @@
} }
.btn-success:hover:not(:disabled) { .btn-success:hover:not(:disabled) {
background: #16a34a; background: var(--accent-green-hover);
border-color: #16a34a; border-color: var(--accent-green-hover);
} }
/* Button sizes */ /* Button sizes */
@@ -415,6 +415,28 @@
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Button loading state */
.btn-loading {
position: relative;
color: transparent;
pointer-events: none;
}
.btn-loading::after {
content: '';
position: absolute;
width: 14px;
height: 14px;
top: 50%;
left: 50%;
margin-top: -7px;
margin-left: -7px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-cyan);
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
}
/* Loading overlay */ /* Loading overlay */
.loading-overlay { .loading-overlay {
position: absolute; position: absolute;
@@ -855,3 +877,205 @@ textarea:focus {
cursor: not-allowed; cursor: not-allowed;
filter: grayscale(30%); filter: grayscale(30%);
} }
/* ============================================
CONFIRMATION MODAL
============================================ */
.confirm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
animation: fadeIn 0.15s ease-out;
}
.confirm-modal {
background: var(--surface-panel-gradient);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-6);
min-width: 320px;
max-width: 440px;
box-shadow: var(--shadow-lg);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-3);
}
.confirm-modal-message {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: var(--leading-normal);
margin-bottom: var(--space-6);
}
.confirm-modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
}
/* ============================================
MODE SECTION TRANSITIONS
============================================ */
.mode-section {
display: none;
opacity: 0;
transition: opacity var(--transition-fast);
}
.mode-section.active {
display: block;
opacity: 1;
}
/* ============================================
SWITCHMODE TOGGLE CLASSES
Elements hidden by default, shown via .active
============================================ */
/* Stats sections in header (pager, sensor, wifi, satellite, aircraft) */
#pagerStats,
#sensorStats,
#aircraftStats,
#wifiStats,
#satelliteStats {
display: none;
}
#pagerStats.active,
#sensorStats.active,
#aircraftStats.active,
#wifiStats.active,
#satelliteStats.active {
display: flex;
}
/* Signal meter */
#signalMeter {
display: none;
}
#signalMeter.active {
display: block;
}
/* Dashboard buttons in nav */
#adsbDashboardBtn,
#satelliteDashboardBtn {
display: none;
}
#adsbDashboardBtn.active,
#satelliteDashboardBtn.active {
display: inline-flex;
}
/* Layout containers (wifi, bluetooth) */
.wifi-layout-container,
.bt-layout-container {
display: none;
}
.wifi-layout-container.active {
display: flex;
}
.bt-layout-container.active {
display: flex;
}
/* Visuals containers */
#aircraftVisuals {
display: none;
}
#aircraftVisuals.active {
display: grid;
}
#satelliteVisuals {
display: none;
}
#satelliteVisuals.active {
display: block;
}
/* RTL-SDR device section */
#rtlDeviceSection {
display: none;
}
#rtlDeviceSection.active {
display: block;
}
/* Tool status sections */
.tool-status-section {
display: none;
grid-template-columns: auto auto;
gap: 4px 8px;
align-items: center;
}
.tool-status-section.active {
display: grid;
}
/* Output console and status bar — visible when mode is active, hidden for full-visual modes.
switchMode() adds/removes .active; hidden by default until a mode is selected. */
.app-shell #output:not(.active) {
display: none;
}
.app-shell .status-bar:not(.active) {
display: none;
}
/* Recon panel — controlled by switchMode */
.app-shell #reconPanel:not(.active) {
display: none;
}
/* ============================================
UTILITY CLASSES
============================================ */
.hidden { display: none; }
.d-flex { display: flex; }
.d-grid { display: grid; }
.gap-2 { gap: var(--space-2); }
.gap-4 { gap: var(--space-4); }
.text-center { text-align: center; }
.w-full { width: 100%; }
/* Keyboard focus indicator for list items */
.keyboard-focused {
outline: 2px solid var(--accent-cyan);
outline-offset: -2px;
}
/* Overflow indicator */
.list-overflow-indicator {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
color: var(--text-dim);
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.list-overflow-indicator .btn {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
}

View File

@@ -22,31 +22,31 @@
/* ============================================ /* ============================================
GLOBAL HEADER GLOBAL HEADER
============================================ */ ============================================ */
.app-header { .app-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: var(--header-height); height: var(--header-height);
padding: 0 var(--space-4); padding: 0 var(--space-4);
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%); background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--z-sticky); z-index: var(--z-sticky);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.app-header::after { .app-header::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: 2px; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.6; opacity: 0.6;
pointer-events: none; pointer-events: none;
} }
.app-header-left { .app-header-left {
display: flex; display: flex;
@@ -129,29 +129,29 @@
/* ============================================ /* ============================================
GLOBAL NAVIGATION GLOBAL NAVIGATION
============================================ */ ============================================ */
.app-nav { .app-nav {
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 0 var(--space-4); padding: 0 var(--space-4);
height: var(--nav-height); height: var(--nav-height);
gap: var(--space-1); gap: var(--space-1);
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
} }
.app-nav::after { .app-nav::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: 1px; height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
.app-nav::-webkit-scrollbar { .app-nav::-webkit-scrollbar {
height: 0; height: 0;
@@ -202,14 +202,14 @@
} }
/* Dropdown menu */ /* Dropdown menu */
.nav-dropdown-menu { .nav-dropdown-menu {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
min-width: 180px; min-width: 180px;
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
padding: var(--space-1); padding: var(--space-1);
opacity: 0; opacity: 0;
@@ -299,27 +299,27 @@
/* ============================================ /* ============================================
MOBILE NAVIGATION MOBILE NAVIGATION
============================================ */ ============================================ */
.mobile-nav { .mobile-nav {
display: none; display: none;
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
overflow-x: auto; overflow-x: auto;
gap: var(--space-2); gap: var(--space-2);
position: relative; position: relative;
} }
.mobile-nav::after { .mobile-nav::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: 1px; height: 1px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.45; opacity: 0.45;
pointer-events: none; pointer-events: none;
} }
.mobile-nav::-webkit-scrollbar { .mobile-nav::-webkit-scrollbar {
height: 0; height: 0;
@@ -396,13 +396,13 @@
} }
/* Sidebar */ /* Sidebar */
.app-sidebar { .app-sidebar {
width: var(--sidebar-width); width: var(--sidebar-width);
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-section { .sidebar-section {
padding: var(--space-4); padding: var(--space-4);
@@ -447,28 +447,28 @@
overflow: hidden; overflow: hidden;
} }
.dashboard-header { .dashboard-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%); background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
} }
.dashboard-header::after { .dashboard-header::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: 2px; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.55; opacity: 0.55;
pointer-events: none; pointer-events: none;
} }
.dashboard-header-logo { .dashboard-header-logo {
font-size: var(--text-lg); font-size: var(--text-lg);
@@ -495,10 +495,10 @@
position: relative; position: relative;
} }
.dashboard-sidebar { .dashboard-sidebar {
width: 320px; width: 320px;
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
border-left: 1px solid var(--border-color); border-left: 1px solid var(--border-color);
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -638,27 +638,32 @@
Used by nav.html partial across all pages Used by nav.html partial across all pages
============================================ */ ============================================ */
/* NAVIGATION
Mode nav bar, dropdowns, utilities, theme/effects toggles
============================================ */
/* Mode Navigation Bar */ /* Mode Navigation Bar */
.mode-nav { .mode-nav {
display: none; display: none;
background: var(--bg-secondary) !important; /* Explicit color - forced to ensure consistency */ background: linear-gradient(180deg, rgba(17, 22, 32, 0.92), rgba(15, 20, 28, 0.88));
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 0 20px; padding: 0 20px;
position: relative; position: relative;
z-index: 100; z-index: var(--z-sticky);
} backdrop-filter: blur(10px);
}
.mode-nav::after {
content: ''; .mode-nav::after {
position: absolute; content: '';
left: 0; position: absolute;
right: 0; left: 0;
bottom: 0; right: 0;
height: 1px; bottom: 0;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); height: 1px;
opacity: 0.5; background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
pointer-events: none; opacity: 0.5;
} pointer-events: none;
}
@media (min-width: 1024px) { @media (min-width: 1024px) {
.mode-nav { .mode-nav {
@@ -682,6 +687,7 @@
letter-spacing: 1px; letter-spacing: 1px;
margin-right: 8px; margin-right: 8px;
font-weight: 500; font-weight: 500;
font-family: var(--font-mono);
} }
.mode-nav-divider { .mode-nav-divider {
@@ -692,33 +698,27 @@
} }
.mode-nav-btn { .mode-nav-btn {
display: flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 14px; padding: 8px 14px;
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: var(--radius-lg);
color: var(--text-secondary); color: var(--text-secondary);
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all var(--transition-fast);
} text-decoration: none;
.mode-nav-btn .nav-icon {
font-size: 14px;
}
.mode-nav-btn .nav-icon svg {
width: 14px;
height: 14px;
} }
.mode-nav-btn .nav-label { .mode-nav-btn .nav-label {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
} }
.mode-nav-btn:hover { .mode-nav-btn:hover {
@@ -728,13 +728,14 @@
} }
.mode-nav-btn.active { .mode-nav-btn.active {
background: var(--accent-cyan); background: var(--bg-elevated);
color: var(--bg-primary); color: var(--text-primary);
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
box-shadow: inset 0 -2px 0 var(--accent-cyan);
} }
.mode-nav-btn.active .nav-icon { .mode-nav-btn.active .nav-icon {
filter: brightness(0); color: var(--accent-cyan);
} }
.mode-nav-actions { .mode-nav-actions {
@@ -749,29 +750,29 @@
gap: 6px; gap: 6px;
padding: 8px 14px; padding: 8px 14px;
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--accent-cyan); border: 1px solid var(--border-light);
border-radius: 4px; border-radius: var(--radius-lg);
color: var(--accent-cyan); color: var(--text-primary);
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all var(--transition-fast);
}
.nav-action-btn .nav-icon {
font-size: 12px;
} }
.nav-action-btn .nav-label { .nav-action-btn .nav-label {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
} }
.nav-action-btn:hover { .nav-action-btn:hover {
background: var(--accent-cyan); background: var(--bg-tertiary);
color: var(--bg-primary); color: var(--text-primary);
box-shadow: var(--shadow-md);
border-color: var(--accent-cyan);
} }
/* Dropdown Navigation */ /* Dropdown Navigation */
@@ -780,19 +781,41 @@
} }
.mode-nav-dropdown-btn { .mode-nav-dropdown-btn {
display: flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 14px; padding: 8px 14px;
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: var(--radius-lg);
color: var(--text-secondary); color: var(--text-secondary);
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all var(--transition-fast);
}
.mode-nav-dropdown-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.mode-nav-dropdown-btn .dropdown-arrow {
width: 12px;
height: 12px;
margin-left: 4px;
transition: transform 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.mode-nav-dropdown-btn .dropdown-arrow svg {
width: 100%;
height: 100%;
} }
.mode-nav-dropdown-btn:hover { .mode-nav-dropdown-btn:hover {
@@ -801,31 +824,6 @@
border-color: var(--border-color); 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 { .mode-nav-dropdown.open .mode-nav-dropdown-btn {
background: var(--bg-elevated); background: var(--bg-elevated);
color: var(--text-primary); color: var(--text-primary);
@@ -837,13 +835,14 @@
} }
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn { .mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: var(--accent-cyan); background: var(--bg-elevated);
color: var(--bg-primary); color: var(--text-primary);
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
box-shadow: inset 0 -2px 0 var(--accent-cyan);
} }
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon { .mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
filter: brightness(0); color: var(--accent-cyan);
} }
.mode-nav-dropdown-menu { .mode-nav-dropdown-menu {
@@ -852,16 +851,17 @@
left: 0; left: 0;
margin-top: 4px; margin-top: 4px;
min-width: 180px; min-width: 180px;
background: var(--bg-secondary); background: var(--surface-glass);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 6px; border-radius: var(--radius-xl);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); box-shadow: var(--shadow-lg);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-8px); transform: translateY(-8px);
transition: all 0.15s ease; transition: all var(--transition-fast);
z-index: 1000; z-index: var(--z-dropdown);
padding: 6px; padding: 6px;
backdrop-filter: blur(10px);
} }
.mode-nav-dropdown.open .mode-nav-dropdown-menu { .mode-nav-dropdown.open .mode-nav-dropdown-menu {
@@ -874,8 +874,7 @@
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
padding: 10px 12px; padding: 10px 12px;
border-radius: 4px; border-radius: var(--radius-lg);
margin: 0;
} }
.mode-nav-dropdown-menu .mode-nav-btn:hover { .mode-nav-dropdown-menu .mode-nav-btn:hover {
@@ -883,8 +882,18 @@
} }
.mode-nav-dropdown-menu .mode-nav-btn.active { .mode-nav-dropdown-menu .mode-nav-btn.active {
background: var(--accent-cyan); background: var(--bg-elevated);
color: var(--bg-primary); color: var(--text-primary);
box-shadow: inset 0 -2px 0 var(--accent-cyan);
}
/* Focus-visible states for nav elements */
.mode-nav-btn:focus-visible,
.mode-nav-dropdown-btn:focus-visible,
.nav-action-btn:focus-visible,
.nav-tool-btn:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
} }
/* Nav Bar Utilities (clock, theme, tools) */ /* Nav Bar Utilities (clock, theme, tools) */
@@ -941,15 +950,15 @@
width: 28px; width: 28px;
height: 28px; height: 28px;
min-width: 28px; min-width: 28px;
border-radius: 4px; border-radius: var(--radius-lg);
background: transparent; background: var(--bg-elevated);
border: 1px solid transparent; border: 1px solid var(--border-color);
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all var(--transition-fast);
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
@@ -957,27 +966,36 @@
} }
.nav-tool-btn:hover { .nav-tool-btn:hover {
background: var(--bg-elevated); background: var(--bg-tertiary);
border-color: var(--border-color); border-color: var(--accent-cyan);
color: var(--accent-cyan); color: var(--accent-cyan);
box-shadow: var(--shadow-md);
} }
/* Nav tool button SVG sizing */
.nav-tool-btn svg { .nav-tool-btn svg {
width: 14px; width: 14px;
height: 14px; height: 14px;
stroke: currentColor;
}
.nav-tool-btn .icon {
display: inline-flex;
align-items: center;
justify-content: center;
} }
.nav-tool-btn .icon svg { .nav-tool-btn .icon svg {
width: 14px; width: 14px;
height: 14px; height: 14px;
stroke: currentColor;
} }
/* Theme toggle icon states in nav bar */ /* Theme toggle icon states */
.nav-tool-btn .icon-sun, .nav-tool-btn .icon-sun,
.nav-tool-btn .icon-moon { .nav-tool-btn .icon-moon {
position: absolute; position: absolute;
transition: opacity 0.2s, transform 0.2s; transition: opacity 0.2s, transform 0.2s;
font-size: 14px;
} }
.nav-tool-btn .icon-sun { .nav-tool-btn .icon-sun {
@@ -1000,7 +1018,7 @@
transform: rotate(90deg); transform: rotate(90deg);
} }
/* Effects toggle icon states */ /* Effects/animations toggle icon states */
.nav-tool-btn .icon-effects-off { .nav-tool-btn .icon-effects-off {
display: none; display: none;
} }
@@ -1012,3 +1030,114 @@
[data-animations="off"] .nav-tool-btn .icon-effects-off { [data-animations="off"] .nav-tool-btn .icon-effects-off {
display: flex; display: flex;
} }
/* Dashboard Button in Nav */
a.nav-dashboard-btn,
a.nav-dashboard-btn:link,
a.nav-dashboard-btn:visited {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: var(--radius-lg);
background: var(--bg-elevated);
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
text-decoration: none;
}
a.nav-dashboard-btn:hover {
background: var(--bg-tertiary);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
box-shadow: var(--shadow-md);
}
.nav-dashboard-btn .icon {
width: 14px;
height: 14px;
}
.nav-dashboard-btn .icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
}
.nav-dashboard-btn .nav-label {
font-family: var(--font-mono);
letter-spacing: 0.5px;
}
/* ---- Light theme nav overrides ---- */
[data-theme="light"] .mode-nav {
background: linear-gradient(180deg, rgba(240, 244, 250, 0.97) 0%, rgba(232, 238, 247, 0.95) 100%);
}
[data-theme="light"] .mode-nav-btn:hover {
background: rgba(220, 230, 244, 0.8);
}
[data-theme="light"] .mode-nav-btn.active {
background: rgba(220, 230, 244, 0.9);
color: var(--text-primary);
}
[data-theme="light"] .mode-nav-dropdown-btn:hover,
[data-theme="light"] .mode-nav-dropdown.open .mode-nav-dropdown-btn,
[data-theme="light"] .mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: rgba(220, 230, 244, 0.9);
color: var(--text-primary);
}
[data-theme="light"] .mode-nav-dropdown-menu {
background: rgba(248, 250, 253, 0.99);
box-shadow: 0 16px 36px rgba(18, 40, 66, 0.15);
}
[data-theme="light"] .mode-nav-dropdown-menu .mode-nav-btn:hover {
background: rgba(220, 230, 244, 0.85);
}
[data-theme="light"] .mode-nav-dropdown-menu .mode-nav-btn.active {
background: rgba(220, 230, 244, 0.95);
color: var(--text-primary);
}
[data-theme="light"] .nav-tool-btn {
background: rgba(235, 241, 250, 0.7);
border-color: var(--border-color);
}
[data-theme="light"] .nav-tool-btn:hover {
background: rgba(220, 230, 244, 0.9);
box-shadow: var(--shadow-sm);
}
[data-theme="light"] .nav-action-btn {
background: rgba(235, 241, 250, 0.85);
border-color: var(--border-color);
}
[data-theme="light"] .nav-action-btn:hover {
background: rgba(220, 230, 244, 0.95);
box-shadow: var(--shadow-sm);
}
[data-theme="light"] a.nav-dashboard-btn,
[data-theme="light"] a.nav-dashboard-btn:link,
[data-theme="light"] a.nav-dashboard-btn:visited {
background: rgba(235, 241, 250, 0.7);
border-color: var(--border-color);
color: var(--text-secondary);
}
[data-theme="light"] a.nav-dashboard-btn:hover {
background: rgba(220, 230, 244, 0.9);
box-shadow: var(--shadow-sm);
}

View File

@@ -31,8 +31,10 @@
--accent-cyan-dim: rgba(74, 163, 255, 0.16); --accent-cyan-dim: rgba(74, 163, 255, 0.16);
--accent-cyan-hover: #6bb3ff; --accent-cyan-hover: #6bb3ff;
--accent-green: #38c180; --accent-green: #38c180;
--accent-green-hover: #16a34a;
--accent-green-dim: rgba(56, 193, 128, 0.18); --accent-green-dim: rgba(56, 193, 128, 0.18);
--accent-red: #e25d5d; --accent-red: #e25d5d;
--accent-red-hover: #dc2626;
--accent-red-dim: rgba(226, 93, 93, 0.16); --accent-red-dim: rgba(226, 93, 93, 0.16);
--accent-orange: #d6a85e; --accent-orange: #d6a85e;
--accent-orange-dim: rgba(214, 168, 94, 0.16); --accent-orange-dim: rgba(214, 168, 94, 0.16);
@@ -96,7 +98,7 @@
TYPOGRAPHY TYPOGRAPHY
============================================ */ ============================================ */
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif; --font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif; --font-mono: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', Consolas, monospace;
/* Font sizes */ /* Font sizes */
--text-xs: 10px; --text-xs: 10px;
@@ -189,8 +191,10 @@
--accent-cyan-dim: rgba(31, 95, 168, 0.12); --accent-cyan-dim: rgba(31, 95, 168, 0.12);
--accent-cyan-hover: #2c73bf; --accent-cyan-hover: #2c73bf;
--accent-green: #1f8a57; --accent-green: #1f8a57;
--accent-green-hover: #167a4a;
--accent-green-dim: rgba(31, 138, 87, 0.12); --accent-green-dim: rgba(31, 138, 87, 0.12);
--accent-red: #c74444; --accent-red: #c74444;
--accent-red-hover: #b33a3a;
--accent-red-dim: rgba(199, 68, 68, 0.12); --accent-red-dim: rgba(199, 68, 68, 0.12);
--accent-orange: #b5863a; --accent-orange: #b5863a;
--accent-orange-dim: rgba(181, 134, 58, 0.12); --accent-orange-dim: rgba(181, 134, 58, 0.12);

View File

@@ -1,507 +0,0 @@
/* ============================================
Global Navigation Styles
Shared across all pages using nav.html
============================================ */
/* Icon base (kept lightweight for nav usage) */
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
}
.icon svg {
width: 100%;
height: 100%;
}
.icon--sm {
width: 14px;
height: 14px;
}
/* Mode Navigation Bar */
.mode-nav {
display: none;
background: linear-gradient(180deg, rgba(17, 22, 32, 0.92), rgba(15, 20, 28, 0.88));
border-bottom: 1px solid var(--border-color, #202833);
padding: 0 20px;
position: relative;
z-index: 1100;
backdrop-filter: blur(10px);
}
@media (min-width: 1024px) {
.mode-nav {
display: flex;
align-items: center;
gap: 8px;
height: 44px;
}
}
.mode-nav-label {
font-size: 9px;
color: var(--text-secondary, #b7c1cf);
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 8px;
font-weight: 500;
font-family: var(--font-mono);
}
.mode-nav-divider {
width: 1px;
height: 24px;
background: var(--border-color, #202833);
margin: 0 12px;
}
.mode-nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-secondary, #b7c1cf);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
}
.mode-nav-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.mode-nav-btn:hover {
background: rgba(27, 36, 51, 0.8);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-btn.active {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--accent-cyan, #4d7dbf);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
.mode-nav-btn.active .nav-icon {
color: var(--accent-cyan, #4d7dbf);
}
.mode-nav-actions {
display: flex;
align-items: center;
gap: 16px;
}
.nav-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: rgba(24, 31, 44, 0.85);
border: 1px solid var(--border-light, #2b3645);
border-radius: 6px;
color: var(--text-primary, #e7ebf2);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.nav-action-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.nav-action-btn:hover {
background: rgba(27, 36, 51, 0.95);
color: var(--text-primary, #e7ebf2);
box-shadow: 0 8px 16px rgba(5, 9, 15, 0.35);
border-color: var(--accent-cyan, #4d7dbf);
}
/* Dropdown Navigation */
.mode-nav-dropdown {
position: relative;
}
.mode-nav-dropdown-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-secondary, #b7c1cf);
font-family: var(--font-sans);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.mode-nav-dropdown-btn .nav-label {
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-mono);
font-size: 10px;
}
.mode-nav-dropdown-btn .dropdown-arrow {
width: 12px;
height: 12px;
margin-left: 4px;
transition: transform 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.mode-nav-dropdown-btn .dropdown-arrow svg {
width: 100%;
height: 100%;
}
.mode-nav-dropdown-btn:hover {
background: rgba(27, 36, 51, 0.8);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-dropdown.open .mode-nav-dropdown-btn {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--border-color, #202833);
}
.mode-nav-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: rgba(27, 36, 51, 0.9);
color: var(--text-primary, #e7ebf2);
border-color: var(--accent-cyan, #4d7dbf);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
.mode-nav-dropdown.has-active .mode-nav-dropdown-btn .nav-icon {
color: var(--accent-cyan, #4d7dbf);
}
.mode-nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 180px;
background: rgba(16, 22, 32, 0.98);
border: 1px solid var(--border-color, #202833);
border-radius: 8px;
box-shadow: 0 16px 36px rgba(5, 9, 15, 0.55);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.15s ease;
z-index: 1000;
padding: 6px;
}
.mode-nav-dropdown.open .mode-nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.mode-nav-dropdown-menu .mode-nav-btn {
width: 100%;
justify-content: flex-start;
padding: 10px 12px;
border-radius: 6px;
}
.mode-nav-dropdown-menu .mode-nav-btn:hover {
background: rgba(27, 36, 51, 0.85);
}
.mode-nav-dropdown-menu .mode-nav-btn.active {
background: rgba(27, 36, 51, 0.95);
color: var(--text-primary, #e7ebf2);
box-shadow: inset 0 -2px 0 var(--accent-cyan, #4d7dbf);
}
/* Nav Bar Utilities */
.nav-utilities {
display: none;
align-items: center;
gap: 12px;
margin-left: auto;
flex-shrink: 0;
}
@media (min-width: 1024px) {
.nav-utilities {
display: flex;
}
}
.nav-clock {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
flex-shrink: 0;
white-space: nowrap;
}
.nav-clock .utc-label {
font-size: 9px;
color: var(--text-dim, #8a97a8);
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-clock .utc-time {
color: var(--accent-cyan, #4d7dbf);
font-weight: 600;
}
.nav-divider {
width: 1px;
height: 20px;
background: var(--border-color, #202833);
}
.nav-tools {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.nav-tool-btn {
width: 28px;
height: 28px;
min-width: 28px;
border-radius: 6px;
background: rgba(20, 33, 53, 0.6);
border: 1px solid rgba(77, 125, 191, 0.12);
color: var(--text-secondary, #b7c1cf);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-tool-btn:hover {
background: rgba(27, 36, 51, 0.9);
border-color: var(--accent-cyan, #4d7dbf);
color: var(--accent-cyan, #4d7dbf);
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
}
/* Position relative needed for absolute positioned icon children */
.nav-tool-btn {
position: relative;
}
.mode-nav-btn:focus-visible,
.mode-nav-dropdown-btn:focus-visible,
.nav-action-btn:focus-visible,
.nav-tool-btn:focus-visible {
outline: 2px solid var(--accent-cyan, #4d7dbf);
outline-offset: 2px;
}
/* Nav tool button SVG sizing and styling */
.nav-tool-btn svg {
width: 14px;
height: 14px;
stroke: currentColor;
}
.nav-tool-btn .icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-tool-btn .icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
}
/* Theme toggle icon states */
.nav-tool-btn .icon-sun,
.nav-tool-btn .icon-moon {
position: absolute;
transition: opacity 0.2s, transform 0.2s;
}
.nav-tool-btn .icon-sun {
opacity: 0;
transform: rotate(-90deg);
}
.nav-tool-btn .icon-moon {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-sun {
opacity: 1;
transform: rotate(0deg);
}
[data-theme="light"] .nav-tool-btn .icon-moon {
opacity: 0;
transform: rotate(90deg);
}
/* ---- Light theme overrides ---- */
[data-theme="light"] .mode-nav {
background: linear-gradient(180deg, rgba(240, 244, 250, 0.97) 0%, rgba(232, 238, 247, 0.95) 100%);
}
[data-theme="light"] .mode-nav-btn:hover {
background: rgba(220, 230, 244, 0.8);
}
[data-theme="light"] .mode-nav-btn.active {
background: rgba(220, 230, 244, 0.9);
color: var(--text-primary);
}
[data-theme="light"] .mode-nav-dropdown-btn:hover,
[data-theme="light"] .mode-nav-dropdown.open .mode-nav-dropdown-btn,
[data-theme="light"] .mode-nav-dropdown.has-active .mode-nav-dropdown-btn {
background: rgba(220, 230, 244, 0.9);
color: var(--text-primary);
}
[data-theme="light"] .mode-nav-dropdown-menu {
background: rgba(248, 250, 253, 0.99);
box-shadow: 0 16px 36px rgba(18, 40, 66, 0.15);
}
[data-theme="light"] .mode-nav-dropdown-menu .mode-nav-btn:hover {
background: rgba(220, 230, 244, 0.85);
}
[data-theme="light"] .mode-nav-dropdown-menu .mode-nav-btn.active {
background: rgba(220, 230, 244, 0.95);
color: var(--text-primary);
}
[data-theme="light"] .nav-tool-btn {
background: rgba(235, 241, 250, 0.7);
border-color: rgba(31, 95, 168, 0.12);
}
[data-theme="light"] .nav-tool-btn:hover {
background: rgba(220, 230, 244, 0.9);
box-shadow: 0 4px 10px rgba(18, 40, 66, 0.1);
}
[data-theme="light"] .nav-action-btn {
background: rgba(235, 241, 250, 0.85);
border-color: rgba(31, 95, 168, 0.14);
}
[data-theme="light"] .nav-action-btn:hover {
background: rgba(220, 230, 244, 0.95);
box-shadow: 0 6px 14px rgba(18, 40, 66, 0.1);
}
[data-theme="light"] a.nav-dashboard-btn,
[data-theme="light"] a.nav-dashboard-btn:link,
[data-theme="light"] a.nav-dashboard-btn:visited {
background: rgba(235, 241, 250, 0.7) !important;
border-color: rgba(31, 95, 168, 0.12) !important;
color: var(--text-secondary) !important;
}
[data-theme="light"] a.nav-dashboard-btn:hover {
background: rgba(220, 230, 244, 0.9) !important;
box-shadow: 0 4px 10px rgba(18, 40, 66, 0.1);
}
/* Effects/animations toggle icon states */
.nav-tool-btn .icon-effects-off {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-on {
display: none;
}
[data-animations="off"] .nav-tool-btn .icon-effects-off {
display: flex;
}
/* Main Dashboard Button in Nav */
a.nav-dashboard-btn,
a.nav-dashboard-btn:link,
a.nav-dashboard-btn:visited {
display: inline-flex !important;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
background: rgba(20, 33, 53, 0.6) !important;
border: 1px solid rgba(77, 125, 191, 0.12) !important;
color: #b7c1cf !important;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
text-decoration: none !important;
}
a.nav-dashboard-btn:hover {
background: rgba(27, 36, 51, 0.9) !important;
border-color: #4d7dbf !important;
color: #4d7dbf !important;
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
}
.nav-dashboard-btn .icon {
width: 14px;
height: 14px;
}
.nav-dashboard-btn .icon svg {
width: 100%;
height: 100%;
stroke: currentColor;
}
.nav-dashboard-btn .nav-label {
font-family: var(--font-mono, 'Roboto Condensed', 'Arial Narrow', sans-serif);
letter-spacing: 0.5px;
}

View File

@@ -2739,7 +2739,7 @@ header h1 .tagline {
gap: 15px; gap: 15px;
} }
@media (max-width: 1100px) { @media (max-width: 1023px) {
.pass-predictor { .pass-predictor {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -4090,13 +4090,13 @@ header h1 .tagline {
} }
/* WiFi Responsive */ /* WiFi Responsive */
@media (max-width: 1400px) { @media (max-width: 1280px) {
.wifi-main-content { .wifi-main-content {
grid-template-columns: minmax(280px, 1fr) 240px 240px; grid-template-columns: minmax(280px, 1fr) 240px 240px;
} }
} }
@media (max-width: 1200px) { @media (max-width: 1280px) {
.wifi-layout-container { .wifi-layout-container {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -5415,7 +5415,7 @@ header h1 .tagline {
background: var(--bg-secondary, #1a1a2e); background: var(--bg-secondary, #1a1a2e);
} }
@media (max-width: 1200px) { @media (max-width: 1280px) {
.bt-layout-container { .bt-layout-container {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;

View File

@@ -513,7 +513,7 @@
RESPONSIVE — stack HUD vertically on narrow RESPONSIVE — stack HUD vertically on narrow
============================================ */ ============================================ */
@media (max-width: 900px) { @media (max-width: 1023px) {
.btl-hud { .btl-hud {
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;

View File

@@ -1378,7 +1378,7 @@
} }
/* Responsive traceroute path */ /* Responsive traceroute path */
@media (max-width: 600px) { @media (max-width: 480px) {
.mesh-traceroute-path { .mesh-traceroute-path {
flex-direction: column; flex-direction: column;
} }

View File

@@ -451,7 +451,7 @@
/* ── Responsive ── */ /* ── Responsive ── */
@media (max-width: 900px) { @media (max-width: 1023px) {
.ms-stats-strip { .ms-stats-strip {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
@@ -460,7 +460,7 @@
} }
} }
@media (max-width: 600px) { @media (max-width: 480px) {
.ms-stats-strip { .ms-stats-strip {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }

View File

@@ -67,7 +67,7 @@
.ook-warning { .ook-warning {
font-size: 11px; font-size: 11px;
color: #ffaa00; color: var(--accent-orange);
line-height: 1.5; line-height: 1.5;
} }

View File

@@ -221,7 +221,7 @@
} }
/* Responsive: stack cards on narrow screens */ /* Responsive: stack cards on narrow screens */
@media (max-width: 600px) { @media (max-width: 480px) {
.radiosonde-card { .radiosonde-card {
flex: 1 1 100%; flex: 1 1 100%;
max-width: 100%; max-width: 100%;

View File

@@ -408,7 +408,7 @@
} }
/* Small tablet / large phone (640px) */ /* Small tablet / large phone (640px) */
@media (max-width: 640px) { @media (max-width: 768px) {
.spy-station-footer { .spy-station-footer {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;

View File

@@ -1582,13 +1582,13 @@
gap: 12px; gap: 12px;
} }
@media (max-width: 1200px) { @media (max-width: 1280px) {
.subghz-rx-info-grid { .subghz-rx-info-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
@media (max-width: 900px) { @media (max-width: 1023px) {
.subghz-decode-layout { .subghz-decode-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -22,13 +22,13 @@
opacity: 0.7; opacity: 0.7;
margin-top: 2px; margin-top: 2px;
} }
.threat-card.critical { border-color: #ff3366; color: #ff3366; } .threat-card.critical { border-color: var(--severity-critical); color: var(--severity-critical); }
.threat-card.critical.active { background: rgba(255,51,102,0.2); } .threat-card.critical.active { background: rgba(255,51,102,0.2); }
.threat-card.high { border-color: #ff9933; color: #ff9933; } .threat-card.high { border-color: var(--severity-high); color: var(--severity-high); }
.threat-card.high.active { background: rgba(255,153,51,0.2); } .threat-card.high.active { background: rgba(255,153,51,0.2); }
.threat-card.medium { border-color: #ffcc00; color: #ffcc00; } .threat-card.medium { border-color: var(--severity-medium); color: var(--severity-medium); }
.threat-card.medium.active { background: rgba(255,204,0,0.2); } .threat-card.medium.active { background: rgba(255,204,0,0.2); }
.threat-card.low { border-color: #00ff88; color: #00ff88; } .threat-card.low { border-color: var(--severity-low); color: var(--severity-low); }
.threat-card.low.active { background: rgba(0,255,136,0.2); } .threat-card.low.active { background: rgba(0,255,136,0.2); }
/* TSCM Dashboard */ /* TSCM Dashboard */
@@ -105,26 +105,26 @@
background: rgba(74,158,255,0.1); background: rgba(74,158,255,0.1);
} }
.tscm-device-item.new { .tscm-device-item.new {
border-left-color: #ff9933; border-left-color: var(--severity-high);
animation: pulse-glow 2s infinite; animation: pulse-glow 2s infinite;
} }
.tscm-device-item.threat { .tscm-device-item.threat {
border-left-color: #ff3366; border-left-color: var(--severity-critical);
} }
.tscm-device-item.baseline { .tscm-device-item.baseline {
border-left-color: #00ff88; border-left-color: var(--neon-green);
} }
/* Classification colors */ /* Classification colors */
.tscm-device-item.classification-green { .tscm-device-item.classification-green {
border-left-color: #00cc00; border-left-color: var(--accent-green);
background: rgba(0, 204, 0, 0.1); background: rgba(0, 204, 0, 0.1);
} }
.tscm-device-item.classification-yellow { .tscm-device-item.classification-yellow {
border-left-color: #ffcc00; border-left-color: var(--severity-medium);
background: rgba(255, 204, 0, 0.1); background: rgba(255, 204, 0, 0.1);
} }
.tscm-device-item.classification-red { .tscm-device-item.classification-red {
border-left-color: #ff3333; border-left-color: var(--accent-red);
background: rgba(255, 51, 51, 0.15); background: rgba(255, 51, 51, 0.15);
animation: pulse-glow 2s infinite; animation: pulse-glow 2s infinite;
} }
@@ -182,7 +182,7 @@
transition: all 0.2s; transition: all 0.2s;
} }
.tscm-action-btn:hover { .tscm-action-btn:hover {
background: #2ecc71; background: var(--accent-green-hover);
transform: translateY(-1px); transform: translateY(-1px);
} }
.tscm-device-reasons { .tscm-device-reasons {
@@ -202,7 +202,7 @@
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; border-radius: 3px;
background: rgba(255, 51, 102, 0.2); background: rgba(255, 51, 102, 0.2);
color: #ff3366; color: var(--severity-critical);
border: 1px solid rgba(255, 51, 102, 0.4); border: 1px solid rgba(255, 51, 102, 0.4);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@@ -213,7 +213,7 @@
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; border-radius: 3px;
background: rgba(74, 158, 255, 0.2); background: rgba(74, 158, 255, 0.2);
color: #4a9eff; color: var(--accent-cyan);
border: 1px solid rgba(74, 158, 255, 0.4); border: 1px solid rgba(74, 158, 255, 0.4);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@@ -224,7 +224,7 @@
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 255, 136, 0.2); background: rgba(0, 255, 136, 0.2);
color: #00ff88; color: var(--neon-green);
border: 1px solid rgba(0, 255, 136, 0.4); border: 1px solid rgba(0, 255, 136, 0.4);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@@ -268,20 +268,20 @@
} }
.score-badge.score-low { .score-badge.score-low {
background: rgba(0, 204, 0, 0.2); background: rgba(0, 204, 0, 0.2);
color: #00cc00; color: var(--accent-green);
} }
.score-badge.score-medium { .score-badge.score-medium {
background: rgba(255, 204, 0, 0.2); background: rgba(255, 204, 0, 0.2);
color: #ffcc00; color: var(--severity-medium);
} }
.score-badge.score-high { .score-badge.score-high {
background: rgba(255, 51, 51, 0.2); background: rgba(255, 51, 51, 0.2);
color: #ff3333; color: var(--accent-red);
} }
.tscm-action { .tscm-action {
margin-top: 4px; margin-top: 4px;
font-size: 10px; font-size: 10px;
color: #ff9933; color: var(--severity-high);
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -290,12 +290,12 @@
padding: 12px; padding: 12px;
background: rgba(255, 153, 51, 0.1); background: rgba(255, 153, 51, 0.1);
border-radius: 6px; border-radius: 6px;
border: 1px solid #ff9933; border: 1px solid var(--severity-high);
} }
.tscm-correlations h4 { .tscm-correlations h4 {
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: 12px; font-size: 12px;
color: #ff9933; color: var(--severity-high);
} }
.correlation-item { .correlation-item {
padding: 8px; padding: 8px;
@@ -332,9 +332,9 @@
color: var(--text-muted); color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
} }
.summary-stat.high-interest .count { color: #ff3333; } .summary-stat.high-interest .count { color: var(--accent-red); }
.summary-stat.needs-review .count { color: #ffcc00; } .summary-stat.needs-review .count { color: var(--severity-medium); }
.summary-stat.informational .count { color: #00cc00; } .summary-stat.informational .count { color: var(--accent-green); }
.tscm-assessment { .tscm-assessment {
padding: 10px 14px; padding: 10px 14px;
margin: 12px 0; margin: 12px 0;
@@ -343,18 +343,18 @@
} }
.tscm-assessment.high-interest { .tscm-assessment.high-interest {
background: rgba(255, 51, 51, 0.15); background: rgba(255, 51, 51, 0.15);
border: 1px solid #ff3333; border: 1px solid var(--accent-red);
color: #ff3333; color: var(--accent-red);
} }
.tscm-assessment.needs-review { .tscm-assessment.needs-review {
background: rgba(255, 204, 0, 0.15); background: rgba(255, 204, 0, 0.15);
border: 1px solid #ffcc00; border: 1px solid var(--severity-medium);
color: #ffcc00; color: var(--severity-medium);
} }
.tscm-assessment.informational { .tscm-assessment.informational {
background: rgba(0, 204, 0, 0.15); background: rgba(0, 204, 0, 0.15);
border: 1px solid #00cc00; border: 1px solid var(--accent-green);
color: #00cc00; color: var(--accent-green);
} }
.tscm-disclaimer { .tscm-disclaimer {
font-size: 10px; font-size: 10px;
@@ -452,16 +452,16 @@
justify-content: center; justify-content: center;
border: 3px solid; border: 3px solid;
} }
.score-circle.high { border-color: #ff3333; background: rgba(255, 51, 51, 0.1); } .score-circle.high { border-color: var(--accent-red); background: rgba(255, 51, 51, 0.1); }
.score-circle.medium { border-color: #ffcc00; background: rgba(255, 204, 0, 0.1); } .score-circle.medium { border-color: var(--severity-medium); background: rgba(255, 204, 0, 0.1); }
.score-circle.low { border-color: #00cc00; background: rgba(0, 204, 0, 0.1); } .score-circle.low { border-color: var(--accent-green); background: rgba(0, 204, 0, 0.1); }
.score-circle .score-value { .score-circle .score-value {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
} }
.score-circle.high .score-value { color: #ff3333; } .score-circle.high .score-value { color: var(--accent-red); }
.score-circle.medium .score-value { color: #ffcc00; } .score-circle.medium .score-value { color: var(--severity-medium); }
.score-circle.low .score-value { color: #00cc00; } .score-circle.low .score-value { color: var(--accent-green); }
.score-circle .score-label { .score-circle .score-label {
font-size: 8px; font-size: 8px;
color: var(--text-muted); color: var(--text-muted);
@@ -521,7 +521,7 @@
} }
.indicator-type { .indicator-type {
background: rgba(255, 153, 51, 0.2); background: rgba(255, 153, 51, 0.2);
color: #ff9933; color: var(--severity-high);
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
font-size: 10px; font-size: 10px;
@@ -550,7 +550,7 @@
.tscm-threat-action { .tscm-threat-action {
margin-top: 6px; margin-top: 6px;
font-size: 10px; font-size: 10px;
color: #ff9933; color: var(--severity-high);
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
} }
@@ -606,7 +606,7 @@
font-size: 9px; font-size: 9px;
padding: 2px 6px; padding: 2px 6px;
background: rgba(74, 158, 255, 0.2); background: rgba(74, 158, 255, 0.2);
color: #4a9eff; color: var(--accent-cyan);
border-radius: 3px; border-radius: 3px;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -614,7 +614,7 @@
font-size: 9px; font-size: 9px;
padding: 2px 6px; padding: 2px 6px;
background: rgba(255, 153, 51, 0.2); background: rgba(255, 153, 51, 0.2);
color: #ff9933; color: var(--severity-high);
border-radius: 3px; border-radius: 3px;
} }
.correlation-detail-item { .correlation-detail-item {
@@ -634,10 +634,10 @@
background: rgba(0,0,0,0.2); background: rgba(0,0,0,0.2);
border: 1px solid; border: 1px solid;
} }
.tscm-threat-item.critical { border-color: #ff3366; background: rgba(255,51,102,0.1); } .tscm-threat-item.critical { border-color: var(--severity-critical); background: rgba(255,51,102,0.1); }
.tscm-threat-item.high { border-color: #ff9933; background: rgba(255,153,51,0.1); } .tscm-threat-item.high { border-color: var(--severity-high); background: rgba(255,153,51,0.1); }
.tscm-threat-item.medium { border-color: #ffcc00; background: rgba(255,204,0,0.1); } .tscm-threat-item.medium { border-color: var(--severity-medium); background: rgba(255,204,0,0.1); }
.tscm-threat-item.low { border-color: #00ff88; background: rgba(0,255,136,0.1); } .tscm-threat-item.low { border-color: var(--severity-low); background: rgba(0,255,136,0.1); }
.tscm-threat-header { .tscm-threat-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -807,7 +807,7 @@
.meeting-pulse { .meeting-pulse {
width: 10px; width: 10px;
height: 10px; height: 10px;
background: #ff3366; background: var(--severity-critical);
border-radius: 50%; border-radius: 50%;
animation: pulse-dot 1.5s ease-in-out infinite; animation: pulse-dot 1.5s ease-in-out infinite;
} }
@@ -819,7 +819,7 @@
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
letter-spacing: 1px; letter-spacing: 1px;
color: #ff3366; color: var(--severity-critical);
text-transform: uppercase; text-transform: uppercase;
} }
.meeting-info { .meeting-info {
@@ -865,15 +865,15 @@
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
} }
.cap-status.available { color: #00cc00; } .cap-status.available { color: var(--accent-green); }
.cap-status.limited { color: #ffcc00; } .cap-status.limited { color: var(--severity-medium); }
.cap-status.unavailable { color: #ff3333; } .cap-status.unavailable { color: var(--accent-red); }
.cap-limitations { .cap-limitations {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: #ff9933; color: var(--severity-high);
font-size: 10px; font-size: 10px;
} }
.cap-warn { .cap-warn {
@@ -907,15 +907,15 @@
} }
.health-badge.healthy { .health-badge.healthy {
background: rgba(0, 204, 0, 0.2); background: rgba(0, 204, 0, 0.2);
color: #00cc00; color: var(--accent-green);
} }
.health-badge.noisy { .health-badge.noisy {
background: rgba(255, 204, 0, 0.2); background: rgba(255, 204, 0, 0.2);
color: #ffcc00; color: var(--severity-medium);
} }
.health-badge.stale { .health-badge.stale {
background: rgba(255, 51, 51, 0.2); background: rgba(255, 51, 51, 0.2);
color: #ff3333; color: var(--accent-red);
} }
.health-age { .health-age {
color: var(--text-muted); color: var(--text-muted);
@@ -998,9 +998,9 @@
border-radius: 4px; border-radius: 4px;
border-left: 3px solid var(--border-color); border-left: 3px solid var(--border-color);
} }
.cap-detail-item.available { border-left-color: #00cc00; } .cap-detail-item.available { border-left-color: var(--accent-green); }
.cap-detail-item.limited { border-left-color: #ffcc00; } .cap-detail-item.limited { border-left-color: var(--severity-medium); }
.cap-detail-item.unavailable { border-left-color: #ff3333; } .cap-detail-item.unavailable { border-left-color: var(--accent-red); }
.cap-detail-header { .cap-detail-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1016,9 +1016,9 @@
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
} }
.cap-detail-status.available { background: rgba(0, 204, 0, 0.2); color: #00cc00; } .cap-detail-status.available { background: rgba(0, 204, 0, 0.2); color: var(--accent-green); }
.cap-detail-status.limited { background: rgba(255, 204, 0, 0.2); color: #ffcc00; } .cap-detail-status.limited { background: rgba(255, 204, 0, 0.2); color: var(--severity-medium); }
.cap-detail-status.unavailable { background: rgba(255, 51, 51, 0.2); color: #ff3333; } .cap-detail-status.unavailable { background: rgba(255, 51, 51, 0.2); color: var(--accent-red); }
.cap-detail-limits { .cap-detail-limits {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
@@ -1034,7 +1034,7 @@
margin-bottom: 6px; margin-bottom: 6px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
border-left: 3px solid #00cc00; border-left: 3px solid var(--accent-green);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -1064,7 +1064,7 @@
} }
.known-device-btn.remove { .known-device-btn.remove {
background: rgba(255, 51, 51, 0.2); background: rgba(255, 51, 51, 0.2);
color: #ff3333; color: var(--accent-red);
} }
.known-device-btn.remove:hover { .known-device-btn.remove:hover {
background: rgba(255, 51, 51, 0.4); background: rgba(255, 51, 51, 0.4);
@@ -1083,9 +1083,9 @@
.case-item:hover { .case-item:hover {
background: rgba(74, 158, 255, 0.1); background: rgba(74, 158, 255, 0.1);
} }
.case-item.priority-high { border-left-color: #ff3333; } .case-item.priority-high { border-left-color: var(--accent-red); }
.case-item.priority-normal { border-left-color: #4a9eff; } .case-item.priority-normal { border-left-color: var(--accent-cyan); }
.case-item.priority-low { border-left-color: #00cc00; } .case-item.priority-low { border-left-color: var(--accent-green); }
.case-header { .case-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1102,8 +1102,8 @@
border-radius: 3px; border-radius: 3px;
text-transform: uppercase; text-transform: uppercase;
} }
.case-status.open { background: rgba(0, 204, 0, 0.2); color: #00cc00; } .case-status.open { background: rgba(0, 204, 0, 0.2); color: var(--accent-green); }
.case-status.closed { background: rgba(128, 128, 128, 0.2); color: #888; } .case-status.closed { background: rgba(128, 128, 128, 0.2); color: var(--text-secondary); }
.case-meta { .case-meta {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
@@ -1117,7 +1117,7 @@
margin-bottom: 8px; margin-bottom: 8px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 6px; border-radius: 6px;
border-left: 3px solid #ff9933; border-left: 3px solid var(--severity-high);
} }
.playbook-header { .playbook-header {
display: flex; display: flex;
@@ -1135,9 +1135,9 @@
border-radius: 3px; border-radius: 3px;
text-transform: uppercase; text-transform: uppercase;
} }
.playbook-risk.high_interest { background: rgba(255, 51, 51, 0.2); color: #ff3333; } .playbook-risk.high_interest { background: rgba(255, 51, 51, 0.2); color: var(--accent-red); }
.playbook-risk.needs_review { background: rgba(255, 204, 0, 0.2); color: #ffcc00; } .playbook-risk.needs_review { background: rgba(255, 204, 0, 0.2); color: var(--severity-medium); }
.playbook-risk.informational { background: rgba(0, 204, 0, 0.2); color: #00cc00; } .playbook-risk.informational { background: rgba(0, 204, 0, 0.2); color: var(--accent-green); }
.playbook-desc { .playbook-desc {
font-size: 11px; font-size: 11px;
color: var(--text-secondary); color: var(--text-secondary);
@@ -1153,7 +1153,7 @@
border-radius: 3px; border-radius: 3px;
} }
.playbook-step-num { .playbook-step-num {
color: #ff9933; color: var(--severity-high);
font-weight: 600; font-weight: 600;
margin-right: 6px; margin-right: 6px;
} }
@@ -1223,19 +1223,19 @@
} }
.proximity-badge.very_close { .proximity-badge.very_close {
background: rgba(255, 51, 51, 0.2); background: rgba(255, 51, 51, 0.2);
color: #ff3333; color: var(--accent-red);
} }
.proximity-badge.close { .proximity-badge.close {
background: rgba(255, 153, 51, 0.2); background: rgba(255, 153, 51, 0.2);
color: #ff9933; color: var(--severity-high);
} }
.proximity-badge.moderate { .proximity-badge.moderate {
background: rgba(255, 204, 0, 0.2); background: rgba(255, 204, 0, 0.2);
color: #ffcc00; color: var(--severity-medium);
} }
.proximity-badge.far { .proximity-badge.far {
background: rgba(0, 204, 0, 0.2); background: rgba(0, 204, 0, 0.2);
color: #00cc00; color: var(--accent-green);
} }
/* Add to Known Device Button */ /* Add to Known Device Button */
@@ -1243,7 +1243,7 @@
padding: 4px 8px; padding: 4px 8px;
font-size: 10px; font-size: 10px;
background: rgba(0, 204, 0, 0.2); background: rgba(0, 204, 0, 0.2);
color: #00cc00; color: var(--accent-green);
border: 1px solid rgba(0, 204, 0, 0.3); border: 1px solid rgba(0, 204, 0, 0.3);
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
@@ -1307,15 +1307,15 @@
/* Modal Header Classification Colors */ /* Modal Header Classification Colors */
.device-detail-header.classification-cyan { .device-detail-header.classification-cyan {
background: linear-gradient(135deg, rgba(0, 204, 255, 0.2) 0%, rgba(0, 150, 200, 0.1) 100%); background: linear-gradient(135deg, rgba(0, 204, 255, 0.2) 0%, rgba(0, 150, 200, 0.1) 100%);
border-bottom: 2px solid #00ccff; border-bottom: 2px solid var(--accent-cyan);
} }
.device-detail-header.classification-orange { .device-detail-header.classification-orange {
background: linear-gradient(135deg, rgba(255, 153, 51, 0.2) 0%, rgba(200, 120, 40, 0.1) 100%); background: linear-gradient(135deg, rgba(255, 153, 51, 0.2) 0%, rgba(200, 120, 40, 0.1) 100%);
border-bottom: 2px solid #ff9933; border-bottom: 2px solid var(--severity-high);
} }
.device-detail-header.classification-green { .device-detail-header.classification-green {
background: linear-gradient(135deg, rgba(0, 204, 0, 0.2) 0%, rgba(0, 150, 0, 0.1) 100%); background: linear-gradient(135deg, rgba(0, 204, 0, 0.2) 0%, rgba(0, 150, 0, 0.1) 100%);
border-bottom: 2px solid #00cc00; border-bottom: 2px solid var(--accent-green);
} }
/* Playbook Enhancements */ /* Playbook Enhancements */
@@ -1330,7 +1330,7 @@
font-size: 9px; font-size: 9px;
padding: 2px 6px; padding: 2px 6px;
background: rgba(255, 153, 51, 0.2); background: rgba(255, 153, 51, 0.2);
color: #ff9933; color: var(--severity-high);
border-radius: 3px; border-radius: 3px;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -1360,7 +1360,7 @@
font-size: 9px; font-size: 9px;
padding: 2px 6px; padding: 2px 6px;
background: rgba(74, 158, 255, 0.2); background: rgba(74, 158, 255, 0.2);
color: #4a9eff; color: var(--accent-cyan);
border-radius: 3px; border-radius: 3px;
margin-left: 8px; margin-left: 8px;
} }
@@ -1404,7 +1404,7 @@
/* Recording State */ /* Recording State */
.icon-recording { .icon-recording {
color: #ff3366; color: var(--severity-critical);
} }
.icon-recording.active svg { .icon-recording.active svg {
@@ -1418,11 +1418,11 @@
/* Anomaly Indicator */ /* Anomaly Indicator */
.icon-anomaly { .icon-anomaly {
color: #ff9933; color: var(--severity-high);
} }
.icon-anomaly.critical { .icon-anomaly.critical {
color: #ff3366; color: var(--severity-critical);
} }
/* Export Icon */ /* Export Icon */
@@ -1508,7 +1508,7 @@
} }
.recording-status.active { .recording-status.active {
color: #ff3366; color: var(--severity-critical);
font-weight: 600; font-weight: 600;
} }
@@ -1526,12 +1526,12 @@
.anomaly-flag.needs-review { .anomaly-flag.needs-review {
background: rgba(255, 153, 51, 0.2); background: rgba(255, 153, 51, 0.2);
color: #ff9933; color: var(--severity-high);
} }
.anomaly-flag.high-interest { .anomaly-flag.high-interest {
background: rgba(255, 51, 51, 0.2); background: rgba(255, 51, 51, 0.2);
color: #ff3333; color: var(--accent-red);
} }
.anomaly-flag .icon { .anomaly-flag .icon {
@@ -1639,7 +1639,7 @@
} }
.tscm-summary-risk { .tscm-summary-risk {
font-size: 10px; font-size: 10px;
color: #ff9933; color: var(--severity-high);
margin-top: 4px; margin-top: 4px;
} }

View File

@@ -763,7 +763,7 @@
border: 1px solid rgba(74, 163, 255, 0.22); border: 1px solid rgba(74, 163, 255, 0.22);
} }
@media (max-width: 1100px) { @media (max-width: 1023px) {
.wf-monitor-strip { .wf-monitor-strip {
grid-template-columns: repeat(2, minmax(220px, 1fr)); grid-template-columns: repeat(2, minmax(220px, 1fr));
grid-auto-rows: minmax(70px, auto); grid-auto-rows: minmax(70px, auto);
@@ -778,7 +778,7 @@
} }
} }
@media (max-width: 720px) { @media (max-width: 768px) {
.wf-headline { .wf-headline {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;

View File

@@ -32,12 +32,12 @@
} }
.wxsat-strip-dot.capturing { .wxsat-strip-dot.capturing {
background: #00ff88; background: var(--neon-green);
animation: wxsat-pulse 1.5s ease-in-out infinite; animation: wxsat-pulse 1.5s ease-in-out infinite;
} }
.wxsat-strip-dot.decoding { .wxsat-strip-dot.decoding {
background: #00d4ff; background: var(--accent-cyan);
animation: wxsat-pulse 0.8s ease-in-out infinite; animation: wxsat-pulse 0.8s ease-in-out infinite;
} }
@@ -70,8 +70,8 @@
} }
.wxsat-strip-btn.stop { .wxsat-strip-btn.stop {
border-color: #ff4444; border-color: var(--accent-red);
color: #ff4444; color: var(--accent-red);
} }
.wxsat-strip-btn.stop:hover { .wxsat-strip-btn.stop:hover {
@@ -124,7 +124,7 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
cursor: pointer; cursor: pointer;
accent-color: #00ff88; accent-color: var(--neon-green);
} }
.wxsat-schedule-toggle input:checked + .wxsat-toggle-label { .wxsat-schedule-toggle input:checked + .wxsat-toggle-label {
@@ -207,12 +207,12 @@
} }
.wxsat-countdown-box.imminent { .wxsat-countdown-box.imminent {
border-color: #ffbb00; border-color: var(--accent-yellow);
box-shadow: 0 0 8px rgba(255, 187, 0, 0.2); box-shadow: 0 0 8px rgba(255, 187, 0, 0.2);
} }
.wxsat-countdown-box.active { .wxsat-countdown-box.active {
border-color: #00ff88; border-color: var(--neon-green);
box-shadow: 0 0 8px rgba(0, 255, 136, 0.3); box-shadow: 0 0 8px rgba(0, 255, 136, 0.3);
animation: wxsat-glow 1.5s ease-in-out infinite; animation: wxsat-glow 1.5s ease-in-out infinite;
} }
@@ -293,14 +293,14 @@
.wxsat-timeline-pass.apt { background: rgba(0, 212, 255, 0.6); } .wxsat-timeline-pass.apt { background: rgba(0, 212, 255, 0.6); }
.wxsat-timeline-pass.lrpt { background: rgba(0, 255, 136, 0.6); } .wxsat-timeline-pass.lrpt { background: rgba(0, 255, 136, 0.6); }
.wxsat-timeline-pass.scheduled { border: 1px solid #ffbb00; } .wxsat-timeline-pass.scheduled { border: 1px solid var(--accent-yellow); }
.wxsat-timeline-cursor { .wxsat-timeline-cursor {
position: absolute; position: absolute;
top: 2px; top: 2px;
width: 2px; width: 2px;
height: 20px; height: 20px;
background: #ff4444; background: var(--accent-red);
border-radius: 1px; border-radius: 1px;
z-index: 2; z-index: 2;
} }
@@ -375,7 +375,7 @@
.wxsat-pass-card.active, .wxsat-pass-card.active,
.wxsat-pass-card.selected { .wxsat-pass-card.selected {
border-color: #00ff88; border-color: var(--neon-green);
background: rgba(0, 255, 136, 0.05); background: rgba(0, 255, 136, 0.05);
} }
@@ -385,7 +385,7 @@
padding: 1px 4px; padding: 1px 4px;
border-radius: 2px; border-radius: 2px;
background: rgba(255, 187, 0, 0.15); background: rgba(255, 187, 0, 0.15);
color: #ffbb00; color: var(--accent-yellow);
margin-left: 6px; margin-left: 6px;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
text-transform: uppercase; text-transform: uppercase;
@@ -414,12 +414,12 @@
.wxsat-pass-mode.apt { .wxsat-pass-mode.apt {
background: rgba(0, 212, 255, 0.15); background: rgba(0, 212, 255, 0.15);
color: #00d4ff; color: var(--accent-cyan);
} }
.wxsat-pass-mode.lrpt { .wxsat-pass-mode.lrpt {
background: rgba(0, 255, 136, 0.15); background: rgba(0, 255, 136, 0.15);
color: #00ff88; color: var(--neon-green);
} }
.wxsat-pass-details { .wxsat-pass-details {
@@ -450,17 +450,17 @@
.wxsat-pass-quality.excellent { .wxsat-pass-quality.excellent {
background: rgba(0, 255, 136, 0.15); background: rgba(0, 255, 136, 0.15);
color: #00ff88; color: var(--neon-green);
} }
.wxsat-pass-quality.good { .wxsat-pass-quality.good {
background: rgba(0, 212, 255, 0.15); background: rgba(0, 212, 255, 0.15);
color: #00d4ff; color: var(--accent-cyan);
} }
.wxsat-pass-quality.fair { .wxsat-pass-quality.fair {
background: rgba(255, 187, 0, 0.15); background: rgba(255, 187, 0, 0.15);
color: #ffbb00; color: var(--accent-yellow);
} }
/* ===== Center Panel (Polar + Map) ===== */ /* ===== Center Panel (Polar + Map) ===== */
@@ -900,7 +900,7 @@
.wxsat-modal-btn.delete:hover { .wxsat-modal-btn.delete:hover {
background: rgba(255, 68, 68, 0.9); background: rgba(255, 68, 68, 0.9);
border-color: #ff4444; border-color: var(--accent-red);
color: var(--text-inverse); color: var(--text-inverse);
} }
@@ -920,12 +920,12 @@
} }
.wxsat-gallery-clear-btn:hover { .wxsat-gallery-clear-btn:hover {
color: #ff4444; color: var(--accent-red);
background: rgba(255, 68, 68, 0.1); background: rgba(255, 68, 68, 0.1);
} }
/* ===== Responsive ===== */ /* ===== Responsive ===== */
@media (max-width: 1100px) { @media (max-width: 1023px) {
.wxsat-content { .wxsat-content {
flex-direction: column; flex-direction: column;
} }
@@ -1041,8 +1041,8 @@
} }
.wxsat-phase-step.active { .wxsat-phase-step.active {
color: #00ff88; color: var(--neon-green);
border-color: #00ff88; border-color: var(--neon-green);
background: rgba(0, 255, 136, 0.1); background: rgba(0, 255, 136, 0.1);
box-shadow: 0 0 8px rgba(0, 255, 136, 0.2); box-shadow: 0 0 8px rgba(0, 255, 136, 0.2);
} }
@@ -1055,8 +1055,8 @@
} }
.wxsat-phase-step.error { .wxsat-phase-step.error {
color: #ff4444; color: var(--accent-red);
border-color: #ff4444; border-color: var(--accent-red);
background: rgba(255, 68, 68, 0.1); background: rgba(255, 68, 68, 0.1);
box-shadow: 0 0 8px rgba(255, 68, 68, 0.2); box-shadow: 0 0 8px rgba(255, 68, 68, 0.2);
} }
@@ -1115,8 +1115,8 @@
} }
.wxsat-console-entry.wxsat-log-signal { .wxsat-console-entry.wxsat-log-signal {
border-left-color: #00ff88; border-left-color: var(--neon-green);
color: #00ff88; color: var(--neon-green);
} }
.wxsat-console-entry.wxsat-log-progress { .wxsat-console-entry.wxsat-log-progress {
@@ -1125,18 +1125,18 @@
} }
.wxsat-console-entry.wxsat-log-save { .wxsat-console-entry.wxsat-log-save {
border-left-color: #ffbb00; border-left-color: var(--accent-yellow);
color: #ffbb00; color: var(--accent-yellow);
} }
.wxsat-console-entry.wxsat-log-error { .wxsat-console-entry.wxsat-log-error {
border-left-color: #ff4444; border-left-color: var(--accent-red);
color: #ff4444; color: var(--accent-red);
} }
.wxsat-console-entry.wxsat-log-warning { .wxsat-console-entry.wxsat-log-warning {
border-left-color: #ff8800; border-left-color: var(--neon-orange);
color: #ff8800; color: var(--neon-orange);
} }
.wxsat-console-entry.wxsat-log-debug { .wxsat-console-entry.wxsat-log-debug {

View File

@@ -41,15 +41,15 @@
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #444; background: var(--text-muted);
flex-shrink: 0; flex-shrink: 0;
} }
.wefax-strip-dot.scanning { background: #ffaa00; animation: wefax-pulse 1.5s ease-in-out infinite; } .wefax-strip-dot.scanning { background: var(--accent-orange); animation: wefax-pulse 1.5s ease-in-out infinite; }
.wefax-strip-dot.phasing { background: #ffcc44; animation: wefax-pulse 0.8s ease-in-out infinite; } .wefax-strip-dot.phasing { background: var(--accent-yellow); animation: wefax-pulse 0.8s ease-in-out infinite; }
.wefax-strip-dot.receiving { background: #00cc66; animation: wefax-pulse 1s ease-in-out infinite; } .wefax-strip-dot.receiving { background: var(--accent-green); animation: wefax-pulse 1s ease-in-out infinite; }
.wefax-strip-dot.complete { background: #00cc66; } .wefax-strip-dot.complete { background: var(--accent-green); }
.wefax-strip-dot.error { background: #f44; } .wefax-strip-dot.error { background: var(--accent-red); }
@keyframes wefax-pulse { @keyframes wefax-pulse {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
@@ -81,17 +81,17 @@
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.wefax-strip-btn.start { color: #ffaa00; border-color: #ffaa0044; } .wefax-strip-btn.start { color: var(--accent-orange); border-color: #ffaa0044; }
.wefax-strip-btn.start:hover { background: #ffaa0015; border-color: #ffaa00; } .wefax-strip-btn.start:hover { background: #ffaa0015; border-color: var(--accent-orange); }
.wefax-strip-btn.start.wefax-strip-btn-error { .wefax-strip-btn.start.wefax-strip-btn-error {
border-color: #ffaa00; border-color: var(--accent-orange);
color: #ffaa00; color: var(--accent-orange);
box-shadow: 0 0 8px rgba(255, 170, 0, 0.3); box-shadow: 0 0 8px rgba(255, 170, 0, 0.3);
animation: wefax-pulse 0.6s ease-in-out 3; animation: wefax-pulse 0.6s ease-in-out 3;
} }
.wefax-strip-btn.stop { color: #f44; border-color: #f4444444; } .wefax-strip-btn.stop { color: var(--accent-red); border-color: #f4444444; }
.wefax-strip-btn.stop:hover { background: #f4441a; border-color: #f44; } .wefax-strip-btn.stop:hover { background: #f4441a; border-color: var(--accent-red); }
.wefax-strip-divider { .wefax-strip-divider {
width: 1px; width: 1px;
@@ -114,7 +114,7 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.wefax-strip-value.accent-amber { color: #ffaa00; } .wefax-strip-value.accent-amber { color: var(--accent-orange); }
.wefax-strip-label { .wefax-strip-label {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
@@ -141,11 +141,11 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
cursor: pointer; cursor: pointer;
accent-color: #ffaa00; accent-color: var(--accent-orange);
} }
.wefax-schedule-toggle input:checked + span { .wefax-schedule-toggle input:checked + span {
color: #ffaa00; color: var(--accent-orange);
} }
/* --- Visuals Container --- */ /* --- Visuals Container --- */
@@ -185,7 +185,7 @@
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
color: #ffaa00; color: var(--accent-orange);
} }
.wefax-schedule-list { .wefax-schedule-list {
@@ -209,7 +209,7 @@
.wefax-schedule-entry.active { .wefax-schedule-entry.active {
background: #ffaa0010; background: #ffaa0010;
border-left: 3px solid #ffaa00; border-left: 3px solid var(--accent-orange);
} }
.wefax-schedule-entry.upcoming { .wefax-schedule-entry.upcoming {
@@ -221,7 +221,7 @@
} }
.wefax-schedule-time { .wefax-schedule-time {
color: #ffaa00; color: var(--accent-orange);
min-width: 45px; min-width: 45px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -241,7 +241,7 @@
.wefax-schedule-badge.live { .wefax-schedule-badge.live {
background: #ffaa0030; background: #ffaa0030;
color: #ffaa00; color: var(--accent-orange);
font-weight: 600; font-weight: 600;
} }
@@ -279,7 +279,7 @@
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
color: #ffaa00; color: var(--accent-orange);
} }
.wefax-live-content { .wefax-live-content {
@@ -298,7 +298,7 @@
.wefax-idle-state svg { .wefax-idle-state svg {
width: 48px; width: 48px;
height: 48px; height: 48px;
color: #ffaa0033; color: rgba(214, 168, 94, 0.2);
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -341,7 +341,7 @@
font-size: 11px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
color: #ffaa00; color: var(--accent-orange);
} }
.wefax-gallery-controls { .wefax-gallery-controls {
@@ -370,8 +370,8 @@
} }
.wefax-gallery-clear-btn:hover { .wefax-gallery-clear-btn:hover {
border-color: #f44; border-color: var(--accent-red);
color: #f44; color: var(--accent-red);
} }
.wefax-gallery-grid { .wefax-gallery-grid {
@@ -442,7 +442,7 @@
border-radius: 3px; border-radius: 3px;
border: none; border: none;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
color: #ccc; color: var(--text-secondary);
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -451,8 +451,8 @@
text-decoration: none; text-decoration: none;
} }
.wefax-gallery-action:hover { color: #fff; } .wefax-gallery-action:hover { color: var(--text-primary); }
.wefax-gallery-action.delete:hover { color: #f44; } .wefax-gallery-action.delete:hover { color: var(--accent-red); }
/* --- Countdown Bar + Timeline --- */ /* --- Countdown Bar + Timeline --- */
.wefax-countdown-bar { .wefax-countdown-bar {
@@ -490,12 +490,12 @@
} }
.wefax-countdown-box.imminent { .wefax-countdown-box.imminent {
border-color: #ffaa00; border-color: var(--accent-orange);
box-shadow: 0 0 8px rgba(255, 170, 0, 0.2); box-shadow: 0 0 8px rgba(255, 170, 0, 0.2);
} }
.wefax-countdown-box.active { .wefax-countdown-box.active {
border-color: #ffaa00; border-color: var(--accent-orange);
box-shadow: 0 0 8px rgba(255, 170, 0, 0.3); box-shadow: 0 0 8px rgba(255, 170, 0, 0.3);
animation: wefax-glow 1.5s ease-in-out infinite; animation: wefax-glow 1.5s ease-in-out infinite;
} }
@@ -530,7 +530,7 @@
.wefax-countdown-content { .wefax-countdown-content {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #ffaa00; color: var(--accent-orange);
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
} }
@@ -576,7 +576,7 @@
.wefax-timeline-broadcast.active { .wefax-timeline-broadcast.active {
background: rgba(255, 170, 0, 0.85); background: rgba(255, 170, 0, 0.85);
border: 1px solid #ffaa00; border: 1px solid var(--accent-orange);
} }
.wefax-timeline-cursor { .wefax-timeline-cursor {
@@ -584,7 +584,7 @@
top: 2px; top: 2px;
width: 2px; width: 2px;
height: 20px; height: 20px;
background: #ff4444; background: var(--accent-red);
border-radius: 1px; border-radius: 1px;
z-index: 2; z-index: 2;
} }

View File

@@ -361,7 +361,7 @@
RESPONSIVE RESPONSIVE
============================================ */ ============================================ */
@media (max-width: 900px) { @media (max-width: 1023px) {
.wfl-rssi-display { .wfl-rssi-display {
font-size: 48px; font-size: 48px;
} }

View File

@@ -424,30 +424,30 @@
/* ============== MOBILE LAYOUT FIXES ============== */ /* ============== MOBILE LAYOUT FIXES ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
/* Fix main content to allow scrolling on mobile */ /* Fix main content to allow scrolling on mobile */
.main-content { .app-shell .main-content {
height: auto !important; height: auto;
min-height: calc(100dvh - var(--header-height) - var(--nav-height)); min-height: calc(100dvh - var(--header-height) - var(--nav-height));
overflow-y: auto !important; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.sidebar { .app-shell .sidebar {
padding: 10px; padding: 10px;
gap: 10px; gap: 10px;
} }
.output-panel { .app-shell .output-panel {
min-height: 58vh; min-height: 58vh;
} }
.output-header { .app-shell .output-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
} }
.header-controls { .app-shell .header-controls {
width: 100%; width: 100%;
gap: 8px; gap: 8px;
overflow-x: auto; overflow-x: auto;
@@ -455,20 +455,21 @@
padding-bottom: 2px; padding-bottom: 2px;
} }
.header-controls .stats { .app-shell .header-controls .stats {
min-width: max-content; min-width: max-content;
} }
/* Container should not clip content */ /* Container should not clip content */
.container { .app-shell .container {
overflow: visible; overflow: visible;
height: auto; height: auto;
min-height: 100dvh; min-height: 100dvh;
} }
/* Layout containers need to stack vertically on mobile */ /* Layout containers need to stack vertically on mobile */
.wifi-layout-container, /* overrides inline style - JS sets display via style attribute */
.bt-layout-container { .app-shell .wifi-layout-container,
.app-shell .bt-layout-container {
flex-direction: column !important; flex-direction: column !important;
height: auto !important; height: auto !important;
max-height: none !important; max-height: none !important;
@@ -478,126 +479,128 @@
} }
/* Visual panels should be scrollable, not clipped */ /* Visual panels should be scrollable, not clipped */
.wifi-visuals, .app-shell .wifi-visuals,
.bt-visuals-column { .app-shell .bt-visuals-column {
max-height: none !important; max-height: none;
overflow: visible !important; overflow: visible;
margin-bottom: 15px; margin-bottom: 15px;
} }
/* Device lists should have reasonable height on mobile */ /* Device lists should have reasonable height on mobile */
.wifi-device-list, .app-shell .wifi-device-list,
.bt-device-list { .app-shell .bt-device-list {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
/* Visual panels should stack in single column on mobile when visible */ /* Visual panels should stack in single column on mobile when visible */
.wifi-visuals, .app-shell .wifi-visuals,
.bt-visuals-column { .app-shell .bt-visuals-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
/* Only apply flex when aircraft visuals are shown (via JS setting display: grid) */ /* Stack aircraft visuals vertically on mobile when active */
#aircraftVisuals[style*="grid"] { #aircraftVisuals.active {
display: flex !important; display: flex;
flex-direction: column !important; flex-direction: column;
gap: 10px; gap: 10px;
} }
/* APRS visuals - only when visible */ /* APRS visuals stack vertically on mobile */
#aprsVisuals[style*="flex"] { .app-shell #aprsVisuals {
flex-direction: column !important; flex-direction: column;
} }
.wifi-visual-panel { .app-shell .wifi-visual-panel {
grid-column: auto !important; grid-column: auto;
} }
.bt-main-area { .app-shell .bt-main-area {
flex-direction: column !important; flex-direction: column;
min-height: auto !important; min-height: auto;
} }
.bt-side-panels { .app-shell .bt-side-panels {
width: 100% !important; width: 100%;
flex-direction: column !important; flex-direction: column;
} }
.bt-detail-grid { .app-shell .bt-detail-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.bt-row-secondary { .app-shell .bt-row-secondary {
padding-left: 0 !important; padding-left: 0;
white-space: normal !important; white-space: normal;
} }
.bt-row-actions { .app-shell .bt-row-actions {
padding-left: 0 !important; padding-left: 0;
justify-content: flex-start !important; justify-content: flex-start;
} }
.bt-list-summary { .app-shell .bt-list-summary {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
/* ============== MOBILE MAP FIXES ============== */ /* ============== MOBILE MAP FIXES ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
/* Aircraft map container needs explicit height on mobile */ /* Aircraft map container needs explicit height on mobile */
.aircraft-map-container { .app-shell .aircraft-map-container {
height: 300px !important; height: 300px;
min-height: 300px !important; min-height: 300px;
width: 100% !important; width: 100%;
} }
#aircraftMap { .app-shell #aircraftMap {
height: 100% !important; height: 100%;
width: 100% !important; width: 100%;
min-height: 250px; min-height: 250px;
} }
/* APRS map container */ /* APRS map container */
#aprsMap { .app-shell #aprsMap {
min-height: 300px !important; min-height: 300px;
height: 300px !important; height: 300px;
width: 100% !important; width: 100%;
} }
/* Satellite embed */ /* Satellite embed */
.satellite-dashboard-embed { .app-shell .satellite-dashboard-embed {
height: 400px !important; height: 400px;
min-height: 400px !important; min-height: 400px;
} }
/* Map panels should be full width */ /* Map panels should be full width */
/* overrides inline style - HTML sets grid-column via style attribute */
.wifi-visual-panel[style*="grid-column: span 2"] { .wifi-visual-panel[style*="grid-column: span 2"] {
grid-column: auto !important; grid-column: auto !important;
} }
/* Make map container full width when it has ACARS sidebar */ /* Make map container full width when it has ACARS sidebar */
/* overrides inline style - HTML sets flex-direction via style attribute */
.wifi-visual-panel[style*="display: flex"][style*="gap: 0"] { .wifi-visual-panel[style*="display: flex"][style*="gap: 0"] {
flex-direction: column !important; flex-direction: column !important;
} }
/* ACARS sidebar should be below map on mobile */ /* ACARS sidebar should be below map on mobile */
.main-acars-sidebar { .app-shell .main-acars-sidebar {
width: 100% !important; width: 100%;
max-width: none !important; max-width: none;
border-left: none !important; border-left: none;
border-top: 1px solid var(--border-color, #1f2937) !important; border-top: 1px solid var(--border-color, #1f2937);
} }
.main-acars-sidebar.collapsed { .app-shell .main-acars-sidebar.collapsed {
width: 100% !important; width: 100%;
} }
.main-acars-content { .app-shell .main-acars-content {
max-height: 200px !important; max-height: 200px;
} }
} }
@@ -611,55 +614,55 @@
touch-action: manipulation; touch-action: manipulation;
} }
.leaflet-control-zoom a { .app-shell .leaflet-container .leaflet-control-zoom a {
min-width: var(--touch-min, 44px) !important; min-width: var(--touch-min, 44px);
min-height: var(--touch-min, 44px) !important; min-height: var(--touch-min, 44px);
line-height: var(--touch-min, 44px) !important; line-height: var(--touch-min, 44px);
font-size: 18px !important; font-size: 18px;
} }
/* ============== MOBILE HEADER STATS ============== */ /* ============== MOBILE HEADER STATS ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
.header-stats { .app-shell .header-stats {
display: none !important;
}
/* Simplify header on mobile */
header h1 {
font-size: 16px !important;
}
header h1 .tagline,
header h1 .version-badge {
display: none; display: none;
} }
header .subtitle { /* Simplify header on mobile */
font-size: 10px !important; .app-shell header h1 {
font-size: 16px;
}
.app-shell header h1 .tagline,
.app-shell header h1 .version-badge {
display: none;
}
.app-shell header .subtitle {
font-size: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
header .logo svg { .app-shell header .logo svg {
width: 30px !important; width: 30px;
height: 30px !important; height: 30px;
} }
} }
/* ============== MOBILE MODE PANELS ============== */ /* ============== MOBILE MODE PANELS ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
/* Mode panel grids should be single column */ /* Mode panel grids should be single column */
.data-grid, .app-shell .data-grid,
.stats-grid, .app-shell .stats-grid,
.sensor-grid { .app-shell .sensor-grid {
grid-template-columns: 1fr !important; grid-template-columns: 1fr;
} }
/* Section headers should be easier to tap */ /* Section headers should be easier to tap */
.section h3 { .app-shell .section h3 {
min-height: var(--touch-min); min-height: var(--touch-min);
padding: 12px !important; padding: 12px;
} }
/* Tables need horizontal scroll */ /* Tables need horizontal scroll */
@@ -682,85 +685,85 @@
/* ============== WELCOME PAGE MOBILE ============== */ /* ============== WELCOME PAGE MOBILE ============== */
@media (max-width: 767px) { @media (max-width: 767px) {
.welcome-container { .app-shell .welcome-container {
padding: 15px !important; padding: 15px;
max-width: 100% !important; max-width: 100%;
} }
.welcome-header { .app-shell .welcome-header {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
gap: 10px; gap: 10px;
} }
.welcome-logo svg { .app-shell .welcome-logo svg {
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.welcome-title { .app-shell .welcome-title {
font-size: 24px !important; font-size: 24px;
} }
.welcome-content { .app-shell .welcome-content {
grid-template-columns: 1fr !important; grid-template-columns: 1fr;
} }
.mode-grid { .app-shell .mode-grid {
grid-template-columns: repeat(2, 1fr) !important; grid-template-columns: repeat(2, 1fr);
gap: 8px !important; gap: 8px;
} }
.mode-card { .app-shell .mode-card {
padding: 12px 8px !important; padding: 12px 8px;
} }
.mode-icon { .app-shell .mode-icon {
font-size: 20px !important; font-size: 20px;
} }
.mode-name { .app-shell .mode-name {
font-size: 11px !important; font-size: 11px;
} }
.mode-desc { .app-shell .mode-desc {
font-size: 9px !important; font-size: 9px;
} }
.changelog-release { .app-shell .changelog-release {
padding: 10px !important; padding: 10px;
} }
} }
/* ============== TSCM MODE MOBILE ============== */ /* ============== TSCM MODE MOBILE ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
.tscm-layout { .app-shell .tscm-layout {
flex-direction: column !important; flex-direction: column;
height: auto !important; height: auto;
} }
.tscm-spectrum-panel, .app-shell .tscm-spectrum-panel,
.tscm-detection-panel { .app-shell .tscm-detection-panel {
width: 100% !important; width: 100%;
max-width: none !important; max-width: none;
height: auto !important; height: auto;
min-height: 300px; min-height: 300px;
} }
} }
/* ============== LISTENING POST MOBILE ============== */ /* ============== LISTENING POST MOBILE ============== */
@media (max-width: 1023px) { @media (max-width: 1023px) {
.radio-controls-section { .app-shell .radio-controls-section {
flex-direction: column !important; flex-direction: column;
gap: 15px; gap: 15px;
} }
.knobs-row { .app-shell .knobs-row {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
.radio-module-box { .app-shell .radio-module-box {
width: 100% !important; width: 100%;
} }
} }

View File

@@ -134,7 +134,7 @@ body {
} }
/* Mobile header adjustments */ /* Mobile header adjustments */
@media (max-width: 800px) { @media (max-width: 768px) {
.header { .header {
padding: 10px 12px; padding: 10px 12px;
flex-wrap: wrap; flex-wrap: wrap;
@@ -709,7 +709,7 @@ body {
} }
/* Responsive */ /* Responsive */
@media (max-width: 1200px) { @media (max-width: 1280px) {
.dashboard { .dashboard {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr auto auto; grid-template-rows: 1fr auto auto;
@@ -745,7 +745,7 @@ body {
} }
} }
@media (max-width: 800px) { @media (max-width: 768px) {
.dashboard { .dashboard {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -528,13 +528,13 @@ html.map-cyber-enabled .leaflet-container::after {
} }
/* Responsive */ /* Responsive */
@media (max-width: 960px) { @media (max-width: 1023px) {
.settings-tabs { .settings-tabs {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
} }
@media (max-width: 640px) { @media (max-width: 768px) {
.settings-modal.active { .settings-modal.active {
padding: 20px 10px; padding: 20px 10px;
} }

View File

@@ -485,7 +485,7 @@ async function syncLocalModeStates() {
*/ */
function showAgentModeWarnings(runningModes, modesDetail = {}) { function showAgentModeWarnings(runningModes, modesDetail = {}) {
// SDR modes that can't run simultaneously on same device // SDR modes that can't run simultaneously on same device
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc']; const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
const runningSdrModes = runningModes.filter(m => sdrModes.includes(m)); const runningSdrModes = runningModes.filter(m => sdrModes.includes(m));
let warning = document.getElementById('agentModeWarning'); let warning = document.getElementById('agentModeWarning');
@@ -613,7 +613,7 @@ function checkAgentAudioMode(modeToStart) {
* @param {string} modeToStart - Mode to start * @param {string} modeToStart - Mode to start
* @param {number} deviceToUse - Device index to use (optional, for smarter conflict detection) * @param {number} deviceToUse - Device index to use (optional, for smarter conflict detection)
*/ */
function checkAgentModeConflict(modeToStart, deviceToUse = null) { async function checkAgentModeConflict(modeToStart, deviceToUse = null) {
if (currentAgent === 'local') return true; // No conflict checking for local if (currentAgent === 'local') return true; // No conflict checking for local
// First check if this is an audio mode // First check if this is an audio mode
@@ -621,7 +621,7 @@ function checkAgentModeConflict(modeToStart, deviceToUse = null) {
return false; return false;
} }
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc']; const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
// If we're trying to start an SDR mode // If we're trying to start an SDR mode
if (sdrModes.includes(modeToStart)) { if (sdrModes.includes(modeToStart)) {
@@ -648,11 +648,12 @@ function checkAgentModeConflict(modeToStart, deviceToUse = null) {
return detail ? `${m} (SDR ${detail.device})` : m; return detail ? `${m} (SDR ${detail.device})` : m;
}).join(', '); }).join(', ');
const proceed = confirm( const proceed = await AppFeedback.confirmAction({
`The agent's SDR device is currently running: ${modeList}\n\n` + title: 'SDR Device Conflict',
`Starting ${modeToStart} on the same device will fail.\n\n` + message: `The agent's SDR device is currently running: ${modeList}. Starting ${modeToStart} on the same device will fail. Do you want to stop the conflicting mode(s) first?`,
`Do you want to stop the conflicting mode(s) first?` confirmLabel: 'Stop & Continue',
); confirmClass: 'btn-danger'
});
if (proceed) { if (proceed) {
// Stop conflicting modes // Stop conflicting modes

View File

@@ -269,8 +269,14 @@ const AlertCenter = (function() {
}); });
} }
function deleteRule(ruleId) { async function deleteRule(ruleId) {
if (!confirm('Delete this alert rule?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Alert Rule',
message: 'Delete this alert rule?',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
fetch(`/alerts/rules/${ruleId}`, { method: 'DELETE' }) fetch(`/alerts/rules/${ruleId}`, { method: 'DELETE' })
.then((r) => r.json()) .then((r) => r.json())

View File

@@ -120,19 +120,19 @@ function switchMode(mode) {
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations'); document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic'); document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
// Toggle stats visibility // Toggle stats visibility via class
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none'; document.getElementById('pagerStats')?.classList.toggle('active', mode === 'pager');
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none'; document.getElementById('sensorStats')?.classList.toggle('active', mode === 'sensor');
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none'; document.getElementById('aircraftStats')?.classList.toggle('active', mode === 'aircraft');
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none'; document.getElementById('satelliteStats')?.classList.toggle('active', mode === 'satellite');
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none'; document.getElementById('wifiStats')?.classList.toggle('active', mode === 'wifi');
// Hide signal meter - individual panels show signal strength where needed // Hide signal meter
document.getElementById('signalMeter').style.display = 'none'; document.getElementById('signalMeter')?.classList.remove('active');
// Show/hide dashboard buttons in nav bar // Show/hide dashboard buttons in nav bar
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none'; document.getElementById('adsbDashboardBtn')?.classList.toggle('active', mode === 'aircraft');
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none'; document.getElementById('satelliteDashboardBtn')?.classList.toggle('active', mode === 'satellite');
// Update active mode indicator // Update active mode indicator
const modeNames = { const modeNames = {
@@ -156,14 +156,14 @@ function switchMode(mode) {
window.closeMobileDrawer(); window.closeMobileDrawer();
} }
// Toggle layout containers // Toggle layout containers via class
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none'; document.getElementById('wifiLayoutContainer')?.classList.toggle('active', mode === 'wifi');
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none'; document.getElementById('btLayoutContainer')?.classList.toggle('active', mode === 'bluetooth');
// Respect the "Show Radar Display" checkbox for aircraft mode // Respect the "Show Radar Display" checkbox for aircraft mode
const showRadar = document.getElementById('adsbEnableMap')?.checked; const showRadar = document.getElementById('adsbEnableMap')?.checked;
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none'; document.getElementById('aircraftVisuals')?.classList.toggle('active', mode === 'aircraft' && showRadar);
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none'; document.getElementById('satelliteVisuals')?.classList.toggle('active', mode === 'satellite');
// Update output panel title based on mode // Update output panel title based on mode
const titles = { const titles = {
@@ -178,35 +178,30 @@ function switchMode(mode) {
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor'; document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
// Show/hide Device Intelligence for modes that use it // Show/hide Device Intelligence for modes that use it
const hideRecon = (mode === 'satellite' || mode === 'aircraft');
const reconBtn = document.getElementById('reconBtn'); const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]'); const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
if (mode === 'satellite' || mode === 'aircraft') { document.getElementById('reconPanel')?.classList.toggle('active', !hideRecon && typeof reconEnabled !== 'undefined' && reconEnabled);
document.getElementById('reconPanel').style.display = 'none'; if (reconBtn) reconBtn.classList.toggle('hidden', hideRecon);
if (reconBtn) reconBtn.style.display = 'none'; if (intelBtn) intelBtn.classList.toggle('hidden', hideRecon);
if (intelBtn) intelBtn.style.display = 'none';
} else {
if (reconBtn) reconBtn.style.display = 'inline-block';
if (intelBtn) intelBtn.style.display = 'inline-block';
if (typeof reconEnabled !== 'undefined' && reconEnabled) {
document.getElementById('reconPanel').style.display = 'block';
}
}
// Show RTL-SDR device section for modes that use it // Show RTL-SDR device section for modes that use it
document.getElementById('rtlDeviceSection').style.display = const showRtl = (mode === 'pager' || mode === 'sensor' || mode === 'aircraft');
(mode === 'pager' || mode === 'sensor' || mode === 'aircraft') ? 'block' : 'none'; document.getElementById('rtlDeviceSection')?.classList.toggle('active', showRtl);
// Toggle mode-specific tool status displays // Toggle mode-specific tool status displays
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none'; document.getElementById('toolStatusPager')?.classList.toggle('active', mode === 'pager');
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none'; document.getElementById('toolStatusSensor')?.classList.toggle('active', mode === 'sensor');
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none'; document.getElementById('toolStatusAircraft')?.classList.toggle('active', mode === 'aircraft');
// Hide waterfall and output console for modes with their own visualizations // Hide waterfall and output console for modes with their own visualizations
document.querySelector('.waterfall-container').style.display = const fullVisualModes = ['satellite', 'aircraft', 'wifi', 'bluetooth', 'meshtastic', 'aprs', 'tscm', 'spystations'];
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block'; const hideConsole = fullVisualModes.includes(mode);
document.getElementById('output').style.display = document.querySelector('.waterfall-container')?.classList.toggle('active', !hideConsole);
(mode === 'satellite' || mode === 'aircraft' || mode === 'wifi' || mode === 'bluetooth' || mode === 'meshtastic' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations') ? 'none' : 'block'; document.getElementById('output')?.classList.toggle('active', !hideConsole);
document.querySelector('.status-bar').style.display = (mode === 'satellite' || mode === 'tscm' || mode === 'meshtastic' || mode === 'aprs' || mode === 'spystations') ? 'none' : 'flex';
const hideStatusBar = ['satellite', 'tscm', 'meshtastic', 'aprs', 'spystations'].includes(mode);
document.querySelector('.status-bar')?.classList.toggle('active', !hideStatusBar);
// Load interfaces and initialize visualizations when switching modes // Load interfaces and initialize visualizations when switching modes
if (mode === 'wifi') { if (mode === 'wifi') {

View File

@@ -0,0 +1,245 @@
/**
* SSEManager - Centralized Server-Sent Events connection manager
* Handles connection lifecycle, reconnection with exponential backoff,
* visibility-based pause/resume, and state change notifications.
*/
const SSEManager = (function() {
'use strict';
const STATES = {
CONNECTING: 'connecting',
OPEN: 'open',
RECONNECTING: 'reconnecting',
CLOSED: 'closed',
ERROR: 'error',
};
const BACKOFF_INITIAL = 1000;
const BACKOFF_MAX = 30000;
const BACKOFF_MULTIPLIER = 2;
/** @type {Map<string, ConnectionEntry>} */
const connections = new Map();
/**
* @typedef {Object} ConnectionEntry
* @property {string} key
* @property {string} url
* @property {EventSource|null} source
* @property {string} state
* @property {number} backoff
* @property {number|null} retryTimer
* @property {boolean} intentionallyClosed
* @property {Function|null} onMessage
* @property {Function|null} onStateChange
*/
function connect(key, url, options) {
const opts = options || {};
// Disconnect existing connection for this key
if (connections.has(key)) {
disconnect(key);
}
const entry = {
key: key,
url: url,
source: null,
state: STATES.CLOSED,
backoff: BACKOFF_INITIAL,
retryTimer: null,
intentionallyClosed: false,
onMessage: typeof opts.onMessage === 'function' ? opts.onMessage : null,
onStateChange: typeof opts.onStateChange === 'function' ? opts.onStateChange : null,
};
connections.set(key, entry);
openConnection(entry);
return entry;
}
function openConnection(entry) {
if (entry.intentionallyClosed) return;
setState(entry, entry.state === STATES.CLOSED ? STATES.CONNECTING : STATES.RECONNECTING);
try {
const source = new EventSource(entry.url);
entry.source = source;
source.onopen = function() {
entry.backoff = BACKOFF_INITIAL;
setState(entry, STATES.OPEN);
};
source.onmessage = function(event) {
if (entry.onMessage) {
try {
entry.onMessage(event);
} catch (err) {
console.debug('[SSEManager] onMessage error for ' + entry.key + ':', err);
}
}
};
source.onerror = function() {
// EventSource fires error on close and connection loss
if (entry.intentionallyClosed) return;
closeSource(entry);
setState(entry, STATES.ERROR);
scheduleReconnect(entry);
};
} catch (err) {
setState(entry, STATES.ERROR);
scheduleReconnect(entry);
}
}
function closeSource(entry) {
if (entry.source) {
entry.source.onopen = null;
entry.source.onmessage = null;
entry.source.onerror = null;
try { entry.source.close(); } catch (e) { /* ignore */ }
entry.source = null;
}
}
function scheduleReconnect(entry) {
if (entry.intentionallyClosed) return;
if (entry.retryTimer) return;
// Pause reconnection when tab is hidden
if (document.hidden) {
setState(entry, STATES.RECONNECTING);
return;
}
const delay = entry.backoff;
entry.backoff = Math.min(entry.backoff * BACKOFF_MULTIPLIER, BACKOFF_MAX);
setState(entry, STATES.RECONNECTING);
entry.retryTimer = window.setTimeout(function() {
entry.retryTimer = null;
if (!entry.intentionallyClosed) {
openConnection(entry);
}
}, delay);
}
function disconnect(key) {
const entry = connections.get(key);
if (!entry) return;
entry.intentionallyClosed = true;
if (entry.retryTimer) {
clearTimeout(entry.retryTimer);
entry.retryTimer = null;
}
closeSource(entry);
setState(entry, STATES.CLOSED);
connections.delete(key);
}
function disconnectAll() {
for (const key of Array.from(connections.keys())) {
disconnect(key);
}
}
function getState(key) {
const entry = connections.get(key);
return entry ? entry.state : STATES.CLOSED;
}
function getActiveKeys() {
const keys = [];
connections.forEach(function(entry, key) {
if (entry.state === STATES.OPEN) {
keys.push(key);
}
});
return keys;
}
function setState(entry, newState) {
if (entry.state === newState) return;
const oldState = entry.state;
entry.state = newState;
if (entry.onStateChange) {
try {
entry.onStateChange(newState, oldState, entry.key);
} catch (err) {
console.debug('[SSEManager] onStateChange error:', err);
}
}
// Update global indicator
updateGlobalIndicator();
}
// --- Global SSE Status Indicator ---
function updateGlobalIndicator() {
const dot = document.getElementById('sseStatusDot');
if (!dot) return;
let hasOpen = false;
let hasReconnecting = false;
let hasError = false;
connections.forEach(function(entry) {
if (entry.state === STATES.OPEN) hasOpen = true;
else if (entry.state === STATES.RECONNECTING || entry.state === STATES.CONNECTING) hasReconnecting = true;
else if (entry.state === STATES.ERROR) hasError = true;
});
// Remove all state classes
dot.classList.remove('online', 'warning', 'error', 'inactive');
if (connections.size === 0) {
dot.classList.add('inactive');
dot.setAttribute('data-tooltip', 'No active streams');
} else if (hasError && !hasOpen) {
dot.classList.add('error');
dot.setAttribute('data-tooltip', 'Stream connection error');
} else if (hasReconnecting) {
dot.classList.add('warning');
dot.setAttribute('data-tooltip', 'Reconnecting...');
} else if (hasOpen) {
dot.classList.add('online');
dot.setAttribute('data-tooltip', 'Streams connected');
} else {
dot.classList.add('inactive');
dot.setAttribute('data-tooltip', 'Streams idle');
}
}
// --- Visibility API: pause/resume reconnection ---
document.addEventListener('visibilitychange', function() {
if (document.hidden) return;
// Tab became visible — reconnect any entries that were waiting
connections.forEach(function(entry) {
if (!entry.intentionallyClosed && !entry.source && !entry.retryTimer) {
openConnection(entry);
}
});
});
return {
STATES: STATES,
connect: connect,
disconnect: disconnect,
disconnectAll: disconnectAll,
getState: getState,
getActiveKeys: getActiveKeys,
};
})();

View File

@@ -3,6 +3,7 @@ const AppFeedback = (function() {
let stackEl = null; let stackEl = null;
let nextToastId = 1; let nextToastId = 1;
const TOAST_MAX = 5;
function init() { function init() {
ensureStack(); ensureStack();
@@ -17,6 +18,8 @@ const AppFeedback = (function() {
stackEl = document.createElement('div'); stackEl = document.createElement('div');
stackEl.id = 'appToastStack'; stackEl.id = 'appToastStack';
stackEl.className = 'app-toast-stack'; stackEl.className = 'app-toast-stack';
stackEl.setAttribute('aria-live', 'assertive');
stackEl.setAttribute('role', 'alert');
document.body.appendChild(stackEl); document.body.appendChild(stackEl);
} }
return stackEl; return stackEl;
@@ -64,7 +67,14 @@ const AppFeedback = (function() {
root.appendChild(actionsEl); root.appendChild(actionsEl);
} }
ensureStack().appendChild(root); const stack = ensureStack();
// Enforce toast cap — remove oldest when exceeded
while (stack.children.length >= TOAST_MAX) {
stack.removeChild(stack.firstChild);
}
stack.appendChild(root);
if (durationMs > 0) { if (durationMs > 0) {
window.setTimeout(() => { window.setTimeout(() => {
@@ -240,6 +250,151 @@ const AppFeedback = (function() {
return text.includes('permission') || text.includes('denied') || text.includes('dependency') || text.includes('tool'); return text.includes('permission') || text.includes('denied') || text.includes('dependency') || text.includes('tool');
} }
// --- Button loading state ---
function withLoadingButton(btn, asyncFn) {
if (!btn || btn.disabled) return Promise.resolve();
const originalText = btn.textContent;
btn.disabled = true;
btn.classList.add('btn-loading');
return Promise.resolve()
.then(function() { return asyncFn(); })
.then(function(result) {
btn.disabled = false;
btn.classList.remove('btn-loading');
btn.textContent = originalText;
return result;
})
.catch(function(err) {
btn.disabled = false;
btn.classList.remove('btn-loading');
btn.textContent = originalText;
throw err;
});
}
// --- Confirmation modal ---
function confirmAction(options) {
var opts = options || {};
var title = opts.title || 'Confirm Action';
var message = opts.message || 'Are you sure?';
var confirmLabel = opts.confirmLabel || 'Confirm';
var confirmClass = opts.confirmClass || 'btn-danger';
return new Promise(function(resolve) {
// Create backdrop
var backdrop = document.createElement('div');
backdrop.className = 'confirm-modal-backdrop';
var modal = document.createElement('div');
modal.className = 'confirm-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'confirm-modal-title');
var titleEl = document.createElement('div');
titleEl.className = 'confirm-modal-title';
titleEl.id = 'confirm-modal-title';
titleEl.textContent = title;
modal.appendChild(titleEl);
var msgEl = document.createElement('div');
msgEl.className = 'confirm-modal-message';
msgEl.textContent = message;
modal.appendChild(msgEl);
var actions = document.createElement('div');
actions.className = 'confirm-modal-actions';
var cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'btn btn-ghost';
cancelBtn.textContent = 'Cancel';
var confirmBtn = document.createElement('button');
confirmBtn.type = 'button';
confirmBtn.className = 'btn ' + confirmClass;
confirmBtn.textContent = confirmLabel;
actions.appendChild(cancelBtn);
actions.appendChild(confirmBtn);
modal.appendChild(actions);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Focus confirm button
confirmBtn.focus();
function cleanup(result) {
backdrop.remove();
document.removeEventListener('keydown', onKey);
resolve(result);
}
function onKey(e) {
if (e.key === 'Escape') cleanup(false);
if (e.key === 'Enter') cleanup(true);
}
cancelBtn.addEventListener('click', function() { cleanup(false); });
confirmBtn.addEventListener('click', function() { cleanup(true); });
backdrop.addEventListener('click', function(e) {
if (e.target === backdrop) cleanup(false);
});
document.addEventListener('keydown', onKey);
});
}
// --- Keyboard navigation for lists ---
function enableListKeyNav(container, itemSelector) {
if (!container) return;
container.setAttribute('role', 'listbox');
container.setAttribute('tabindex', '0');
container.addEventListener('keydown', function(e) {
var items = container.querySelectorAll(itemSelector);
if (!items.length) return;
var current = container.querySelector(itemSelector + '[aria-selected="true"]');
var idx = current ? Array.prototype.indexOf.call(items, current) : -1;
if (e.key === 'ArrowDown') {
e.preventDefault();
var next = Math.min(idx + 1, items.length - 1);
selectItem(items, next);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
var prev = Math.max(idx - 1, 0);
selectItem(items, prev);
} else if (e.key === 'Enter' && current) {
e.preventDefault();
current.click();
} else if (e.key === 'Escape' && current) {
e.preventDefault();
current.setAttribute('aria-selected', 'false');
current.classList.remove('keyboard-focused');
}
});
function selectItem(items, index) {
items.forEach(function(item) {
item.setAttribute('aria-selected', 'false');
item.classList.remove('keyboard-focused');
});
var target = items[index];
if (target) {
target.setAttribute('aria-selected', 'true');
target.classList.add('keyboard-focused');
target.scrollIntoView({ block: 'nearest' });
}
}
}
return { return {
init, init,
toast, toast,
@@ -249,6 +404,9 @@ const AppFeedback = (function() {
isOffline, isOffline,
isTransientNetworkError, isTransientNetworkError,
isTransientOrOffline, isTransientOrOffline,
withLoadingButton,
confirmAction,
enableListKeyNav,
}; };
})(); })();

View File

@@ -75,12 +75,12 @@ const BluetoothMode = (function() {
/** /**
* Check for agent mode conflicts before starting scan. * Check for agent mode conflicts before starting scan.
*/ */
function checkAgentConflicts() { async function checkAgentConflicts() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') { if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return true; return true;
} }
if (typeof checkAgentModeConflict === 'function') { if (typeof checkAgentModeConflict === 'function') {
return checkAgentModeConflict('bluetooth'); return await checkAgentModeConflict('bluetooth');
} }
return true; return true;
} }
@@ -883,7 +883,7 @@ const BluetoothMode = (function() {
async function startScan() { async function startScan() {
// Check for agent mode conflicts // Check for agent mode conflicts
if (!checkAgentConflicts()) { if (!await checkAgentConflicts()) {
return; return;
} }
@@ -940,7 +940,9 @@ const BluetoothMode = (function() {
} catch (err) { } catch (err) {
console.error('Failed to start scan:', err); console.error('Failed to start scan:', err);
showErrorMessage('Failed to start scan: ' + err.message); reportActionableError('Start Bluetooth Scan', err, {
onRetry: () => startScan()
});
} }
} }
@@ -968,6 +970,7 @@ const BluetoothMode = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to stop scan:', err); console.error('Failed to stop scan:', err);
reportActionableError('Stop Bluetooth Scan', err);
} finally { } finally {
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -1537,6 +1540,9 @@ const BluetoothMode = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to set baseline:', err); console.error('Failed to set baseline:', err);
reportActionableError('Set Baseline', err, {
onRetry: () => setBaseline()
});
} }
} }
@@ -1552,6 +1558,9 @@ const BluetoothMode = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to clear baseline:', err); console.error('Failed to clear baseline:', err);
reportActionableError('Clear Baseline', err, {
onRetry: () => clearBaseline()
});
} }
} }

View File

@@ -266,8 +266,10 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to start Meshtastic:', err); console.error('Failed to start Meshtastic:', err);
reportActionableError('Start Meshtastic', err, {
onRetry: () => start()
});
updateStatusIndicator('disconnected', 'Connection error'); updateStatusIndicator('disconnected', 'Connection error');
showStatusMessage('Connection error: ' + err.message, 'error');
} }
} }
@@ -283,6 +285,7 @@ const Meshtastic = (function() {
showNotification('Meshtastic', 'Disconnected'); showNotification('Meshtastic', 'Disconnected');
} catch (err) { } catch (err) {
console.error('Failed to stop Meshtastic:', err); console.error('Failed to stop Meshtastic:', err);
reportActionableError('Stop Meshtastic', err);
} }
} }
@@ -589,7 +592,9 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to configure channel:', err); console.error('Failed to configure channel:', err);
showStatusMessage('Error configuring channel: ' + err.message, 'error'); reportActionableError('Configure Channel', err, {
onRetry: () => saveChannel()
});
} }
} }
@@ -1246,11 +1251,11 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to send message:', err); console.error('Failed to send message:', err);
reportActionableError('Send Message', err, {
onRetry: () => sendMessage()
});
optimisticMsg._failed = true; optimisticMsg._failed = true;
updatePendingMessage(optimisticMsg, true); updatePendingMessage(optimisticMsg, true);
if (typeof showNotification === 'function') {
showNotification('Meshtastic', 'Send error: ' + err.message);
}
} finally { } finally {
if (sendBtn) { if (sendBtn) {
sendBtn.disabled = false; sendBtn.disabled = false;
@@ -1382,6 +1387,9 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Traceroute error:', err); console.error('Traceroute error:', err);
reportActionableError('Send Traceroute', err, {
onRetry: () => sendTraceroute(destination)
});
showTracerouteModal(destination, { error: err.message }, false); showTracerouteModal(destination, { error: err.message }, false);
} }
} }
@@ -1564,7 +1572,9 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Position request error:', err); console.error('Position request error:', err);
showStatusMessage('Error requesting position: ' + err.message, 'error'); reportActionableError('Request Position', err, {
onRetry: () => requestPosition(nodeId)
});
} }
} }
@@ -2085,7 +2095,9 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('Range test error:', err); console.error('Range test error:', err);
showStatusMessage('Error starting range test: ' + err.message, 'error'); reportActionableError('Start Range Test', err, {
onRetry: () => startRangeTest()
});
} }
} }
@@ -2099,6 +2111,7 @@ const Meshtastic = (function() {
showNotification('Meshtastic', 'Range test stopped'); showNotification('Meshtastic', 'Range test stopped');
} catch (err) { } catch (err) {
console.error('Error stopping range test:', err); console.error('Error stopping range test:', err);
reportActionableError('Stop Range Test', err);
} }
} }
@@ -2243,7 +2256,9 @@ const Meshtastic = (function() {
} }
} catch (err) { } catch (err) {
console.error('S&F request error:', err); console.error('S&F request error:', err);
showStatusMessage('Error: ' + err.message, 'error'); reportActionableError('Request Store & Forward History', err, {
onRetry: () => requestStoreForwardHistory()
});
} }
} }

View File

@@ -498,15 +498,27 @@ var OokMode = (function () {
input.value = ''; input.value = '';
} }
function removePreset(freq) { async function removePreset(freq) {
if (!confirm('Remove preset ' + freq + ' MHz?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Remove Preset',
message: 'Remove preset ' + freq + ' MHz?',
confirmLabel: 'Remove',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
var presets = loadPresets().filter(function (p) { return p !== freq; }); var presets = loadPresets().filter(function (p) { return p !== freq; });
savePresets(presets); savePresets(presets);
renderPresets(); renderPresets();
} }
function resetPresets() { async function resetPresets() {
if (!confirm('Reset to default presets?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Reset Presets',
message: 'Reset to default presets?',
confirmLabel: 'Reset',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
savePresets(DEFAULT_FREQ_PRESETS.slice()); savePresets(DEFAULT_FREQ_PRESETS.slice());
renderPresets(); renderPresets();
} }

View File

@@ -802,7 +802,13 @@ const SSTVGeneral = (function() {
* Delete a single image * Delete a single image
*/ */
async function deleteImage(filename) { async function deleteImage(filename) {
if (!confirm('Delete this image?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Image',
message: 'Delete this image? This cannot be undone.',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch(`/sstv-general/images/${encodeURIComponent(filename)}`, { method: 'DELETE' }); const response = await fetch(`/sstv-general/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
const data = await response.json(); const data = await response.json();
@@ -822,7 +828,13 @@ const SSTVGeneral = (function() {
* Delete all images * Delete all images
*/ */
async function deleteAllImages() { async function deleteAllImages() {
if (!confirm('Delete all decoded images?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete All Images',
message: 'Delete all decoded images? This cannot be undone.',
confirmLabel: 'Delete All',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch('/sstv-general/images', { method: 'DELETE' }); const response = await fetch('/sstv-general/images', { method: 'DELETE' });
const data = await response.json(); const data = await response.json();

View File

@@ -606,8 +606,10 @@ const SSTV = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to start SSTV:', err); console.error('Failed to start SSTV:', err);
reportActionableError('Start SSTV', err, {
onRetry: () => start()
});
updateStatusUI('idle', 'Error'); updateStatusUI('idle', 'Error');
showStatusMessage('Connection error: ' + err.message, 'error');
} }
} }
@@ -626,6 +628,7 @@ const SSTV = (function() {
showNotification('SSTV', 'Decoder stopped'); showNotification('SSTV', 'Decoder stopped');
} catch (err) { } catch (err) {
console.error('Failed to stop SSTV:', err); console.error('Failed to stop SSTV:', err);
reportActionableError('Stop SSTV', err);
} }
} }
@@ -1297,7 +1300,13 @@ const SSTV = (function() {
* Delete a single image * Delete a single image
*/ */
async function deleteImage(filename) { async function deleteImage(filename) {
if (!confirm('Delete this image?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Image',
message: 'Delete this image? This cannot be undone.',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch(`/sstv/images/${encodeURIComponent(filename)}`, { method: 'DELETE' }); const response = await fetch(`/sstv/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
const data = await response.json(); const data = await response.json();
@@ -1310,6 +1319,7 @@ const SSTV = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to delete image:', err); console.error('Failed to delete image:', err);
reportActionableError('Delete Image', err);
} }
} }
@@ -1317,7 +1327,13 @@ const SSTV = (function() {
* Delete all images * Delete all images
*/ */
async function deleteAllImages() { async function deleteAllImages() {
if (!confirm('Delete all decoded images?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete All Images',
message: 'Delete all decoded images? This cannot be undone.',
confirmLabel: 'Delete All',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch('/sstv/images', { method: 'DELETE' }); const response = await fetch('/sstv/images', { method: 'DELETE' });
const data = await response.json(); const data = await response.json();
@@ -1329,6 +1345,7 @@ const SSTV = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to delete images:', err); console.error('Failed to delete images:', err);
reportActionableError('Delete All Images', err);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -291,8 +291,10 @@ const WeatherSat = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to start weather sat:', err); console.error('Failed to start weather sat:', err);
reportActionableError('Start Weather Satellite', err, {
onRetry: () => start()
});
updateStatusUI('idle', 'Error'); updateStatusUI('idle', 'Error');
showNotification('Weather Sat', 'Connection error');
} }
} }
@@ -322,6 +324,7 @@ const WeatherSat = (function() {
showNotification('Weather Sat', 'Capture stopped'); showNotification('Weather Sat', 'Capture stopped');
} catch (err) { } catch (err) {
console.error('Failed to stop weather sat:', err); console.error('Failed to stop weather sat:', err);
reportActionableError('Stop Weather Satellite', err);
} }
} }
@@ -375,8 +378,10 @@ const WeatherSat = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to start test decode:', err); console.error('Failed to start test decode:', err);
reportActionableError('Start Test Decode', err, {
onRetry: () => testDecode()
});
updateStatusUI('idle', 'Error'); updateStatusUI('idle', 'Error');
showNotification('Weather Sat', 'Connection error');
} }
} }
@@ -1439,9 +1444,11 @@ const WeatherSat = (function() {
showNotification('Weather Sat', `Auto-scheduler enabled (${data.scheduled_count || 0} passes)`); showNotification('Weather Sat', `Auto-scheduler enabled (${data.scheduled_count || 0} passes)`);
} catch (err) { } catch (err) {
console.error('Failed to enable scheduler:', err); console.error('Failed to enable scheduler:', err);
reportActionableError('Enable Scheduler', err, {
onRetry: () => enableScheduler()
});
schedulerEnabled = false; schedulerEnabled = false;
updateSchedulerUI({ enabled: false, scheduled_count: 0 }); updateSchedulerUI({ enabled: false, scheduled_count: 0 });
showNotification('Weather Sat', 'Failed to enable auto-scheduler');
} }
} }
@@ -1461,6 +1468,7 @@ const WeatherSat = (function() {
showNotification('Weather Sat', 'Auto-scheduler disabled'); showNotification('Weather Sat', 'Auto-scheduler disabled');
} catch (err) { } catch (err) {
console.error('Failed to disable scheduler:', err); console.error('Failed to disable scheduler:', err);
reportActionableError('Disable Scheduler', err);
} }
} }
@@ -1649,7 +1657,13 @@ const WeatherSat = (function() {
*/ */
async function deleteImage(filename) { async function deleteImage(filename) {
if (!filename) return; if (!filename) return;
if (!confirm(`Delete this image?`)) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Image',
message: 'Delete this image? This cannot be undone.',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch(`/weather-sat/images/${encodeURIComponent(filename)}`, { method: 'DELETE' }); const response = await fetch(`/weather-sat/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
@@ -1668,7 +1682,7 @@ const WeatherSat = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to delete image:', err); console.error('Failed to delete image:', err);
showNotification('Weather Sat', 'Failed to delete image'); reportActionableError('Delete Image', err);
} }
} }
@@ -1677,7 +1691,13 @@ const WeatherSat = (function() {
*/ */
async function deleteAllImages() { async function deleteAllImages() {
if (images.length === 0) return; if (images.length === 0) return;
if (!confirm(`Delete all ${images.length} decoded images?`)) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete All Images',
message: `Delete all ${images.length} decoded images? This cannot be undone.`,
confirmLabel: 'Delete All',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch('/weather-sat/images', { method: 'DELETE' }); const response = await fetch('/weather-sat/images', { method: 'DELETE' });
@@ -1693,7 +1713,7 @@ const WeatherSat = (function() {
} }
} catch (err) { } catch (err) {
console.error('Failed to delete all images:', err); console.error('Failed to delete all images:', err);
showNotification('Weather Sat', 'Failed to delete images'); reportActionableError('Delete All Images', err);
} }
} }

View File

@@ -242,6 +242,7 @@ var WeFax = (function () {
.catch(function (err) { .catch(function (err) {
setStatus('Stopped'); setStatus('Stopped');
console.error('WeFax stop error:', err); console.error('WeFax stop error:', err);
reportActionableError('Stop WeFax', err);
}); });
} }
@@ -626,9 +627,15 @@ var WeFax = (function () {
gallery.innerHTML = html; gallery.innerHTML = html;
} }
function deleteImage(filename) { async function deleteImage(filename) {
if (!filename) return; if (!filename) return;
if (!confirm('Delete this image?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Image',
message: 'Delete this image? This cannot be undone.',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
fetch('/wefax/images/' + encodeURIComponent(filename), { method: 'DELETE' }) fetch('/wefax/images/' + encodeURIComponent(filename), { method: 'DELETE' })
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
@@ -641,12 +648,18 @@ var WeFax = (function () {
}) })
.catch(function (err) { .catch(function (err) {
console.error('WeFax delete error:', err); console.error('WeFax delete error:', err);
setStatus('Delete failed: ' + err.message); reportActionableError('Delete Image', err);
}); });
} }
function deleteAllImages() { async function deleteAllImages() {
if (!confirm('Delete all WeFax images?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete All Images',
message: 'Delete all WeFax images? This cannot be undone.',
confirmLabel: 'Delete All',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
fetch('/wefax/images', { method: 'DELETE' }) fetch('/wefax/images', { method: 'DELETE' })
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
@@ -654,7 +667,10 @@ var WeFax = (function () {
loadImages(); loadImages();
} }
}) })
.catch(function (err) { console.error('WeFax delete all error:', err); }); .catch(function (err) {
console.error('WeFax delete all error:', err);
reportActionableError('Delete All Images', err);
});
} }
var currentModalUrl = null; var currentModalUrl = null;
@@ -1107,6 +1123,7 @@ var WeFax = (function () {
}) })
.catch(function (err) { .catch(function (err) {
console.error('WeFax scheduler disable error:', err); console.error('WeFax scheduler disable error:', err);
reportActionableError('Disable Scheduler', err);
}); });
} }

View File

@@ -59,12 +59,12 @@ const WiFiMode = (function() {
/** /**
* Check for agent mode conflicts before starting WiFi scan. * Check for agent mode conflicts before starting WiFi scan.
*/ */
function checkAgentConflicts() { async function checkAgentConflicts() {
if (typeof currentAgent === 'undefined' || currentAgent === 'local') { if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
return true; return true;
} }
if (typeof checkAgentModeConflict === 'function') { if (typeof checkAgentModeConflict === 'function') {
return checkAgentModeConflict('wifi'); return await checkAgentModeConflict('wifi');
} }
return true; return true;
} }
@@ -411,7 +411,7 @@ const WiFiMode = (function() {
if (isScanning) return; if (isScanning) return;
// Check for agent mode conflicts // Check for agent mode conflicts
if (!checkAgentConflicts()) { if (!await checkAgentConflicts()) {
return; return;
} }
@@ -503,7 +503,7 @@ const WiFiMode = (function() {
if (isScanning) return; if (isScanning) return;
// Check for agent mode conflicts // Check for agent mode conflicts
if (!checkAgentConflicts()) { if (!await checkAgentConflicts()) {
return; return;
} }

View File

@@ -21,7 +21,7 @@
<!-- Core CSS variables --> <!-- Core CSS variables -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_dashboard.css') }}">
@@ -2162,7 +2162,7 @@ sudo make install</code>
if (remoteConfig === false) return; if (remoteConfig === false) return;
// Check for agent SDR conflicts // Check for agent SDR conflicts
if (useAgent && typeof checkAgentModeConflict === 'function') { if (useAgent && typeof checkAgentModeConflict === 'function') {
if (!checkAgentModeConflict('adsb')) { if (!await checkAgentModeConflict('adsb')) {
return; // User cancelled or conflict not resolved return; // User cancelled or conflict not resolved
} }
} }
@@ -3661,11 +3661,12 @@ sudo make install</code>
// Check if ADS-B tracking is using this device // Check if ADS-B tracking is using this device
if (isTracking && adsbActiveDevice !== null && device === adsbActiveDevice) { if (isTracking && adsbActiveDevice !== null && device === adsbActiveDevice) {
const useAnyway = confirm( const useAnyway = await AppFeedback.confirmAction({
`Warning: ADS-B tracking is using SDR ${adsbActiveDevice}.\n\n` + title: 'SDR Device Conflict',
'Using the same device for airband will stop ADS-B tracking.\n\n' + message: `ADS-B tracking is using SDR ${adsbActiveDevice}. Using the same device for airband will stop ADS-B tracking. Select a different SDR device for airband listening, or continue to stop tracking and listen.`,
'Select a different SDR device for airband listening, or click OK to stop tracking and listen.' confirmLabel: 'Continue',
); confirmClass: 'btn-danger'
});
if (!useAnyway) { if (!useAnyway) {
return; return;
} }
@@ -3900,7 +3901,7 @@ sudo make install</code>
} }
} }
function startAcars() { async function startAcars() {
const acarsSelect = document.getElementById('acarsDeviceSelect'); const acarsSelect = document.getElementById('acarsDeviceSelect');
const compositeVal = acarsSelect.value || 'rtlsdr:0'; const compositeVal = acarsSelect.value || 'rtlsdr:0';
const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal]; const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal];
@@ -3913,12 +3914,12 @@ sudo make install</code>
// Warn if using same device as ADS-B (only for local mode) // Warn if using same device as ADS-B (only for local mode)
if (!isAgentMode && isTracking && adsbActiveDevice !== null && device === String(adsbActiveDevice)) { if (!isAgentMode && isTracking && adsbActiveDevice !== null && device === String(adsbActiveDevice)) {
const useAnyway = confirm( const useAnyway = await AppFeedback.confirmAction({
`Warning: ADS-B tracking is using SDR device ${adsbActiveDevice}.\n\n` + title: 'SDR Device Conflict',
'ACARS uses VHF frequencies (129-131 MHz) while ADS-B uses 1090 MHz.\n' + message: `ADS-B tracking is using SDR device ${adsbActiveDevice}. ACARS uses VHF frequencies (129-131 MHz) while ADS-B uses 1090 MHz. You need TWO separate SDR devices to receive both simultaneously. Continue to start ACARS on device ${device} anyway.`,
'You need TWO separate SDR devices to receive both simultaneously.\n\n' + confirmLabel: 'Continue',
'Click OK to start ACARS on device ' + device + ' anyway.' confirmClass: 'btn-danger'
); });
if (!useAnyway) return; if (!useAnyway) return;
} }
@@ -4348,7 +4349,7 @@ sudo make install</code>
} }
} }
function startVdl2() { async function startVdl2() {
const vdl2Select = document.getElementById('vdl2DeviceSelect'); const vdl2Select = document.getElementById('vdl2DeviceSelect');
const compositeVal = vdl2Select.value || 'rtlsdr:0'; const compositeVal = vdl2Select.value || 'rtlsdr:0';
const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal]; const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal];
@@ -4361,12 +4362,12 @@ sudo make install</code>
// Warn if using same device as ADS-B (only for local mode) // Warn if using same device as ADS-B (only for local mode)
if (!isAgentMode && isTracking && adsbActiveDevice !== null && device === String(adsbActiveDevice)) { if (!isAgentMode && isTracking && adsbActiveDevice !== null && device === String(adsbActiveDevice)) {
const useAnyway = confirm( const useAnyway = await AppFeedback.confirmAction({
`Warning: ADS-B tracking is using SDR device ${adsbActiveDevice}.\n\n` + title: 'SDR Device Conflict',
'VDL2 uses VHF frequencies (~137 MHz) while ADS-B uses 1090 MHz.\n' + message: `ADS-B tracking is using SDR device ${adsbActiveDevice}. VDL2 uses VHF frequencies (~137 MHz) while ADS-B uses 1090 MHz. You need TWO separate SDR devices to receive both simultaneously. Continue to start VDL2 on device ${device} anyway.`,
'You need TWO separate SDR devices to receive both simultaneously.\n\n' + confirmLabel: 'Continue',
'Click OK to start VDL2 on device ' + device + ' anyway.' confirmClass: 'btn-danger'
); });
if (!useAnyway) return; if (!useAnyway) return;
} }

View File

@@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %} {% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
@@ -608,7 +608,12 @@
} }
const dayEndLocal = new Date(dayStartLocal.getTime() + (24 * 60 * 60 * 1000)); const dayEndLocal = new Date(dayStartLocal.getTime() + (24 * 60 * 60 * 1000));
const dayLabel = dayStartLocal.toLocaleDateString(); const dayLabel = dayStartLocal.toLocaleDateString();
const confirmed = window.confirm(`Delete ADS-B history for ${dayLabel}? This cannot be undone.`); const confirmed = await AppFeedback.confirmAction({
title: 'Delete Day History',
message: `Delete ADS-B history for ${dayLabel}? This cannot be undone.`,
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) { if (!confirmed) {
return; return;
} }
@@ -623,7 +628,12 @@
} }
async function clearAllHistory() { async function clearAllHistory() {
const confirmed = window.confirm('Delete ALL ADS-B history records? This cannot be undone.'); const confirmed = await AppFeedback.confirmAction({
title: 'Delete All History',
message: 'Delete ALL ADS-B history records? This cannot be undone.',
confirmLabel: 'Delete All',
confirmClass: 'btn-danger'
});
if (!confirmed) { if (!confirmed) {
return; return;
} }

View File

@@ -9,7 +9,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}"> <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/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
@@ -514,7 +514,13 @@
} }
async function deleteAgent(agentId, agentName) { async function deleteAgent(agentId, agentName) {
if (!confirm(`Are you sure you want to remove agent "${agentName}"?`)) { const confirmed = await AppFeedback.confirmAction({
title: 'Remove Agent',
message: `Are you sure you want to remove agent "${agentName}"?`,
confirmLabel: 'Remove',
confirmClass: 'btn-danger'
});
if (!confirmed) {
return; return;
} }

View File

@@ -22,7 +22,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<script> <script>
@@ -539,7 +539,7 @@
}); });
} }
function toggleTracking() { async function toggleTracking() {
if (isTracking) { if (isTracking) {
stopTracking(); stopTracking();
} else { } else {
@@ -556,7 +556,7 @@
// For agent mode, check conflicts and route through proxy // For agent mode, check conflicts and route through proxy
if (useAgent) { if (useAgent) {
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) { if (typeof checkAgentModeConflict === 'function' && !await checkAgentModeConflict('ais')) {
return; return;
} }
@@ -1651,12 +1651,12 @@
// Override startTracking for agent support // Override startTracking for agent support
const originalStartTracking = startTracking; const originalStartTracking = startTracking;
startTracking = function() { startTracking = async function() {
const useAgent = aisCurrentAgent !== 'local'; const useAgent = aisCurrentAgent !== 'local';
if (useAgent) { if (useAgent) {
// Check for conflicts // Check for conflicts
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('ais')) { if (typeof checkAgentModeConflict === 'function' && !await checkAgentModeConflict('ais')) {
return; return;
} }

View File

@@ -55,7 +55,8 @@
<!-- Chart.js date adapter for time-scale axes --> <!-- Chart.js date adapter for time-scale axes -->
<script src="{{ url_for('static', filename='vendor/chartjs/chartjs-adapter-date-fns.bundle.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/chartjs/chartjs-adapter-date-fns.bundle.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-timeline.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-timeline.css') }}">
@@ -666,16 +667,14 @@
</div> </div>
</div> </div>
<div id="toolStatusPager" class="info-text tool-status-section" <div id="toolStatusPager" class="info-text tool-status-section">
style="display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
<span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' <span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK'
if tools.rtl_fm else 'Missing' }}</span> if tools.rtl_fm else 'Missing' }}</span>
<span>multimon-ng:</span><span <span>multimon-ng:</span><span
class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon
else 'Missing' }}</span> else 'Missing' }}</span>
</div> </div>
<div id="toolStatusSensor" class="info-text tool-status-section" <div id="toolStatusSensor" class="info-text tool-status-section">
style="display: none; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
<span>rtl_433:</span><span class="tool-status {{ 'ok' if tools.rtl_433 else 'missing' }}">{{ <span>rtl_433:</span><span class="tool-status {{ 'ok' if tools.rtl_433 else 'missing' }}">{{
'OK' if tools.rtl_433 else 'Missing' }}</span> 'OK' if tools.rtl_433 else 'Missing' }}</span>
</div> </div>
@@ -751,11 +750,11 @@
<div title="POCSAG Messages"><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> <span id="pocsagCount">0</span></div> <div title="POCSAG Messages"><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> <span id="pocsagCount">0</span></div>
<div title="FLEX Messages"><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> <span id="flexCount">0</span></div> <div title="FLEX Messages"><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> <span id="flexCount">0</span></div>
</div> </div>
<div class="stats" id="sensorStats" style="display: none;"> <div class="stats" id="sensorStats">
<div title="Unique Sensors"><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> <span id="sensorCount">0</span></div> <div title="Unique Sensors"><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> <span id="sensorCount">0</span></div>
<div title="Device Types"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span> <span id="deviceCount">0</span></div> <div title="Device Types"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span> <span id="deviceCount">0</span></div>
</div> </div>
<div class="stats" id="wifiStats" style="display: none;"> <div class="stats" id="wifiStats">
<div title="Access Points"><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> <span id="apCount">0</span></div> <div title="Access Points"><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> <span id="apCount">0</span></div>
<div title="Connected Clients"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span> <span id="clientCount">0</span></div> <div title="Connected Clients"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span> <span id="clientCount">0</span></div>
<div title="Captured Handshakes" style="color: var(--accent-green);"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/></svg></span> <span id="handshakeCount">0</span></div> <div title="Captured Handshakes" style="color: var(--accent-green);"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/></svg></span> <span id="handshakeCount">0</span></div>
@@ -764,14 +763,14 @@
<div style="color: var(--accent-red); cursor: pointer;" onclick="showRogueApDetails()" <div style="color: var(--accent-red); cursor: pointer;" onclick="showRogueApDetails()"
title="Click: Rogue AP details"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> <span id="rogueApCount">0</span></div> title="Click: Rogue AP details"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> <span id="rogueApCount">0</span></div>
</div> </div>
<div class="stats" id="satelliteStats" style="display: none;"> <div class="stats" id="satelliteStats">
<div title="Upcoming Passes"><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> <span id="passCount">0</span></div> <div title="Upcoming Passes"><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> <span id="passCount">0</span></div>
</div> </div>
</div> </div>
</div> </div>
<!-- WiFi Layout Container --> <!-- WiFi Layout Container -->
<div class="wifi-layout-container" id="wifiLayoutContainer" style="display: none;"> <div class="wifi-layout-container" id="wifiLayoutContainer">
<!-- Status Bar --> <!-- Status Bar -->
<div class="wifi-status-bar"> <div class="wifi-status-bar">
<div class="wifi-status-item"> <div class="wifi-status-item">
@@ -945,7 +944,7 @@
</div> </div>
<!-- Bluetooth Layout Container (visualizations left, device cards right) --> <!-- Bluetooth Layout Container (visualizations left, device cards right) -->
<div class="bt-layout-container" id="btLayoutContainer" style="display: none;"> <div class="bt-layout-container" id="btLayoutContainer">
<!-- Left: Bluetooth Visualizations --> <!-- Left: Bluetooth Visualizations -->
<div class="bt-visuals-column" id="btVisuals"> <div class="bt-visuals-column" id="btVisuals">
<!-- Device Detail Panel (always visible) --> <!-- Device Detail Panel (always visible) -->
@@ -1347,7 +1346,7 @@
</div> </div>
<!-- Satellite Dashboard (Embedded) --> <!-- Satellite Dashboard (Embedded) -->
<div id="satelliteVisuals" class="satellite-dashboard-embed" style="display: none;"> <div id="satelliteVisuals" class="satellite-dashboard-embed">
<iframe id="satelliteDashboardFrame" src="/satellite/dashboard?embedded=true" frameborder="0" <iframe id="satelliteDashboardFrame" src="/satellite/dashboard?embedded=true" frameborder="0"
style="width: 100%; height: 100%; min-height: 700px; border: none; border-radius: 8px;" style="width: 100%; height: 100%; min-height: 700px; border: none; border-radius: 8px;"
allowfullscreen> allowfullscreen>
@@ -4460,14 +4459,10 @@
document.getElementById('ookMode')?.classList.toggle('active', mode === 'ook'); document.getElementById('ookMode')?.classList.toggle('active', mode === 'ook');
const pagerStats = document.getElementById('pagerStats'); document.getElementById('pagerStats')?.classList.toggle('active', mode === 'pager');
const sensorStats = document.getElementById('sensorStats'); document.getElementById('sensorStats')?.classList.toggle('active', mode === 'sensor');
const satelliteStats = document.getElementById('satelliteStats'); document.getElementById('satelliteStats')?.classList.toggle('active', mode === 'satellite');
const wifiStats = document.getElementById('wifiStats'); document.getElementById('wifiStats')?.classList.toggle('active', mode === 'wifi');
if (pagerStats) pagerStats.style.display = mode === 'pager' ? 'flex' : 'none';
if (sensorStats) sensorStats.style.display = mode === 'sensor' ? 'flex' : 'none';
if (satelliteStats) satelliteStats.style.display = mode === 'satellite' ? 'flex' : 'none';
if (wifiStats) wifiStats.style.display = mode === 'wifi' ? 'flex' : 'none';
// Update header stats groups // Update header stats groups
document.getElementById('headerPagerStats')?.classList.toggle('active', mode === 'pager'); document.getElementById('headerPagerStats')?.classList.toggle('active', mode === 'pager');
@@ -4476,8 +4471,7 @@
document.getElementById('headerWifiStats')?.classList.toggle('active', mode === 'wifi'); document.getElementById('headerWifiStats')?.classList.toggle('active', mode === 'wifi');
// Show/hide dashboard buttons in nav bar // Show/hide dashboard buttons in nav bar
const satelliteDashboardBtn = document.getElementById('satelliteDashboardBtn'); document.getElementById('satelliteDashboardBtn')?.classList.toggle('active', mode === 'satellite');
if (satelliteDashboardBtn) satelliteDashboardBtn.style.display = mode === 'satellite' ? 'inline-flex' : 'none';
// Update active mode indicator // Update active mode indicator
const modeMeta = modeCatalog[mode] || {}; const modeMeta = modeCatalog[mode] || {};
@@ -4503,9 +4497,9 @@
const radiosondeVisuals = document.getElementById('radiosondeVisuals'); const radiosondeVisuals = document.getElementById('radiosondeVisuals');
const meteorVisuals = document.getElementById('meteorVisuals'); const meteorVisuals = document.getElementById('meteorVisuals');
const systemVisuals = document.getElementById('systemVisuals'); const systemVisuals = document.getElementById('systemVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none'; if (wifiLayoutContainer) wifiLayoutContainer.classList.toggle('active', mode === 'wifi');
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none'; if (btLayoutContainer) btLayoutContainer.classList.toggle('active', mode === 'bluetooth');
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none'; if (satelliteVisuals) satelliteVisuals.classList.toggle('active', mode === 'satellite');
const satFrame = document.getElementById('satelliteDashboardFrame'); const satFrame = document.getElementById('satelliteDashboardFrame');
if (satFrame && satFrame.contentWindow) { if (satFrame && satFrame.contentWindow) {
satFrame.contentWindow.postMessage({type: 'satellite-visibility', visible: mode === 'satellite'}, '*'); satFrame.contentWindow.postMessage({type: 'satellite-visibility', visible: mode === 'satellite'}, '*');
@@ -4584,18 +4578,10 @@
const reconBtn = document.getElementById('reconBtn'); const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]'); const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
const reconPanel = document.getElementById('reconPanel'); const reconPanel = document.getElementById('reconPanel');
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'gps' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall' || mode === 'meteor' || mode === 'system') { const hideRecon = ['satellite', 'sstv', 'weathersat', 'sstv_general', 'wefax', 'gps', 'aprs', 'tscm', 'spystations', 'meshtastic', 'websdr', 'subghz', 'spaceweather', 'waterfall', 'meteor', 'system'].includes(mode);
if (reconPanel) reconPanel.style.display = 'none'; if (reconPanel) reconPanel.classList.toggle('active', !hideRecon && reconEnabled);
if (reconBtn) reconBtn.style.display = 'none'; if (reconBtn) reconBtn.classList.toggle('hidden', hideRecon);
if (intelBtn) intelBtn.style.display = 'none'; if (intelBtn) intelBtn.classList.toggle('hidden', hideRecon);
} else {
if (reconBtn) reconBtn.style.display = 'inline-block';
if (intelBtn) intelBtn.style.display = 'inline-block';
// Restore panel visibility based on reconEnabled state
if (reconEnabled && reconPanel) {
reconPanel.style.display = 'block';
}
}
// Show agent selector for modes that support remote agents // Show agent selector for modes that support remote agents
const agentSection = document.getElementById('agentSection'); const agentSection = document.getElementById('agentSection');
@@ -4605,7 +4591,8 @@
// Show RTL-SDR device section for modes that use it // Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection'); const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) { if (rtlDeviceSection) {
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse' || mode === 'radiosonde' || mode === 'meteor' || mode === 'ook') ? 'block' : 'none'; const showRtl = ['pager', 'sensor', 'rtlamr', 'aprs', 'sstv', 'weathersat', 'sstv_general', 'wefax', 'morse', 'radiosonde', 'meteor', 'ook'].includes(mode);
rtlDeviceSection.classList.toggle('active', showRtl);
// Save original sidebar position of SDR device section (once) // Save original sidebar position of SDR device section (once)
if (!rtlDeviceSection._origParent) { if (!rtlDeviceSection._origParent) {
rtlDeviceSection._origParent = rtlDeviceSection.parentNode; rtlDeviceSection._origParent = rtlDeviceSection.parentNode;
@@ -4639,16 +4626,15 @@
} }
// Toggle mode-specific tool status displays // Toggle mode-specific tool status displays
const toolStatusPager = document.getElementById('toolStatusPager'); document.getElementById('toolStatusPager')?.classList.toggle('active', mode === 'pager');
const toolStatusSensor = document.getElementById('toolStatusSensor'); document.getElementById('toolStatusSensor')?.classList.toggle('active', mode === 'sensor');
if (toolStatusPager) toolStatusPager.style.display = (mode === 'pager') ? 'grid' : 'none';
if (toolStatusSensor) toolStatusSensor.style.display = (mode === 'sensor') ? 'grid' : 'none';
// Hide output console for modes with their own visualizations // Hide output console for modes with their own visualizations
const outputEl = document.getElementById('output'); const fullVisualModes = ['satellite', 'sstv', 'weathersat', 'sstv_general', 'wefax', 'aprs', 'wifi', 'bluetooth', 'tscm', 'spystations', 'meshtastic', 'websdr', 'subghz', 'spaceweather', 'bt_locate', 'waterfall', 'morse', 'meteor', 'system', 'ook'];
const statusBar = document.querySelector('.status-bar'); const hideConsole = fullVisualModes.includes(mode);
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'bt_locate' || mode === 'waterfall' || mode === 'morse' || mode === 'meteor' || mode === 'system' || mode === 'ook') ? 'none' : 'block'; document.getElementById('output')?.classList.toggle('active', !hideConsole);
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall' || mode === 'morse' || mode === 'meteor' || mode === 'system') ? 'none' : 'flex'; const hideStatusBar = ['satellite', 'websdr', 'subghz', 'spaceweather', 'waterfall', 'morse', 'meteor', 'system'].includes(mode);
document.querySelector('.status-bar')?.classList.toggle('active', !hideStatusBar);
// Restore sidebar when leaving Meshtastic mode (user may have collapsed it) // Restore sidebar when leaving Meshtastic mode (user may have collapsed it)
if (mode !== 'meshtastic') { if (mode !== 'meshtastic') {
@@ -5102,7 +5088,7 @@
} }
// Start sensor decoding // Start sensor decoding
function startSensorDecoding() { async function startSensorDecoding() {
const freq = document.getElementById('sensorFrequency').value; const freq = document.getElementById('sensorFrequency').value;
const gain = document.getElementById('sensorGain').value; const gain = document.getElementById('sensorGain').value;
const ppm = document.getElementById('sensorPpm').value; const ppm = document.getElementById('sensorPpm').value;
@@ -5111,7 +5097,7 @@
// Check if using remote agent // Check if using remote agent
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') { if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') {
// Check for conflicts with other running SDR modes // Check for conflicts with other running SDR modes
if (typeof checkAgentModeConflict === 'function' && !checkAgentModeConflict('sensor')) { if (typeof checkAgentModeConflict === 'function' && !await checkAgentModeConflict('sensor')) {
return; // User cancelled or conflict not resolved return; // User cancelled or conflict not resolved
} }
@@ -5146,7 +5132,7 @@
} }
// Check if device is available // Check if device is available
if (!checkDeviceAvailability('sensor')) { if (!await checkDeviceAvailability('sensor')) {
return; return;
} }
@@ -5462,7 +5448,7 @@
let rtlamrPollTimer = null; let rtlamrPollTimer = null;
let rtlamrCurrentAgent = null; let rtlamrCurrentAgent = null;
function startRtlamrDecoding() { async function startRtlamrDecoding() {
const freq = document.getElementById('rtlamrFrequency').value; const freq = document.getElementById('rtlamrFrequency').value;
const gain = document.getElementById('rtlamrGain').value; const gain = document.getElementById('rtlamrGain').value;
const ppm = document.getElementById('rtlamrPpm').value; const ppm = document.getElementById('rtlamrPpm').value;
@@ -5476,7 +5462,7 @@
rtlamrCurrentAgent = isAgentMode ? currentAgent : null; rtlamrCurrentAgent = isAgentMode ? currentAgent : null;
// Check if device is available (only for local mode) // Check if device is available (only for local mode)
if (!isAgentMode && !checkDeviceAvailability('rtlamr')) { if (!isAgentMode && !await checkDeviceAvailability('rtlamr')) {
return; return;
} }
@@ -5897,8 +5883,14 @@
input.value = ''; input.value = '';
} }
function removePreset(freq) { async function removePreset(freq) {
if (confirm('Remove preset ' + freq + ' MHz?')) { const confirmed = await AppFeedback.confirmAction({
title: 'Remove Preset',
message: 'Remove preset ' + freq + ' MHz?',
confirmLabel: 'Remove',
confirmClass: 'btn-danger'
});
if (confirmed) {
let presets = loadPresets(); let presets = loadPresets();
presets = presets.filter(p => p !== freq); presets = presets.filter(p => p !== freq);
savePresets(presets); savePresets(presets);
@@ -5906,8 +5898,14 @@
} }
} }
function resetPresets() { async function resetPresets() {
if (confirm('Reset to default presets?')) { const confirmed = await AppFeedback.confirmAction({
title: 'Reset Presets',
message: 'Reset to default presets?',
confirmLabel: 'Reset',
confirmClass: 'btn-danger'
});
if (confirmed) {
savePresets([...defaultPresets]); savePresets([...defaultPresets]);
renderPresets(); renderPresets();
} }
@@ -6008,7 +6006,7 @@
}); });
} }
function checkDeviceAvailability(modeName) { async function checkDeviceAvailability(modeName) {
const selectedDevice = parseInt(getSelectedDevice()); const selectedDevice = parseInt(getSelectedDevice());
const usedBy = getDeviceInUseBy(selectedDevice); const usedBy = getDeviceInUseBy(selectedDevice);
@@ -6018,10 +6016,12 @@
if (availableDevice !== null) { if (availableDevice !== null) {
// Another device is available - offer to switch // Another device is available - offer to switch
const switchDevice = confirm( const switchDevice = await AppFeedback.confirmAction({
`Device ${selectedDevice} is in use by ${usedBy.toUpperCase()}.\n\n` + title: 'SDR Device In Use',
`Device ${availableDevice} is available. Switch to it?` message: `Device ${selectedDevice} is in use by ${usedBy.toUpperCase()}. Device ${availableDevice} is available. Switch to it?`,
); confirmLabel: 'Switch Device',
confirmClass: 'btn-danger'
});
if (switchDevice) { if (switchDevice) {
document.getElementById('deviceSelect').value = availableDevice; document.getElementById('deviceSelect').value = availableDevice;
return true; // Can proceed with new device return true; // Can proceed with new device
@@ -6507,7 +6507,7 @@
pagerScopeLastInputSample = 0; pagerScopeLastInputSample = 0;
} }
function startDecoding() { async function startDecoding() {
const freq = document.getElementById('frequency').value; const freq = document.getElementById('frequency').value;
const gain = document.getElementById('gain').value; const gain = document.getElementById('gain').value;
const squelch = document.getElementById('squelch').value; const squelch = document.getElementById('squelch').value;
@@ -6524,7 +6524,7 @@
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
// Check if device is available (only for local mode) // Check if device is available (only for local mode)
if (!isAgentMode && !checkDeviceAvailability('pager')) { if (!isAgentMode && !await checkDeviceAvailability('pager')) {
return; return;
} }
@@ -7263,8 +7263,8 @@
function toggleRecon() { function toggleRecon() {
reconEnabled = !reconEnabled; reconEnabled = !reconEnabled;
localStorage.setItem('reconEnabled', reconEnabled); localStorage.setItem('reconEnabled', reconEnabled);
document.getElementById('reconPanel').style.display = reconEnabled ? 'block' : 'none'; document.getElementById('reconPanel')?.classList.toggle('active', reconEnabled);
document.getElementById('reconBtn').classList.toggle('active', reconEnabled); document.getElementById('reconBtn')?.classList.toggle('active', reconEnabled);
// Populate recon display if enabled and we have data // Populate recon display if enabled and we have data
if (reconEnabled && deviceDatabase.size > 0) { if (reconEnabled && deviceDatabase.size > 0) {
@@ -7275,11 +7275,9 @@
} }
// Initialize recon state // Initialize recon state
document.getElementById('reconPanel')?.classList.toggle('active', reconEnabled);
if (reconEnabled) { if (reconEnabled) {
document.getElementById('reconPanel').style.display = 'block'; document.getElementById('reconBtn')?.classList.add('active');
document.getElementById('reconBtn').classList.add('active');
} else {
document.getElementById('reconPanel').style.display = 'none';
} }
// Hook into existing message handlers to track devices // Hook into existing message handlers to track devices
@@ -9098,7 +9096,13 @@
// Start handshake capture // Start handshake capture
async function captureHandshake(bssid, channel) { async function captureHandshake(bssid, channel) {
if (!confirm('Start handshake capture for ' + bssid + '? This will stop the current scan.')) { const confirmed = await AppFeedback.confirmAction({
title: 'Capture Handshake',
message: 'Start handshake capture for ' + bssid + '? This will stop the current scan.',
confirmLabel: 'Start Capture',
confirmClass: 'btn-danger'
});
if (!confirmed) {
return; return;
} }
@@ -9339,7 +9343,7 @@
} }
// Send deauth // Send deauth
function sendDeauth() { async function sendDeauth() {
const bssid = document.getElementById('targetBssid').value; const bssid = document.getElementById('targetBssid').value;
const client = document.getElementById('targetClient').value || 'FF:FF:FF:FF:FF:FF'; const client = document.getElementById('targetClient').value || 'FF:FF:FF:FF:FF:FF';
const count = document.getElementById('deauthCount').value || '5'; const count = document.getElementById('deauthCount').value || '5';
@@ -9349,7 +9353,13 @@
return; return;
} }
if (!confirm('Send ' + count + ' deauth packets to ' + bssid + '?\\n\\n⚠ Only use on networks you own or have authorization to test!')) { const deauthConfirmed = await AppFeedback.confirmAction({
title: 'Send Deauth Packets',
message: 'Send ' + count + ' deauth packets to ' + bssid + '? Only use on networks you own or have authorization to test.',
confirmLabel: 'Send Deauth',
confirmClass: 'btn-danger'
});
if (!deauthConfirmed) {
return; return;
} }
@@ -11779,7 +11789,7 @@
// Check for conflicts if using agent // Check for conflicts if using agent
if (isAgentMode && typeof checkAgentModeConflict === 'function') { if (isAgentMode && typeof checkAgentModeConflict === 'function') {
if (!checkAgentModeConflict('tscm')) { if (!await checkAgentModeConflict('tscm')) {
return; // Conflict detected, user cancelled return; // Conflict detected, user cancelled
} }
} }
@@ -15082,7 +15092,13 @@
} }
async function tscmRemoveKnownDevice(identifier) { async function tscmRemoveKnownDevice(identifier) {
if (!confirm('Remove this device from known devices list?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Remove Known Device',
message: 'Remove this device from known devices list?',
confirmLabel: 'Remove',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch(`/tscm/known-devices/${identifier}`, { const response = await fetch(`/tscm/known-devices/${identifier}`, {
@@ -15603,7 +15619,13 @@
} }
async function tscmDeleteSchedule(scheduleId) { async function tscmDeleteSchedule(scheduleId) {
if (!confirm('Delete this schedule?')) return; const confirmed = await AppFeedback.confirmAction({
title: 'Delete Schedule',
message: 'Delete this schedule?',
confirmLabel: 'Delete',
confirmClass: 'btn-danger'
});
if (!confirmed) return;
try { try {
const response = await fetch(`/tscm/schedules/${scheduleId}`, { method: 'DELETE' }); const response = await fetch(`/tscm/schedules/${scheduleId}`, { method: 'DELETE' });
const data = await response.json(); const data = await response.json();
@@ -16121,6 +16143,7 @@
<script src="{{ url_for('static', filename='js/core/alerts.js') }}"></script> <script src="{{ url_for('static', filename='js/core/alerts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/recordings.js') }}"></script> <script src="{{ url_for('static', filename='js/core/recordings.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/ui-feedback.js') }}"></script> <script src="{{ url_for('static', filename='js/core/ui-feedback.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/sse-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/run-state.js') }}"></script> <script src="{{ url_for('static', filename='js/core/run-state.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/command-palette.js') }}"></script> <script src="{{ url_for('static', filename='js/core/command-palette.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/first-run-setup.js') }}"></script> <script src="{{ url_for('static', filename='js/core/first-run-setup.js') }}"></script>

View File

@@ -11,7 +11,7 @@
{% endif %} {% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<style> <style>

View File

@@ -14,13 +14,13 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Adapter</label> <label for="btAdapterSelect">Adapter</label>
<select id="btAdapterSelect"> <select id="btAdapterSelect">
<option value="">Detecting adapters...</option> <option value="">Detecting adapters...</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Scan Mode</label> <label for="btScanMode">Scan Mode</label>
<select id="btScanMode"> <select id="btScanMode">
<option value="auto">Auto (Recommended)</option> <option value="auto">Auto (Recommended)</option>
<option value="bleak">Bleak Library</option> <option value="bleak">Bleak Library</option>
@@ -30,7 +30,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Transport</label> <label for="btTransport">Transport</label>
<select id="btTransport"> <select id="btTransport">
<option value="auto">Auto (BLE + Classic)</option> <option value="auto">Auto (BLE + Classic)</option>
<option value="le">BLE Only</option> <option value="le">BLE Only</option>
@@ -38,11 +38,11 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Duration (seconds, 0 = continuous)</label> <label for="btScanDuration">Duration (seconds, 0 = continuous)</label>
<input type="number" id="btScanDuration" value="0" min="0" max="300" placeholder="0"> <input type="number" id="btScanDuration" value="0" min="0" max="300" placeholder="0">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Min RSSI Filter (dBm)</label> <label for="btMinRssi">Min RSSI Filter (dBm)</label>
<input type="number" id="btMinRssi" value="-100" min="-100" max="-20" placeholder="-100"> <input type="number" id="btMinRssi" value="-100" min="-100" max="-20" placeholder="-100">
</div> </div>
<button class="preset-btn" onclick="btCheckCapabilities()" style="width: 100%;"> <button class="preset-btn" onclick="btCheckCapabilities()" style="width: 100%;">

View File

@@ -13,7 +13,7 @@
<div id="btLocateHandoffCard" style="display: none; background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.3); border-radius: 6px; padding: 8px; margin-bottom: 8px;"> <div id="btLocateHandoffCard" style="display: none; background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.3); border-radius: 6px; padding: 8px; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 10px; color: var(--accent-green); text-transform: uppercase; font-weight: 600;">Handed off from BT</span> <span style="font-size: 10px; color: var(--accent-green); text-transform: uppercase; font-weight: 600;">Handed off from BT</span>
<button onclick="BtLocate.clearHandoff()" style="background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 10px;">&times;</button> <button onclick="BtLocate.clearHandoff()" aria-label="Clear handoff" style="background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 10px;">&times;</button>
</div> </div>
<div id="btLocateHandoffName" style="font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 4px;"></div> <div id="btLocateHandoffName" style="font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 4px;"></div>
<div id="btLocateHandoffMeta" style="font-size: 10px; color: var(--text-dim); font-family: var(--font-mono);"></div> <div id="btLocateHandoffMeta" style="font-size: 10px; color: var(--text-dim); font-family: var(--font-mono);"></div>

View File

@@ -132,7 +132,7 @@
<div class="signal-details-modal-content"> <div class="signal-details-modal-content">
<div class="signal-details-modal-header"> <div class="signal-details-modal-header">
<h3>Configure Channel <span id="meshModalChannelIndex">0</span></h3> <h3>Configure Channel <span id="meshModalChannelIndex">0</span></h3>
<button class="signal-details-modal-close" onclick="Meshtastic.closeChannelModal()">&times;</button> <button class="signal-details-modal-close" onclick="Meshtastic.closeChannelModal()" aria-label="Close channel config">&times;</button>
</div> </div>
<div class="signal-details-modal-body"> <div class="signal-details-modal-body">
<div class="signal-details-section"> <div class="signal-details-section">
@@ -175,7 +175,7 @@
<div class="signal-details-modal-content"> <div class="signal-details-modal-content">
<div class="signal-details-modal-header"> <div class="signal-details-modal-header">
<h3>Traceroute to <span id="meshTracerouteDest">--</span></h3> <h3>Traceroute to <span id="meshTracerouteDest">--</span></h3>
<button class="signal-details-modal-close" onclick="Meshtastic.closeTracerouteModal()">&times;</button> <button class="signal-details-modal-close" onclick="Meshtastic.closeTracerouteModal()" aria-label="Close traceroute">&times;</button>
</div> </div>
<div class="signal-details-modal-body"> <div class="signal-details-modal-body">
<div id="meshTracerouteContent" class="mesh-traceroute-content"> <div id="meshTracerouteContent" class="mesh-traceroute-content">

View File

@@ -3,7 +3,7 @@
<div class="section"> <div class="section">
<h3>Frequency</h3> <h3>Frequency</h3>
<div class="form-group"> <div class="form-group">
<label>Frequency (MHz)</label> <label for="frequency">Frequency (MHz)</label>
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350"> <input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
</div> </div>
<div class="preset-buttons" id="presetButtons"> <div class="preset-buttons" id="presetButtons">
@@ -31,15 +31,15 @@
<div class="section"> <div class="section">
<h3>Settings</h3> <h3>Settings</h3>
<div class="form-group"> <div class="form-group">
<label>Gain (dB, 0 = auto)</label> <label for="gain">Gain (dB, 0 = auto)</label>
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto"> <input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Squelch Level</label> <label for="squelch">Squelch Level</label>
<input type="text" id="squelch" value="0" placeholder="0 = off"> <input type="text" id="squelch" value="0" placeholder="0 = off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>PPM Correction</label> <label for="ppm">PPM Correction</label>
<input type="text" id="ppm" value="0" placeholder="Frequency correction"> <input type="text" id="ppm" value="0" placeholder="Frequency correction">
</div> </div>
</div> </div>
@@ -53,7 +53,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Log file path</label> <label for="logFilePath">Log file path</label>
<input type="text" id="logFilePath" value="pager_messages.log" placeholder="pager_messages.log"> <input type="text" id="logFilePath" value="pager_messages.log" placeholder="pager_messages.log">
</div> </div>
</div> </div>
@@ -67,7 +67,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Hide messages containing (comma-separated)</label> <label for="filterKeywords">Hide messages containing (comma-separated)</label>
<input type="text" id="filterKeywords" placeholder="e.g. test, spam, alert" oninput="savePagerFilters()"> <input type="text" id="filterKeywords" placeholder="e.g. test, spam, alert" oninput="savePagerFilters()">
</div> </div>
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 5px;"> <div class="info-text" style="font-size: 10px; color: #666; margin-top: 5px;">

View File

@@ -3,7 +3,7 @@
<div class="section"> <div class="section">
<h3>Frequency</h3> <h3>Frequency</h3>
<div class="form-group"> <div class="form-group">
<label>Frequency (MHz)</label> <label for="sensorFrequency">Frequency (MHz)</label>
<input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92"> <input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92">
</div> </div>
<div class="preset-buttons"> <div class="preset-buttons">
@@ -17,11 +17,11 @@
<div class="section"> <div class="section">
<h3>Settings</h3> <h3>Settings</h3>
<div class="form-group"> <div class="form-group">
<label>Gain (dB, 0 = auto)</label> <label for="sensorGain">Gain (dB, 0 = auto)</label>
<input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto"> <input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>PPM Correction</label> <label for="sensorPpm">PPM Correction</label>
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction"> <input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
</div> </div>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="section"> <div class="section">
<h3>Device</h3> <h3>Device</h3>
<div class="form-group"> <div class="form-group">
<label>SDR Device</label> <label for="wfDevice">SDR Device</label>
<select id="wfDevice" onchange="Waterfall && Waterfall.onDeviceChange && Waterfall.onDeviceChange()"> <select id="wfDevice" onchange="Waterfall && Waterfall.onDeviceChange && Waterfall.onDeviceChange()">
<option value="">Loading devices...</option> <option value="">Loading devices...</option>
</select> </select>
@@ -60,11 +60,11 @@
<div class="section"> <div class="section">
<h3>Tuning</h3> <h3>Tuning</h3>
<div class="form-group"> <div class="form-group">
<label>Center Frequency (MHz)</label> <label for="wfCenterFreq">Center Frequency (MHz)</label>
<input type="number" id="wfCenterFreq" value="100.0000" step="0.001" min="0.001" max="6000"> <input type="number" id="wfCenterFreq" value="100.0000" step="0.001" min="0.001" max="6000">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Span (MHz)</label> <label for="wfSpanMhz">Span (MHz)</label>
<input type="number" id="wfSpanMhz" value="2.4" step="0.1" min="0.05" max="30"> <input type="number" id="wfSpanMhz" value="2.4" step="0.1" min="0.05" max="30">
</div> </div>
<div class="wf-side-grid-2"> <div class="wf-side-grid-2">
@@ -130,11 +130,11 @@
Identify current frequency using local catalog and SigID Wiki matches. Identify current frequency using local catalog and SigID Wiki matches.
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Frequency (MHz)</label> <label for="wfSigIdFreq">Frequency (MHz)</label>
<input type="number" id="wfSigIdFreq" value="100.0000" step="0.0001" min="0.001" max="6000"> <input type="number" id="wfSigIdFreq" value="100.0000" step="0.0001" min="0.001" max="6000">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Mode Hint</label> <label for="wfSigIdMode">Mode Hint</label>
<select id="wfSigIdMode"> <select id="wfSigIdMode">
<option value="auto" selected>Auto (Current Mode)</option> <option value="auto" selected>Auto (Current Mode)</option>
<option value="wfm">WFM</option> <option value="wfm">WFM</option>
@@ -156,15 +156,15 @@
<div class="section"> <div class="section">
<h3>Scan</h3> <h3>Scan</h3>
<div class="form-group"> <div class="form-group">
<label>Range Start (MHz)</label> <label for="wfScanStart">Range Start (MHz)</label>
<input type="number" id="wfScanStart" value="98.8000" step="0.001" min="0.001" max="6000"> <input type="number" id="wfScanStart" value="98.8000" step="0.001" min="0.001" max="6000">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Range End (MHz)</label> <label for="wfScanEnd">Range End (MHz)</label>
<input type="number" id="wfScanEnd" value="101.2000" step="0.001" min="0.001" max="6000"> <input type="number" id="wfScanEnd" value="101.2000" step="0.001" min="0.001" max="6000">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Step (kHz)</label> <label for="wfScanStepKhz">Step (kHz)</label>
<select id="wfScanStepKhz"> <select id="wfScanStepKhz">
<option value="5">5</option> <option value="5">5</option>
<option value="10">10</option> <option value="10">10</option>
@@ -176,15 +176,15 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Dwell (ms)</label> <label for="wfScanDwellMs">Dwell (ms)</label>
<input type="number" id="wfScanDwellMs" value="450" min="60" max="10000" step="10"> <input type="number" id="wfScanDwellMs" value="450" min="60" max="10000" step="10">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Signal Threshold <span id="wfScanThresholdValue" class="wf-inline-value">170</span></label> <label for="wfScanThreshold">Signal Threshold <span id="wfScanThresholdValue" class="wf-inline-value">170</span></label>
<input type="range" id="wfScanThreshold" min="0" max="255" value="170"> <input type="range" id="wfScanThreshold" min="0" max="255" value="170">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Hold On Hit (ms)</label> <label for="wfScanHoldMs">Hold On Hit (ms)</label>
<input type="number" id="wfScanHoldMs" value="2500" min="0" max="30000" step="100"> <input type="number" id="wfScanHoldMs" value="2500" min="0" max="30000" step="100">
</div> </div>
<div class="checkbox-group wf-scan-checkboxes"> <div class="checkbox-group wf-scan-checkboxes">
@@ -246,11 +246,11 @@
<div class="section"> <div class="section">
<h3>Capture</h3> <h3>Capture</h3>
<div class="form-group"> <div class="form-group">
<label>Gain <span class="wf-inline-value">(dB or AUTO)</span></label> <label for="wfGain">Gain <span class="wf-inline-value">(dB or AUTO)</span></label>
<input type="text" id="wfGain" value="AUTO" placeholder="AUTO or numeric"> <input type="text" id="wfGain" value="AUTO" placeholder="AUTO or numeric">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>FFT Size</label> <label for="wfFftSize">FFT Size</label>
<select id="wfFftSize"> <select id="wfFftSize">
<option value="256">256</option> <option value="256">256</option>
<option value="512">512</option> <option value="512">512</option>
@@ -260,7 +260,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Frame Rate</label> <label for="wfFps">Frame Rate</label>
<select id="wfFps"> <select id="wfFps">
<option value="10">10 fps</option> <option value="10">10 fps</option>
<option value="20" selected>20 fps</option> <option value="20" selected>20 fps</option>
@@ -269,7 +269,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>FFT Averaging</label> <label for="wfAvgCount">FFT Averaging</label>
<select id="wfAvgCount"> <select id="wfAvgCount">
<option value="1">1 (none)</option> <option value="1">1 (none)</option>
<option value="2">2</option> <option value="2">2</option>
@@ -279,7 +279,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>PPM Correction</label> <label for="wfPpm">PPM Correction</label>
<input type="number" id="wfPpm" value="0" step="1" min="-200" max="200" placeholder="0"> <input type="number" id="wfPpm" value="0" step="1" min="-200" max="200" placeholder="0">
</div> </div>
<div class="checkbox-group wf-scan-checkboxes"> <div class="checkbox-group wf-scan-checkboxes">
@@ -293,7 +293,7 @@
<div class="section"> <div class="section">
<h3>Display</h3> <h3>Display</h3>
<div class="form-group"> <div class="form-group">
<label>Color Palette</label> <label for="wfPalette">Color Palette</label>
<select id="wfPalette" onchange="Waterfall.setPalette(this.value)"> <select id="wfPalette" onchange="Waterfall.setPalette(this.value)">
<option value="turbo" selected>Turbo</option> <option value="turbo" selected>Turbo</option>
<option value="plasma">Plasma</option> <option value="plasma">Plasma</option>
@@ -302,11 +302,11 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Noise Floor (dB)</label> <label for="wfDbMin">Noise Floor (dB)</label>
<input type="number" id="wfDbMin" value="-100" step="5" disabled> <input type="number" id="wfDbMin" value="-100" step="5" disabled>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Ceiling (dB)</label> <label for="wfDbMax">Ceiling (dB)</label>
<input type="number" id="wfDbMax" value="-20" step="5" disabled> <input type="number" id="wfDbMax" value="-20" step="5" disabled>
</div> </div>
<div class="checkbox-group wf-scan-checkboxes"> <div class="checkbox-group wf-scan-checkboxes">

View File

@@ -24,7 +24,7 @@
<div class="section"> <div class="section">
<h3>WiFi Adapter</h3> <h3>WiFi Adapter</h3>
<div class="form-group"> <div class="form-group">
<label>Select Device</label> <label for="wifiInterfaceSelect">Select Device</label>
<select id="wifiInterfaceSelect" style="font-size: 12px;"> <select id="wifiInterfaceSelect" style="font-size: 12px;">
<option value="">Detecting interfaces...</option> <option value="">Detecting interfaces...</option>
</select> </select>
@@ -62,7 +62,7 @@
<div class="section"> <div class="section">
<h3>Scan Settings</h3> <h3>Scan Settings</h3>
<div class="form-group"> <div class="form-group">
<label>Band</label> <label for="wifiBand">Band</label>
<select id="wifiBand"> <select id="wifiBand">
<option value="abg">All (2.4 + 5 GHz)</option> <option value="abg">All (2.4 + 5 GHz)</option>
<option value="bg">2.4 GHz only</option> <option value="bg">2.4 GHz only</option>
@@ -70,7 +70,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Channel Preset</label> <label for="wifiChannelPreset">Channel Preset</label>
<select id="wifiChannelPreset"> <select id="wifiChannelPreset">
<option value="">Auto hop (all)</option> <option value="">Auto hop (all)</option>
<option value="2.4-common">2.4 GHz Common (1,6,11)</option> <option value="2.4-common">2.4 GHz Common (1,6,11)</option>
@@ -81,11 +81,11 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Channel List (overrides preset)</label> <label for="wifiChannelList">Channel List (overrides preset)</label>
<input type="text" id="wifiChannelList" placeholder="e.g., 1,6,11 or 36,40,44,48"> <input type="text" id="wifiChannelList" placeholder="e.g., 1,6,11 or 36,40,44,48">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Channel (single)</label> <label for="wifiChannel">Channel (single)</label>
<input type="text" id="wifiChannel" placeholder="e.g., 6 or 36"> <input type="text" id="wifiChannel" placeholder="e.g., 6 or 36">
</div> </div>
</div> </div>
@@ -110,15 +110,15 @@
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> Only use on authorized networks <span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> Only use on authorized networks
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Target BSSID</label> <label for="targetBssid">Target BSSID</label>
<input type="text" id="targetBssid" placeholder="AA:BB:CC:DD:EE:FF"> <input type="text" id="targetBssid" placeholder="AA:BB:CC:DD:EE:FF">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Target Client (optional)</label> <label for="targetClient">Target Client (optional)</label>
<input type="text" id="targetClient" placeholder="FF:FF:FF:FF:FF:FF (broadcast)"> <input type="text" id="targetClient" placeholder="FF:FF:FF:FF:FF:FF (broadcast)">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Deauth Count</label> <label for="deauthCount">Deauth Count</label>
<input type="text" id="deauthCount" value="5" placeholder="5"> <input type="text" id="deauthCount" value="5" placeholder="5">
</div> </div>
<button class="preset-btn" onclick="sendDeauth()" style="width: 100%; border-color: var(--accent-red); color: var(--accent-red);"> <button class="preset-btn" onclick="sendDeauth()" style="width: 100%; border-color: var(--accent-red); color: var(--accent-red);">

View File

@@ -13,7 +13,7 @@
<div id="wflHandoffCard" style="display: none; background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.3); border-radius: 6px; padding: 8px; margin-bottom: 8px;"> <div id="wflHandoffCard" style="display: none; background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.3); border-radius: 6px; padding: 8px; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 10px; color: var(--accent-green); text-transform: uppercase; font-weight: 600;">Handed off from WiFi</span> <span style="font-size: 10px; color: var(--accent-green); text-transform: uppercase; font-weight: 600;">Handed off from WiFi</span>
<button onclick="WiFiLocate.clearHandoff()" style="background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 10px;">&times;</button> <button onclick="WiFiLocate.clearHandoff()" aria-label="Clear handoff" style="background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 10px;">&times;</button>
</div> </div>
<div id="wflHandoffName" style="font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 4px;"></div> <div id="wflHandoffName" style="font-size: 12px; font-weight: 600; color: var(--text-primary); margin-top: 4px;"></div>
<div id="wflHandoffMeta" style="font-size: 10px; color: var(--text-dim); font-family: var(--font-mono);"></div> <div id="wflHandoffMeta" style="font-size: 10px; color: var(--text-dim); font-family: var(--font-mono);"></div>

View File

@@ -159,14 +159,15 @@
{# Dynamic dashboard button (shown when in satellite mode) #} {# Dynamic dashboard button (shown when in satellite mode) #}
<div class="mode-nav-actions"> <div class="mode-nav-actions">
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;"> <a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn">
<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-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> <span class="nav-label">Full Dashboard</span>
</a> </a>
</div> </div>
{# Nav Utilities (clock, theme, tools) #} {# Nav Utilities (clock, SSE status, theme, tools) #}
<div class="nav-utilities"> <div class="nav-utilities">
<span class="status-dot inactive" id="sseStatusDot" data-tooltip="No active streams" aria-label="Stream connection status"></span>
<div class="nav-clock"> <div class="nav-clock">
<span class="utc-label">UTC</span> <span class="utc-label">UTC</span>
<span class="utc-time" id="headerUtcTime">--:--:--</span> <span class="utc-time" id="headerUtcTime">--:--:--</span>
@@ -426,9 +427,15 @@
// showHelp is defined by the help-modal.html partial // showHelp is defined by the help-modal.html partial
if (typeof logout === 'undefined') { if (typeof logout === 'undefined') {
window.logout = function(e) { window.logout = async function(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
if (confirm('Are you sure you want to logout?')) { const confirmed = await AppFeedback.confirmAction({
title: 'Logout',
message: 'Are you sure you want to logout?',
confirmLabel: 'Logout',
confirmClass: 'btn-danger'
});
if (confirmed) {
window.location.href = '/logout'; window.location.href = '/logout';
} }
}; };

View File

@@ -21,7 +21,7 @@
<!-- Core CSS variables --> <!-- Core CSS variables -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17"> <link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/satellite_dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/satellite_dashboard.css') }}">