diff --git a/static/css/modes/aprs.css b/static/css/modes/aprs.css
index b87c07b..cb19ccb 100644
--- a/static/css/modes/aprs.css
+++ b/static/css/modes/aprs.css
@@ -1,4 +1,201 @@
-/* APRS Status Bar Styles */
+/* APRS Function Bar (Stats Strip) Styles */
+.aprs-strip {
+ background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 6px 12px;
+ margin-bottom: 10px;
+ overflow-x: auto;
+}
+.aprs-strip-inner {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: max-content;
+}
+.aprs-strip .strip-stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4px 10px;
+ background: rgba(74, 158, 255, 0.05);
+ border: 1px solid rgba(74, 158, 255, 0.15);
+ border-radius: 4px;
+ min-width: 55px;
+}
+.aprs-strip .strip-stat:hover {
+ background: rgba(74, 158, 255, 0.1);
+ border-color: rgba(74, 158, 255, 0.3);
+}
+.aprs-strip .strip-value {
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--accent-cyan);
+ line-height: 1.2;
+}
+.aprs-strip .strip-label {
+ font-size: 8px;
+ font-weight: 600;
+ color: var(--text-dim);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-top: 1px;
+}
+.aprs-strip .strip-divider {
+ width: 1px;
+ height: 28px;
+ background: var(--border-color);
+ margin: 0 4px;
+}
+/* Signal stat coloring */
+.aprs-strip .signal-stat.good .strip-value { color: var(--accent-green); }
+.aprs-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); }
+.aprs-strip .signal-stat.poor .strip-value { color: var(--accent-red); }
+
+/* Controls */
+.aprs-strip .strip-control {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.aprs-strip .strip-select {
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 10px;
+ cursor: pointer;
+}
+.aprs-strip .strip-select:hover {
+ border-color: var(--accent-cyan);
+}
+.aprs-strip .strip-input-label {
+ font-size: 9px;
+ color: var(--text-muted);
+ font-weight: 600;
+}
+.aprs-strip .strip-input {
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-size: 10px;
+ width: 50px;
+ text-align: center;
+}
+.aprs-strip .strip-input:hover,
+.aprs-strip .strip-input:focus {
+ border-color: var(--accent-cyan);
+ outline: none;
+}
+
+/* Tool Status Indicators */
+.aprs-strip .strip-tools {
+ display: flex;
+ gap: 4px;
+}
+.aprs-strip .strip-tool {
+ font-size: 9px;
+ font-weight: 600;
+ padding: 3px 6px;
+ border-radius: 3px;
+ background: rgba(255, 59, 48, 0.2);
+ color: var(--accent-red);
+ border: 1px solid rgba(255, 59, 48, 0.3);
+}
+.aprs-strip .strip-tool.ok {
+ background: rgba(0, 255, 136, 0.1);
+ color: var(--accent-green);
+ border-color: rgba(0, 255, 136, 0.3);
+}
+
+/* Buttons */
+.aprs-strip .strip-btn {
+ background: rgba(74, 158, 255, 0.1);
+ border: 1px solid rgba(74, 158, 255, 0.2);
+ color: var(--text-primary);
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ white-space: nowrap;
+}
+.aprs-strip .strip-btn:hover:not(:disabled) {
+ background: rgba(74, 158, 255, 0.2);
+ border-color: rgba(74, 158, 255, 0.4);
+}
+.aprs-strip .strip-btn.primary {
+ background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%);
+ border: none;
+ color: #000;
+}
+.aprs-strip .strip-btn.primary:hover:not(:disabled) {
+ filter: brightness(1.1);
+}
+.aprs-strip .strip-btn.stop {
+ background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%);
+ border: none;
+ color: #fff;
+}
+.aprs-strip .strip-btn.stop:hover:not(:disabled) {
+ filter: brightness(1.1);
+}
+.aprs-strip .strip-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Status indicator */
+.aprs-strip .strip-status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ background: rgba(0,0,0,0.2);
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+.aprs-strip .status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--text-muted);
+}
+.aprs-strip .status-dot.listening {
+ background: var(--accent-cyan);
+ animation: aprs-strip-pulse 1.5s ease-in-out infinite;
+}
+.aprs-strip .status-dot.tracking {
+ background: var(--accent-green);
+ animation: aprs-strip-pulse 1.5s ease-in-out infinite;
+}
+.aprs-strip .status-dot.error {
+ background: var(--accent-red);
+}
+@keyframes aprs-strip-pulse {
+ 0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; }
+ 50% { opacity: 0.6; box-shadow: none; }
+}
+
+/* Time display */
+.aprs-strip .strip-time {
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 10px;
+ color: var(--text-muted);
+ padding: 4px 8px;
+ background: rgba(0,0,0,0.2);
+ border-radius: 4px;
+ white-space: nowrap;
+}
+
+/* APRS Status Bar Styles (Sidebar - legacy) */
.aprs-status-bar {
margin-top: 12px;
padding: 10px;
diff --git a/templates/index.html b/templates/index.html
index cd0a888..2c2e72a 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -726,6 +726,63 @@
+
+
+
+
+
+
+ MHz
+
+
+
+ STATIONS
+
+
+
+ PACKETS
+
+
+
+
+
+
+
+
+ GAIN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -5958,28 +6015,29 @@
fetch('/aprs/tools')
.then(r => r.json())
.then(data => {
- const direwolfStatus = document.getElementById('direwolfStatus');
- const multimonStatus = document.getElementById('aprsMultimonStatus');
+ // Update function bar tool indicators
+ const direwolfEl = document.getElementById('aprsStripDirewolf');
+ const multimonEl = document.getElementById('aprsStripMultimon');
- if (direwolfStatus) {
- direwolfStatus.textContent = data.direwolf ? 'OK' : 'Missing';
- direwolfStatus.className = 'tool-status ' + (data.direwolf ? 'ok' : 'missing');
+ if (direwolfEl) {
+ direwolfEl.className = 'strip-tool' + (data.direwolf ? ' ok' : '');
+ direwolfEl.title = 'direwolf: ' + (data.direwolf ? 'OK' : 'Missing');
}
- if (multimonStatus) {
- multimonStatus.textContent = data.multimon_ng ? 'OK' : 'Missing';
- multimonStatus.className = 'tool-status ' + (data.multimon_ng ? 'ok' : 'missing');
+ if (multimonEl) {
+ multimonEl.className = 'strip-tool' + (data.multimon_ng ? ' ok' : '');
+ multimonEl.title = 'multimon-ng: ' + (data.multimon_ng ? 'OK' : 'Missing');
}
})
.catch(() => {
- const direwolfStatus = document.getElementById('direwolfStatus');
- const multimonStatus = document.getElementById('aprsMultimonStatus');
- if (direwolfStatus) {
- direwolfStatus.textContent = 'Error';
- direwolfStatus.className = 'tool-status missing';
+ const direwolfEl = document.getElementById('aprsStripDirewolf');
+ const multimonEl = document.getElementById('aprsStripMultimon');
+ if (direwolfEl) {
+ direwolfEl.className = 'strip-tool';
+ direwolfEl.title = 'direwolf: Error';
}
- if (multimonStatus) {
- multimonStatus.textContent = 'Error';
- multimonStatus.className = 'tool-status missing';
+ if (multimonEl) {
+ multimonEl.className = 'strip-tool';
+ multimonEl.title = 'multimon-ng: Error';
}
});
}
@@ -5997,45 +6055,51 @@
maxZoom: 19
}).addTo(aprsMap);
- // Update time display
+ // Update time display (both map header and function bar)
setInterval(() => {
+ const now = new Date();
+ const timeStr = now.toLocaleTimeString('en-US', {hour12: false});
+ const utcStr = now.toUTCString().slice(17, 25) + ' UTC';
+
const timeEl = document.getElementById('aprsMapTime');
- if (timeEl) {
- timeEl.textContent = new Date().toLocaleTimeString('en-US', {hour12: false});
- }
+ if (timeEl) timeEl.textContent = timeStr;
+
+ const stripTimeEl = document.getElementById('aprsStripTime');
+ if (stripTimeEl) stripTimeEl.textContent = utcStr;
}, 1000);
}
function updateAprsStatus(state, freq) {
- const statusBar = document.getElementById('aprsStatusBar');
- const statusDot = document.getElementById('aprsStatusDot');
- const statusText = document.getElementById('aprsStatusText');
- const freqEl = document.getElementById('aprsStatusFreq');
+ // Update function bar status
+ const stripDot = document.getElementById('aprsStripDot');
+ const stripStatus = document.getElementById('aprsStripStatus');
+ const stripFreq = document.getElementById('aprsStripFreq');
- statusBar.style.display = 'block';
- statusDot.className = 'aprs-status-dot ' + state;
- statusText.textContent = state.toUpperCase();
-
- if (freq) {
- freqEl.textContent = freq + ' MHz';
+ if (stripDot) {
+ stripDot.className = 'status-dot ' + state;
}
-
- // Update color based on state
- if (state === 'listening') {
- statusText.style.color = 'var(--accent-cyan)';
- } else if (state === 'tracking') {
- statusText.style.color = 'var(--accent-green)';
- } else if (state === 'error') {
- statusText.style.color = 'var(--accent-red)';
- } else {
- statusText.style.color = 'var(--text-muted)';
+ if (stripStatus) {
+ stripStatus.textContent = state.toUpperCase();
+ if (state === 'listening') {
+ stripStatus.style.color = 'var(--accent-cyan)';
+ } else if (state === 'tracking') {
+ stripStatus.style.color = 'var(--accent-green)';
+ } else if (state === 'error') {
+ stripStatus.style.color = 'var(--accent-red)';
+ } else {
+ stripStatus.style.color = '';
+ }
+ }
+ if (freq && stripFreq) {
+ stripFreq.textContent = freq;
}
}
function startAprs() {
- const region = document.getElementById('aprsRegion').value;
+ // Get values from function bar controls
+ const region = document.getElementById('aprsStripRegion').value;
const device = getSelectedDevice();
- const gain = document.getElementById('aprsGain').value;
+ const gain = document.getElementById('aprsStripGain').value;
fetch('/aprs/start', {
method: 'POST',
@@ -6048,17 +6112,21 @@
isAprsRunning = true;
aprsPacketCount = 0;
aprsStationCount = 0;
- document.getElementById('startAprsBtn').style.display = 'none';
- document.getElementById('stopAprsBtn').style.display = 'block';
+ // Update function bar buttons
+ document.getElementById('aprsStripStartBtn').style.display = 'none';
+ document.getElementById('aprsStripStopBtn').style.display = 'inline-block';
+ // Update map status
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
- // Update sidebar status bar
+ // Update function bar status
updateAprsStatus('listening', data.frequency);
- document.getElementById('aprsStatusStations').textContent = '0';
- document.getElementById('aprsStatusPackets').textContent = '0';
- // Show signal meter
- document.getElementById('aprsSignalMeter').style.display = 'block';
- resetAprsMeter();
+ // Reset function bar stats
+ document.getElementById('aprsStripStations').textContent = '0';
+ document.getElementById('aprsStripPackets').textContent = '0';
+ document.getElementById('aprsStripSignal').textContent = '--';
+ // Disable controls while running
+ document.getElementById('aprsStripRegion').disabled = true;
+ document.getElementById('aprsStripGain').disabled = true;
startAprsMeterCheck();
startAprsStream();
} else {
@@ -6077,13 +6145,24 @@
.then(r => r.json())
.then(data => {
isAprsRunning = false;
- document.getElementById('startAprsBtn').style.display = 'block';
- document.getElementById('stopAprsBtn').style.display = 'none';
- // Hide sidebar status bar and signal meter
- document.getElementById('aprsStatusBar').style.display = 'none';
- document.getElementById('aprsSignalMeter').style.display = 'none';
+ // Update function bar buttons
+ document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
+ document.getElementById('aprsStripStopBtn').style.display = 'none';
+ // Update map status
document.getElementById('aprsMapStatus').textContent = 'STANDBY';
document.getElementById('aprsMapStatus').style.color = '';
+ // Reset function bar status
+ updateAprsStatus('standby');
+ document.getElementById('aprsStripFreq').textContent = '--';
+ document.getElementById('aprsStripSignal').textContent = '--';
+ // Re-enable controls
+ document.getElementById('aprsStripRegion').disabled = false;
+ document.getElementById('aprsStripGain').disabled = false;
+ // Remove signal quality class
+ const signalStat = document.getElementById('aprsStripSignalStat');
+ if (signalStat) {
+ signalStat.classList.remove('good', 'warning', 'poor');
+ }
// Stop meter check interval
stopAprsMeterCheck();
if (aprsEventSource) {
@@ -6101,17 +6180,17 @@
const data = JSON.parse(e.data);
if (data.type === 'aprs') {
aprsPacketCount++;
+ // Update map footer and function bar
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
- // Update sidebar status bar
- document.getElementById('aprsStatusPackets').textContent = aprsPacketCount;
+ document.getElementById('aprsStripPackets').textContent = aprsPacketCount;
// Switch to tracking state on first packet
- const dot = document.getElementById('aprsStatusDot');
+ const dot = document.getElementById('aprsStripDot');
if (dot && !dot.classList.contains('tracking')) {
updateAprsStatus('tracking');
}
processAprsPacket(data);
} else if (data.type === 'meter') {
- // Update signal meter
+ // Update signal indicator in function bar
updateAprsMeter(data.level);
}
};
@@ -6125,59 +6204,41 @@
// Signal Meter Functions
function resetAprsMeter() {
aprsMeterLastUpdate = 0;
- const bar = document.getElementById('aprsMeterBar');
- const value = document.getElementById('aprsMeterValue');
- const burst = document.getElementById('aprsMeterBurst');
- const status = document.getElementById('aprsMeterStatus');
- if (bar) {
- bar.style.width = '0%';
- bar.classList.remove('no-signal');
- }
- if (value) value.textContent = '--';
- if (burst) burst.style.display = 'none';
- if (status) {
- status.textContent = 'Waiting for signal...';
- status.className = 'aprs-meter-status';
- }
+ // Reset function bar signal indicator
+ const signalEl = document.getElementById('aprsStripSignal');
+ const signalStat = document.getElementById('aprsStripSignalStat');
+ if (signalEl) signalEl.textContent = '--';
+ if (signalStat) signalStat.classList.remove('good', 'warning', 'poor');
}
function updateAprsMeter(level) {
aprsMeterLastUpdate = Date.now();
- const bar = document.getElementById('aprsMeterBar');
- const value = document.getElementById('aprsMeterValue');
- const burst = document.getElementById('aprsMeterBurst');
- const status = document.getElementById('aprsMeterStatus');
- if (bar) {
- bar.style.width = level + '%';
- bar.classList.remove('no-signal');
- }
- if (value) value.textContent = level;
+ // Update function bar signal indicator
+ const signalEl = document.getElementById('aprsStripSignal');
+ const signalStat = document.getElementById('aprsStripSignalStat');
- // Show burst indicator for high levels (>70)
- if (burst) {
- if (level > 70) {
- burst.style.display = 'inline';
- // Remove and re-add to trigger animation
- burst.style.animation = 'none';
- burst.offsetHeight; // Trigger reflow
- burst.style.animation = null;
+ if (signalEl) {
+ // Show signal level as bars
+ if (level >= 60) {
+ signalEl.textContent = '●●●';
+ } else if (level >= 30) {
+ signalEl.textContent = '●●○';
+ } else if (level >= 10) {
+ signalEl.textContent = '●○○';
} else {
- burst.style.display = 'none';
+ signalEl.textContent = '○○○';
}
}
- // Update status text
- if (status) {
- status.className = 'aprs-meter-status active';
- if (level < 10) {
- status.textContent = 'Low signal / noise floor';
- } else if (level < 30) {
- status.textContent = 'Weak signal detected';
- } else if (level < 60) {
- status.textContent = 'Moderate signal';
+ if (signalStat) {
+ signalStat.classList.remove('good', 'warning', 'poor');
+ if (level >= 60) {
+ signalStat.classList.add('good');
+ } else if (level >= 30) {
+ signalStat.classList.add('warning');
} else {
- status.textContent = 'Strong signal / packet burst';
+ signalStat.classList.add('poor');
}
}
}
@@ -6187,14 +6248,12 @@
aprsMeterCheckInterval = setInterval(function() {
if (aprsMeterLastUpdate > 0 && (Date.now() - aprsMeterLastUpdate) > APRS_METER_TIMEOUT) {
// No meter updates for 5 seconds - show no-signal state
- const bar = document.getElementById('aprsMeterBar');
- const status = document.getElementById('aprsMeterStatus');
- const burst = document.getElementById('aprsMeterBurst');
- if (bar) bar.classList.add('no-signal');
- if (burst) burst.style.display = 'none';
- if (status) {
- status.textContent = 'No RF activity / silence';
- status.className = 'aprs-meter-status no-signal';
+ const signalEl = document.getElementById('aprsStripSignal');
+ const signalStat = document.getElementById('aprsStripSignalStat');
+ if (signalEl) signalEl.textContent = '○○○';
+ if (signalStat) {
+ signalStat.classList.remove('good', 'warning');
+ signalStat.classList.add('poor');
}
}
}, 1000);
@@ -6250,8 +6309,9 @@
} else {
// Create new marker
aprsStationCount++;
+ // Update map footer and function bar
document.getElementById('aprsStationCount').textContent = aprsStationCount;
- document.getElementById('aprsStatusStations').textContent = aprsStationCount;
+ document.getElementById('aprsStripStations').textContent = aprsStationCount;
const icon = L.divIcon({
className: 'aprs-marker',
diff --git a/templates/partials/modes/aprs.html b/templates/partials/modes/aprs.html
index 7b0ef89..9dceb97 100644
--- a/templates/partials/modes/aprs.html
+++ b/templates/partials/modes/aprs.html
@@ -9,63 +9,8 @@
Amateur Radio
APRS operates on 144.390 MHz (N. America) or 144.800 MHz (Europe). Decodes position, weather, and messages from ham radio operators.
-
-
-
Configuration
-
-
-
-
-
-
-