feat: Cluster alert implemented

This commit is contained in:
2026-07-03 19:08:50 +02:00
parent 8740a4ba66
commit 3e199f9ab6
7 changed files with 823 additions and 0 deletions
+38
View File
@@ -67,6 +67,7 @@ import { ClusterGrid } from '@/components/ClusterGrid';
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
import { NetControlPanel } from '@/components/NetControlPanel';
import { AlertsModal } from '@/components/AlertsModal';
import { BulkEditModal } from '@/components/BulkEditModal';
import { ChatPanel, type ChatMsg, type ChatPresence } from '@/components/ChatPopover';
import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel';
@@ -730,6 +731,8 @@ export default function App() {
}
const chatShown = chatOpen && chatAvailable;
const [alertsOpen, setAlertsOpen] = useState(false); // Alert management modal
// NET Control tab — enabled from Tools (persisted; once on it's a tab like Cluster).
const [netEnabled, setNetEnabled] = useState(() => localStorage.getItem('opslog.netEnabled') === '1');
useEffect(() => { localStorage.setItem('opslog.netEnabled', netEnabled ? '1' : '0'); }, [netEnabled]);
@@ -1100,6 +1103,35 @@ export default function App() {
return () => { off(); };
}, [showToast]);
// DX-cluster spot alerts: a matched rule fires here. Play a beep (WebAudio, no
// asset needed — CSP-safe) and/or show a toast, per the rule's chosen actions.
useEffect(() => {
const off = EventsOn('alert:fired', (p: any) => {
if (p?.sound) {
try {
const AC = (window as any).AudioContext || (window as any).webkitAudioContext;
const ctx = new AC();
const beep = (freq: number, at: number, dur: number) => {
const o = ctx.createOscillator(); const g = ctx.createGain();
o.type = 'sine'; o.frequency.value = freq;
o.connect(g); g.connect(ctx.destination);
g.gain.setValueAtTime(0.0001, ctx.currentTime + at);
g.gain.exponentialRampToValueAtTime(0.25, ctx.currentTime + at + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + at + dur);
o.start(ctx.currentTime + at); o.stop(ctx.currentTime + at + dur + 0.02);
};
beep(880, 0, 0.14); beep(1320, 0.16, 0.18); // two-tone chirp
window.setTimeout(() => ctx.close().catch(() => {}), 600);
} catch { /* audio blocked — ignore */ }
}
if (p?.visual) {
const call = String(p?.call ?? ''); const band = String(p?.band ?? ''); const rule = String(p?.rule ?? 'alert');
showToast(`🔔 ${rule}: ${call}${band ? ` on ${band}` : ''}${p?.country ? `${p.country}` : ''}`);
}
});
return () => { off(); };
}, [showToast]);
// Poll PstRotator for the live antenna heading (status bar). Cheap when the
// rotator is disabled (the backend just reads settings and returns).
useEffect(() => {
@@ -2209,6 +2241,7 @@ export default function App() {
{ type: 'item', label: cwEnabled ? '✓ CW decoder (RX audio)' : 'CW decoder (RX audio)', action: 'tools.cwdecoder' },
{ type: 'separator' },
{ type: 'item', label: netEnabled ? '✓ NET Control' : 'NET Control', action: 'tools.net' },
{ type: 'item', label: 'Alert management…', action: 'tools.alerts' },
{ type: 'separator' },
// Maintenance — bumped here while we only have one entry. Will move
// to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
@@ -2237,6 +2270,7 @@ export default function App() {
case 'tools.dvk': setDvkEnabled((v) => !v); break;
case 'tools.cwdecoder': toggleCwDecoder(); break;
case 'tools.net': setNetEnabled((v) => { const nv = !v; if (nv) setActiveTab('net'); return nv; }); break;
case 'tools.alerts': setAlertsOpen(true); break;
case 'tools.refreshCty': refreshCtyDat(); break;
case 'tools.downloadRefs': downloadRefs(); break;
case 'help.about': setShowAbout(true); break;
@@ -4101,6 +4135,10 @@ export default function App() {
/>
)}
{alertsOpen && (
<AlertsModal onClose={() => setAlertsOpen(false)} bands={bands} modes={modes} countries={countries} />
)}
<AutoEQSL
onSent={(call) => showToast(`OpsLog QSL sent to ${call}`)}
onError={(msg) => showToast(msg)}