Add WeFax 24h broadcast timeline and improve start button feedback

Flash the Start button itself with amber pulse when clicked without a
station selected, and show "Select Station" in the strip status text
right next to the button so the error is immediately visible.

Add a 24-hour timeline bar with broadcast window markers, red UTC time
cursor, and countdown boxes (HRS/MIN/SEC) that tick down to the next
broadcast. Broadcasts show as amber blocks on the timeline track with
imminent/active visual states matching the weather satellite pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-24 15:17:01 +00:00
parent 085a6177f9
commit 2da8dca167
3 changed files with 381 additions and 0 deletions

View File

@@ -74,6 +74,13 @@
.wefax-strip-btn.start { color: #ffaa00; border-color: #ffaa0044; }
.wefax-strip-btn.start:hover { background: #ffaa0015; border-color: #ffaa00; }
.wefax-strip-btn.start.wefax-strip-btn-error {
border-color: #ffaa00;
color: #ffaa00;
box-shadow: 0 0 8px rgba(255, 170, 0, 0.3);
animation: wefax-pulse 0.6s ease-in-out 3;
}
.wefax-strip-btn.stop { color: #f44; border-color: #f4444444; }
.wefax-strip-btn.stop:hover { background: #f4441a; border-color: #f44; }
@@ -438,6 +445,153 @@
.wefax-gallery-action:hover { color: #fff; }
.wefax-gallery-action.delete:hover { color: #f44; }
/* --- Countdown Bar + Timeline --- */
.wefax-countdown-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
background: var(--bg-secondary, #141820);
border: 1px solid var(--border-color, #1e2a3a);
border-radius: 6px;
margin-bottom: 12px;
}
.wefax-countdown-next {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.wefax-countdown-boxes {
display: flex;
gap: 4px;
}
.wefax-countdown-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 8px;
background: var(--bg-primary, #0d1117);
border: 1px solid var(--border-color, #2a3040);
border-radius: 4px;
min-width: 40px;
}
.wefax-countdown-box.imminent {
border-color: #ffaa00;
box-shadow: 0 0 8px rgba(255, 170, 0, 0.2);
}
.wefax-countdown-box.active {
border-color: #ffaa00;
box-shadow: 0 0 8px rgba(255, 170, 0, 0.3);
animation: wefax-glow 1.5s ease-in-out infinite;
}
@keyframes wefax-glow {
0%, 100% { box-shadow: 0 0 8px rgba(255, 170, 0, 0.3); }
50% { box-shadow: 0 0 16px rgba(255, 170, 0, 0.5); }
}
.wefax-cd-value {
font-size: 16px;
font-weight: 700;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
color: var(--text-primary, #e0e0e0);
line-height: 1;
}
.wefax-cd-unit {
font-size: 8px;
color: var(--text-dim, #666);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.wefax-countdown-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.wefax-countdown-content {
font-size: 12px;
font-weight: 600;
color: #ffaa00;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wefax-countdown-detail {
font-size: 10px;
color: var(--text-dim, #666);
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wefax-timeline {
flex: 1;
position: relative;
height: 36px;
min-width: 200px;
}
.wefax-timeline-track {
position: absolute;
top: 4px;
left: 0;
right: 0;
height: 16px;
background: var(--bg-primary, #0d1117);
border: 1px solid var(--border-color, #2a3040);
border-radius: 3px;
overflow: hidden;
}
.wefax-timeline-broadcast {
position: absolute;
top: 0;
height: 100%;
background: rgba(255, 170, 0, 0.5);
border-radius: 2px;
cursor: default;
opacity: 0.8;
min-width: 2px;
}
.wefax-timeline-broadcast:hover {
opacity: 1;
}
.wefax-timeline-broadcast.active {
background: rgba(255, 170, 0, 0.85);
border: 1px solid #ffaa00;
}
.wefax-timeline-cursor {
position: absolute;
top: 2px;
width: 2px;
height: 20px;
background: #ff4444;
border-radius: 1px;
z-index: 2;
}
.wefax-timeline-labels {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
font-size: 8px;
color: var(--text-dim, #666);
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
/* --- Responsive --- */
@media (max-width: 768px) {
.wefax-main-row {

View File

@@ -16,6 +16,7 @@ var WeFax = (function () {
images: [],
selectedStation: null,
pollTimer: null,
countdownInterval: null,
};
// ---- Scope state ----
@@ -56,6 +57,7 @@ var WeFax = (function () {
function destroy() {
disconnectSSE();
stopScope();
stopCountdownTimer();
if (state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
@@ -101,6 +103,8 @@ var WeFax = (function () {
state.selectedStation = null;
renderFrequencyDropdown([]);
renderScheduleTimeline([]);
renderBroadcastTimeline([]);
stopCountdownTimer();
return;
}
@@ -115,6 +119,8 @@ var WeFax = (function () {
if (iocSel && station.ioc) iocSel.value = String(station.ioc);
if (lpmSel && station.lpm) lpmSel.value = String(station.lpm);
renderScheduleTimeline(station.schedule || []);
renderBroadcastTimeline(station.schedule || []);
startCountdownTimer();
}
}
@@ -676,6 +682,29 @@ var WeFax = (function () {
function flashStartError() {
setStatus('Select a station and frequency first');
// Flash the Start button itself (most visible feedback)
var startBtn = document.getElementById('wefaxStartBtn');
if (startBtn) {
startBtn.classList.add('wefax-strip-btn-error');
setTimeout(function () {
startBtn.classList.remove('wefax-strip-btn-error');
}, 2500);
}
// Show error in strip status text (right next to the button)
var stripStatus = document.getElementById('wefaxStripStatus');
if (stripStatus) {
var prevText = stripStatus.textContent;
stripStatus.textContent = 'Select Station';
stripStatus.style.color = '#ffaa00';
setTimeout(function () {
stripStatus.textContent = prevText || 'Idle';
stripStatus.style.color = '';
}, 2500);
}
// Also update the schedule panel status
var statusEl = document.getElementById('wefaxStatusText');
if (statusEl) {
statusEl.style.color = '#ffaa00';
@@ -685,6 +714,8 @@ var WeFax = (function () {
statusEl.style.fontWeight = '';
}, 2500);
}
// Flash station/frequency dropdowns
var stationSel = document.getElementById('wefaxStation');
var freqSel = document.getElementById('wefaxFrequency');
[stationSel, freqSel].forEach(function (el) {
@@ -698,6 +729,180 @@ var WeFax = (function () {
});
}
// ---- Broadcast Timeline + Countdown ----
function renderBroadcastTimeline(schedule) {
var bar = document.getElementById('wefaxCountdownBar');
var track = document.getElementById('wefaxTimelineTrack');
if (!bar || !track) return;
if (!schedule || schedule.length === 0) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
// Clear existing broadcast markers
var existing = track.querySelectorAll('.wefax-timeline-broadcast');
for (var i = 0; i < existing.length; i++) {
existing[i].parentNode.removeChild(existing[i]);
}
var now = new Date();
var nowMin = now.getUTCHours() * 60 + now.getUTCMinutes();
schedule.forEach(function (entry) {
var parts = entry.utc.split(':');
var startMin = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
var duration = entry.duration_min || 20;
var leftPct = (startMin / 1440) * 100;
var widthPct = (duration / 1440) * 100;
var block = document.createElement('div');
block.className = 'wefax-timeline-broadcast';
block.title = entry.utc + ' — ' + entry.content;
// Mark active broadcasts
var diff = nowMin - startMin;
if (diff >= 0 && diff < duration) {
block.classList.add('active');
}
block.style.left = leftPct + '%';
block.style.width = Math.max(widthPct, 0.3) + '%';
track.appendChild(block);
});
updateTimelineCursor();
}
function updateTimelineCursor() {
var cursor = document.getElementById('wefaxTimelineCursor');
if (!cursor) return;
var now = new Date();
var nowMin = now.getUTCHours() * 60 + now.getUTCMinutes() + now.getUTCSeconds() / 60;
cursor.style.left = ((nowMin / 1440) * 100) + '%';
}
function startCountdownTimer() {
stopCountdownTimer();
updateCountdown();
state.countdownInterval = setInterval(function () {
updateCountdown();
updateTimelineCursor();
}, 1000);
}
function updateCountdown() {
var station = state.selectedStation;
if (!station || !station.schedule || !station.schedule.length) return;
var now = new Date();
var nowMin = now.getUTCHours() * 60 + now.getUTCMinutes() + now.getUTCSeconds() / 60;
// Find next upcoming or currently active broadcast
var bestDiff = Infinity;
var bestEntry = null;
var isActive = false;
station.schedule.forEach(function (entry) {
var parts = entry.utc.split(':');
var startMin = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
var duration = entry.duration_min || 20;
// Check if currently active
var elapsed = nowMin - startMin;
if (elapsed < 0) elapsed += 1440;
if (elapsed >= 0 && elapsed < duration) {
bestEntry = entry;
bestDiff = 0;
isActive = true;
return;
}
// Time until start
var diff = startMin - nowMin;
if (diff < 0) diff += 1440;
if (diff < bestDiff) {
bestDiff = diff;
bestEntry = entry;
}
});
if (!bestEntry) return;
var hoursEl = document.getElementById('wefaxCdHours');
var minsEl = document.getElementById('wefaxCdMins');
var secsEl = document.getElementById('wefaxCdSecs');
var contentEl = document.getElementById('wefaxCountdownContent');
var detailEl = document.getElementById('wefaxCountdownDetail');
var boxes = document.getElementById('wefaxCountdownBoxes');
if (isActive) {
// Show "LIVE" countdown
var parts = bestEntry.utc.split(':');
var startMin2 = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
var duration2 = bestEntry.duration_min || 20;
var elapsed2 = nowMin - startMin2;
if (elapsed2 < 0) elapsed2 += 1440;
var remaining = duration2 - elapsed2;
var remTotalSec = Math.max(0, Math.floor(remaining * 60));
var h = Math.floor(remTotalSec / 3600);
var m = Math.floor((remTotalSec % 3600) / 60);
var s = remTotalSec % 60;
if (hoursEl) hoursEl.textContent = String(h).padStart(2, '0');
if (minsEl) minsEl.textContent = String(m).padStart(2, '0');
if (secsEl) secsEl.textContent = String(s).padStart(2, '0');
if (contentEl) contentEl.textContent = bestEntry.content;
if (detailEl) detailEl.textContent = 'LIVE — ' + bestEntry.utc + ' UTC';
// Set active class on boxes
if (boxes) {
var boxEls = boxes.querySelectorAll('.wefax-countdown-box');
for (var i = 0; i < boxEls.length; i++) {
boxEls[i].classList.remove('imminent');
boxEls[i].classList.add('active');
}
}
} else {
// Countdown to next
var totalSec = Math.max(0, Math.floor(bestDiff * 60));
var h2 = Math.floor(totalSec / 3600);
var m2 = Math.floor((totalSec % 3600) / 60);
var s2 = totalSec % 60;
if (hoursEl) hoursEl.textContent = String(h2).padStart(2, '0');
if (minsEl) minsEl.textContent = String(m2).padStart(2, '0');
if (secsEl) secsEl.textContent = String(s2).padStart(2, '0');
if (contentEl) contentEl.textContent = bestEntry.content;
if (detailEl) detailEl.textContent = 'Next at ' + bestEntry.utc + ' UTC';
// Set imminent class when < 10 min
if (boxes) {
var boxEls2 = boxes.querySelectorAll('.wefax-countdown-box');
var isImminent = bestDiff < 10;
for (var j = 0; j < boxEls2.length; j++) {
boxEls2[j].classList.remove('active');
if (isImminent) {
boxEls2[j].classList.add('imminent');
} else {
boxEls2[j].classList.remove('imminent');
}
}
}
}
}
function stopCountdownTimer() {
if (state.countdownInterval) {
clearInterval(state.countdownInterval);
state.countdownInterval = null;
}
}
// ---- Auto-Capture Scheduler ----
function checkSchedulerStatus() {

View File

@@ -2568,6 +2568,28 @@
</div>
</div>
<!-- Countdown + Timeline -->
<div class="wefax-countdown-bar" id="wefaxCountdownBar" style="display: none;">
<div class="wefax-countdown-next">
<div class="wefax-countdown-boxes" id="wefaxCountdownBoxes">
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdHours">--</span><span class="wefax-cd-unit">HRS</span></div>
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdMins">--</span><span class="wefax-cd-unit">MIN</span></div>
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdSecs">--</span><span class="wefax-cd-unit">SEC</span></div>
</div>
<div class="wefax-countdown-info" id="wefaxCountdownInfo">
<span class="wefax-countdown-content" id="wefaxCountdownContent">--</span>
<span class="wefax-countdown-detail" id="wefaxCountdownDetail">Select a station</span>
</div>
</div>
<div class="wefax-timeline" id="wefaxTimeline">
<div class="wefax-timeline-track" id="wefaxTimelineTrack"></div>
<div class="wefax-timeline-cursor" id="wefaxTimelineCursor"></div>
<div class="wefax-timeline-labels">
<span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
</div>
</div>
</div>
<!-- Audio Waveform Scope -->
<div id="wefaxScopePanel" style="display: none;">
<div style="background: #0a0a0a; border: 1px solid #2e2a1a; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">