mirror of
https://github.com/smittix/intercept.git
synced 2026-07-06 00:28:12 -07:00
Merge upstream/main and resolve weather-satellite.js conflict
Keep allPasses assignment for satellite filtering support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1246,7 +1246,7 @@ body {
|
||||
|
||||
.control-group select {
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-cyan);
|
||||
|
||||
@@ -779,7 +779,7 @@ body {
|
||||
|
||||
.control-group select {
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-cyan);
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
gap: 4px;
|
||||
}
|
||||
.aprs-strip .strip-select {
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 4px 8px;
|
||||
|
||||
+201
-118
@@ -1,118 +1,201 @@
|
||||
/* Morse Code / CW Decoder Styles */
|
||||
|
||||
/* Scope canvas container */
|
||||
.morse-scope-container {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.morse-scope-container canvas {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Decoded text panel */
|
||||
.morse-decoded-panel {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
min-height: 120px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.morse-decoded-panel:empty::before {
|
||||
content: 'Decoded text will appear here...';
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Individual decoded character with fade-in */
|
||||
.morse-char {
|
||||
display: inline;
|
||||
animation: morseFadeIn 0.3s ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes morseFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Morse notation above character */
|
||||
.morse-char-morse {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 1px;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
/* Reference grid */
|
||||
.morse-ref-grid {
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.morse-ref-grid.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Toolbar: export/copy/clear */
|
||||
.morse-toolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.morse-toolbar .btn {
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
/* Status bar at bottom */
|
||||
.morse-status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
padding: 6px 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.morse-status-bar .status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Word space styling */
|
||||
.morse-word-space {
|
||||
display: inline;
|
||||
width: 0.5em;
|
||||
}
|
||||
/* Morse Code / CW Decoder Styles */
|
||||
|
||||
.morse-mode-help,
|
||||
.morse-help-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.morse-help-text {
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.morse-hf-note {
|
||||
font-size: 11px;
|
||||
color: #ffaa00;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.morse-presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.morse-actions-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.morse-file-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.morse-file-row input[type='file'] {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.morse-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.morse-status #morseCharCount {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.morse-ref-grid {
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
max-height: 560px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
line-height: 1.8;
|
||||
columns: 2;
|
||||
column-gap: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.morse-ref-grid.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.morse-ref-toggle {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.morse-ref-divider {
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.morse-decoded-panel {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
min-height: 120px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.morse-decoded-panel:empty::before {
|
||||
content: 'Decoded text will appear here...';
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.morse-char {
|
||||
display: inline;
|
||||
animation: morseFadeIn 0.3s ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes morseFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.morse-word-space {
|
||||
display: inline;
|
||||
width: 0.5em;
|
||||
}
|
||||
|
||||
.morse-raw-panel {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid #1a1a2e;
|
||||
border-radius: 4px;
|
||||
background: #080812;
|
||||
}
|
||||
|
||||
.morse-raw-label {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #667;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.morse-raw-text {
|
||||
min-height: 30px;
|
||||
max-height: 90px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: #8fd0ff;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.morse-metrics-panel {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: #7a8694;
|
||||
}
|
||||
|
||||
.morse-metrics-panel span {
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #1a1a2e;
|
||||
background: #080811;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.morse-status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
padding: 6px 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.morse-status-bar .status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.morse-metrics-panel {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.morse-file-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/* System Health Mode Styles */
|
||||
|
||||
.sys-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sys-card {
|
||||
background: var(--bg-card, #1a1a2e);
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.sys-card-header {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-dim, #8888aa);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sys-card-body {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary, #e0e0ff);
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
}
|
||||
|
||||
.sys-card-detail {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #8888aa);
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Metric Bars */
|
||||
.sys-metric-bar-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.sys-metric-bar-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim, #8888aa);
|
||||
min-width: 40px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sys-metric-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--bg-primary, #0d0d1a);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sys-metric-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.sys-metric-bar-fill.ok {
|
||||
background: var(--accent-green, #00ff88);
|
||||
}
|
||||
|
||||
.sys-metric-bar-fill.warn {
|
||||
background: var(--accent-yellow, #ffcc00);
|
||||
}
|
||||
|
||||
.sys-metric-bar-fill.crit {
|
||||
background: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
.sys-metric-bar-value {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
}
|
||||
|
||||
.sys-metric-na {
|
||||
color: var(--text-dim, #8888aa);
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Process items */
|
||||
.sys-process-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.sys-process-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sys-process-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sys-process-dot.running {
|
||||
background: var(--accent-green, #00ff88);
|
||||
box-shadow: 0 0 4px rgba(0, 255, 136, 0.4);
|
||||
}
|
||||
|
||||
.sys-process-dot.stopped {
|
||||
background: var(--text-dim, #555);
|
||||
}
|
||||
|
||||
/* SDR Devices */
|
||||
.sys-sdr-device {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border-color, #2a2a4a);
|
||||
}
|
||||
|
||||
.sys-sdr-device:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sys-rescan-btn {
|
||||
font-size: 9px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
color: var(--accent-cyan, #00d4ff);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.sys-rescan-btn:hover {
|
||||
background: var(--bg-primary, #0d0d1a);
|
||||
}
|
||||
|
||||
/* Sidebar Quick Grid */
|
||||
.sys-quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sys-quick-item {
|
||||
padding: 6px 8px;
|
||||
background: var(--bg-primary, #0d0d1a);
|
||||
border: 1px solid var(--border-color, #2a2a4a);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sys-quick-label {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
color: var(--text-dim, #8888aa);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.sys-quick-value {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
||||
color: var(--text-primary, #e0e0ff);
|
||||
}
|
||||
|
||||
/* Color-coded quick values */
|
||||
.sys-val-ok {
|
||||
color: var(--accent-green, #00ff88) !important;
|
||||
}
|
||||
|
||||
.sys-val-warn {
|
||||
color: var(--accent-yellow, #ffcc00) !important;
|
||||
}
|
||||
|
||||
.sys-val-crit {
|
||||
color: var(--accent-red, #ff3366) !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sys-dashboard {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) and (min-width: 769px) {
|
||||
.sys-dashboard {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
+1220
-400
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* System Health – IIFE module
|
||||
*
|
||||
* Always-on monitoring that auto-connects when the mode is entered.
|
||||
* Streams real-time system metrics via SSE and provides SDR device enumeration.
|
||||
*/
|
||||
const SystemHealth = (function () {
|
||||
'use strict';
|
||||
|
||||
let eventSource = null;
|
||||
let connected = false;
|
||||
let lastMetrics = null;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes == null) return '--';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let val = bytes;
|
||||
while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
|
||||
return val.toFixed(1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function barClass(pct) {
|
||||
if (pct >= 85) return 'crit';
|
||||
if (pct >= 60) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function barHtml(pct, label) {
|
||||
if (pct == null) return '<span class="sys-metric-na">N/A</span>';
|
||||
const cls = barClass(pct);
|
||||
const rounded = Math.round(pct);
|
||||
return '<div class="sys-metric-bar-wrap">' +
|
||||
(label ? '<span class="sys-metric-bar-label">' + label + '</span>' : '') +
|
||||
'<div class="sys-metric-bar"><div class="sys-metric-bar-fill ' + cls + '" style="width:' + rounded + '%"></div></div>' +
|
||||
'<span class="sys-metric-bar-value">' + rounded + '%</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rendering
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderCpuCard(m) {
|
||||
const el = document.getElementById('sysCardCpu');
|
||||
if (!el) return;
|
||||
const cpu = m.cpu;
|
||||
if (!cpu) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">psutil not available</span></div>'; return; }
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">CPU</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(cpu.percent, '') +
|
||||
'<div class="sys-card-detail">Load: ' + cpu.load_1 + ' / ' + cpu.load_5 + ' / ' + cpu.load_15 + '</div>' +
|
||||
'<div class="sys-card-detail">Cores: ' + cpu.count + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderMemoryCard(m) {
|
||||
const el = document.getElementById('sysCardMemory');
|
||||
if (!el) return;
|
||||
const mem = m.memory;
|
||||
if (!mem) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
const swap = m.swap || {};
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">Memory</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(mem.percent, '') +
|
||||
'<div class="sys-card-detail">' + formatBytes(mem.used) + ' / ' + formatBytes(mem.total) + '</div>' +
|
||||
'<div class="sys-card-detail">Swap: ' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderDiskCard(m) {
|
||||
const el = document.getElementById('sysCardDisk');
|
||||
if (!el) return;
|
||||
const disk = m.disk;
|
||||
if (!disk) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">Disk</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(disk.percent, '') +
|
||||
'<div class="sys-card-detail">' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '</div>' +
|
||||
'<div class="sys-card-detail">Path: ' + (disk.path || '/') + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function _extractPrimaryTemp(temps) {
|
||||
if (!temps) return null;
|
||||
// Prefer common chip names
|
||||
const preferred = ['cpu_thermal', 'coretemp', 'k10temp', 'acpitz', 'soc_thermal'];
|
||||
for (const name of preferred) {
|
||||
if (temps[name] && temps[name].length) return temps[name][0];
|
||||
}
|
||||
// Fall back to first available
|
||||
for (const key of Object.keys(temps)) {
|
||||
if (temps[key] && temps[key].length) return temps[key][0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderSdrCard(devices) {
|
||||
const el = document.getElementById('sysCardSdr');
|
||||
if (!el) return;
|
||||
let html = '<div class="sys-card-header">SDR Devices <button class="sys-rescan-btn" onclick="SystemHealth.refreshSdr()">Rescan</button></div>';
|
||||
html += '<div class="sys-card-body">';
|
||||
if (!devices || !devices.length) {
|
||||
html += '<span class="sys-metric-na">No devices found</span>';
|
||||
} else {
|
||||
devices.forEach(function (d) {
|
||||
html += '<div class="sys-sdr-device">' +
|
||||
'<span class="sys-process-dot running"></span> ' +
|
||||
'<strong>' + d.type + ' #' + d.index + '</strong>' +
|
||||
'<div class="sys-card-detail">' + (d.name || 'Unknown') + '</div>' +
|
||||
(d.serial ? '<div class="sys-card-detail">S/N: ' + d.serial + '</div>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderProcessCard(m) {
|
||||
const el = document.getElementById('sysCardProcesses');
|
||||
if (!el) return;
|
||||
const procs = m.processes || {};
|
||||
const keys = Object.keys(procs).sort();
|
||||
let html = '<div class="sys-card-header">Processes</div><div class="sys-card-body">';
|
||||
if (!keys.length) {
|
||||
html += '<span class="sys-metric-na">No data</span>';
|
||||
} else {
|
||||
keys.forEach(function (k) {
|
||||
const running = procs[k];
|
||||
const dotCls = running ? 'running' : 'stopped';
|
||||
const label = k.charAt(0).toUpperCase() + k.slice(1);
|
||||
html += '<div class="sys-process-item">' +
|
||||
'<span class="sys-process-dot ' + dotCls + '"></span> ' +
|
||||
'<span class="sys-process-name">' + label + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderSystemInfoCard(m) {
|
||||
const el = document.getElementById('sysCardInfo');
|
||||
if (!el) return;
|
||||
const sys = m.system || {};
|
||||
const temp = _extractPrimaryTemp(m.temperatures);
|
||||
let html = '<div class="sys-card-header">System Info</div><div class="sys-card-body">';
|
||||
html += '<div class="sys-card-detail">Host: ' + (sys.hostname || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">OS: ' + (sys.platform || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">Python: ' + (sys.python || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">App: v' + (sys.version || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">Uptime: ' + (sys.uptime_human || '--') + '</div>';
|
||||
if (temp) {
|
||||
html += '<div class="sys-card-detail">Temp: ' + Math.round(temp.current) + '°C';
|
||||
if (temp.high) html += ' / ' + Math.round(temp.high) + '°C max';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateSidebarQuickStats(m) {
|
||||
const cpuEl = document.getElementById('sysQuickCpu');
|
||||
const tempEl = document.getElementById('sysQuickTemp');
|
||||
const ramEl = document.getElementById('sysQuickRam');
|
||||
const diskEl = document.getElementById('sysQuickDisk');
|
||||
|
||||
if (cpuEl) cpuEl.textContent = m.cpu ? Math.round(m.cpu.percent) + '%' : '--';
|
||||
if (ramEl) ramEl.textContent = m.memory ? Math.round(m.memory.percent) + '%' : '--';
|
||||
if (diskEl) diskEl.textContent = m.disk ? Math.round(m.disk.percent) + '%' : '--';
|
||||
|
||||
const temp = _extractPrimaryTemp(m.temperatures);
|
||||
if (tempEl) tempEl.innerHTML = temp ? Math.round(temp.current) + '°C' : '--';
|
||||
|
||||
// Color-code values
|
||||
[cpuEl, ramEl, diskEl].forEach(function (el) {
|
||||
if (!el) return;
|
||||
const val = parseInt(el.textContent);
|
||||
el.classList.remove('sys-val-ok', 'sys-val-warn', 'sys-val-crit');
|
||||
if (!isNaN(val)) el.classList.add('sys-val-' + barClass(val));
|
||||
});
|
||||
}
|
||||
|
||||
function updateSidebarProcesses(m) {
|
||||
const el = document.getElementById('sysProcessList');
|
||||
if (!el) return;
|
||||
const procs = m.processes || {};
|
||||
const keys = Object.keys(procs).sort();
|
||||
if (!keys.length) { el.textContent = 'No data'; return; }
|
||||
const running = keys.filter(function (k) { return procs[k]; });
|
||||
const stopped = keys.filter(function (k) { return !procs[k]; });
|
||||
el.innerHTML =
|
||||
(running.length ? '<span style="color: var(--accent-green, #00ff88);">' + running.length + ' running</span>' : '') +
|
||||
(running.length && stopped.length ? ' · ' : '') +
|
||||
(stopped.length ? '<span style="color: var(--text-dim);">' + stopped.length + ' stopped</span>' : '');
|
||||
}
|
||||
|
||||
function renderAll(m) {
|
||||
renderCpuCard(m);
|
||||
renderMemoryCard(m);
|
||||
renderDiskCard(m);
|
||||
renderProcessCard(m);
|
||||
renderSystemInfoCard(m);
|
||||
updateSidebarQuickStats(m);
|
||||
updateSidebarProcesses(m);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SSE Connection
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function connect() {
|
||||
if (eventSource) return;
|
||||
eventSource = new EventSource('/system/stream');
|
||||
eventSource.onmessage = function (e) {
|
||||
try {
|
||||
var data = JSON.parse(e.data);
|
||||
if (data.type === 'keepalive') return;
|
||||
lastMetrics = data;
|
||||
renderAll(data);
|
||||
} catch (_) { /* ignore parse errors */ }
|
||||
};
|
||||
eventSource.onopen = function () {
|
||||
connected = true;
|
||||
};
|
||||
eventSource.onerror = function () {
|
||||
connected = false;
|
||||
};
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
connected = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SDR Devices
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function refreshSdr() {
|
||||
var sidebarEl = document.getElementById('sysSdrList');
|
||||
if (sidebarEl) sidebarEl.innerHTML = 'Scanning…';
|
||||
|
||||
var cardEl = document.getElementById('sysCardSdr');
|
||||
if (cardEl) cardEl.innerHTML = '<div class="sys-card-header">SDR Devices</div><div class="sys-card-body">Scanning…</div>';
|
||||
|
||||
fetch('/system/sdr_devices')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var devices = data.devices || [];
|
||||
renderSdrCard(devices);
|
||||
// Update sidebar
|
||||
if (sidebarEl) {
|
||||
if (!devices.length) {
|
||||
sidebarEl.innerHTML = '<span style="color: var(--text-dim);">No SDR devices found</span>';
|
||||
} else {
|
||||
var html = '';
|
||||
devices.forEach(function (d) {
|
||||
html += '<div style="margin-bottom: 4px;"><span class="sys-process-dot running"></span> ' +
|
||||
d.type + ' #' + d.index + ' — ' + (d.name || 'Unknown') + '</div>';
|
||||
});
|
||||
sidebarEl.innerHTML = html;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
if (sidebarEl) sidebarEl.innerHTML = '<span style="color: var(--accent-red, #ff3366);">Detection failed</span>';
|
||||
renderSdrCard([]);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function init() {
|
||||
connect();
|
||||
refreshSdr();
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
destroy: destroy,
|
||||
refreshSdr: refreshSdr,
|
||||
};
|
||||
})();
|
||||
@@ -39,6 +39,40 @@ const WeatherSat = (function() {
|
||||
startCountdownTimer();
|
||||
checkSchedulerStatus();
|
||||
initGroundMap();
|
||||
|
||||
// Re-filter passes when satellite selection changes
|
||||
const satSelect = document.getElementById('weatherSatSelect');
|
||||
if (satSelect) {
|
||||
satSelect.addEventListener('change', () => {
|
||||
applyPassFilter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get passes filtered by the currently selected satellite.
|
||||
*/
|
||||
function getFilteredPasses() {
|
||||
const satSelect = document.getElementById('weatherSatSelect');
|
||||
const selected = satSelect?.value;
|
||||
if (!selected) return passes;
|
||||
return passes.filter(p => p.satellite === selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render passes, timeline, countdown and polar plot using filtered list.
|
||||
*/
|
||||
function applyPassFilter() {
|
||||
const filtered = getFilteredPasses();
|
||||
selectedPassIndex = -1;
|
||||
renderPasses(filtered);
|
||||
renderTimeline(filtered);
|
||||
updateCountdownFromPasses();
|
||||
if (filtered.length > 0) {
|
||||
selectPass(0);
|
||||
} else {
|
||||
updateGroundTrack(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,9 +627,10 @@ const WeatherSat = (function() {
|
||||
* Select a pass to display in polar plot and map
|
||||
*/
|
||||
function selectPass(index) {
|
||||
if (index < 0 || index >= passes.length) return;
|
||||
const filtered = getFilteredPasses();
|
||||
if (index < 0 || index >= filtered.length) return;
|
||||
selectedPassIndex = index;
|
||||
const pass = passes[index];
|
||||
const pass = filtered[index];
|
||||
|
||||
// Highlight active card
|
||||
document.querySelectorAll('.wxsat-pass-card').forEach((card, i) => {
|
||||
@@ -1048,8 +1083,9 @@ const WeatherSat = (function() {
|
||||
}
|
||||
|
||||
function getSelectedPass() {
|
||||
if (selectedPassIndex < 0 || selectedPassIndex >= passes.length) return null;
|
||||
return passes[selectedPassIndex];
|
||||
const filtered = getFilteredPasses();
|
||||
if (selectedPassIndex < 0 || selectedPassIndex >= filtered.length) return null;
|
||||
return filtered[selectedPassIndex];
|
||||
}
|
||||
|
||||
function getSatellitePositionForPass(pass, atTime = new Date()) {
|
||||
@@ -1161,8 +1197,9 @@ const WeatherSat = (function() {
|
||||
const now = new Date();
|
||||
let nextPass = null;
|
||||
let isActive = false;
|
||||
const filtered = getFilteredPasses();
|
||||
|
||||
for (const pass of passes) {
|
||||
for (const pass of filtered) {
|
||||
const start = parsePassDate(pass.startTimeISO);
|
||||
const end = parsePassDate(pass.endTimeISO);
|
||||
if (!start || !end) {
|
||||
|
||||
Reference in New Issue
Block a user