From 39f095ae75e2d1322e4606595d6fddf1656a8c59 Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 7 Jul 2026 19:52:52 +0200 Subject: [PATCH] feat: Alerts are persistent for the 3 last ones for 10minutes --- frontend/src/App.tsx | 48 +++++++++++++++++++++++++++++++++++++++ frontend/src/lib/i18n.tsx | 2 ++ 2 files changed, 50 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6ac290..b31a6b4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -793,6 +793,19 @@ export default function App() { const [alertsOpen, setAlertsOpen] = useState(false); // Alert management modal + // Persistent spot-alert tray: keep the last few fired alerts visible (they used + // to vanish with the 3 s toast, lost if you were on another window). Each stays + // ~10 min and is clickable to tune the rig to its freq/mode. + type RecentAlert = { id: number; rule: string; call: string; band: string; mode: string; freq_hz: number; country: string; comment: string; at: number }; + const [recentAlerts, setRecentAlerts] = useState([]); + const ALERT_TTL_MS = 10 * 60 * 1000; + useEffect(() => { + const id = window.setInterval(() => { + setRecentAlerts((prev) => { const cut = Date.now() - ALERT_TTL_MS; const n = prev.filter((a) => a.at >= cut); return n.length === prev.length ? prev : n; }); + }, 20000); + return () => window.clearInterval(id); + }, []); + // 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]); @@ -1197,9 +1210,17 @@ export default function App() { 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}` : ''}`); + // Also keep it in the persistent tray (clickable, ~10 min). + const a: RecentAlert = { + id: Date.now() + Math.random(), rule, call, band, + mode: String(p?.mode ?? ''), freq_hz: Number(p?.freq_hz ?? 0), + country: String(p?.country ?? ''), comment: String(p?.comment ?? ''), at: Date.now(), + }; + setRecentAlerts((prev) => [a, ...prev.filter((x) => !(x.call === a.call && x.band === a.band))].slice(0, 3)); } }); return () => { off(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [showToast]); // Poll PstRotator for the live antenna heading (status bar). Cheap when the @@ -4364,6 +4385,33 @@ export default function App() { setAlertsOpen(false)} bands={bands} modes={modes} countries={countries} /> )} + {/* Persistent spot-alert tray (top-right). Each card stays ~10 min; click it + to tune the rig to its freq/mode + fill the callsign; × dismisses it. */} + {recentAlerts.length > 0 && ( +
+ {recentAlerts.map((a) => ( +
+ + +
+ ))} +
+ )} + {showDuplicates && ( setShowDuplicates(false)} onDeleted={() => refresh()} /> )} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index eec41ea..0cd2bf5 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -21,6 +21,7 @@ const en: Dict = { 'tools.qslManager': 'QSL Manager…', 'tools.qslDesigner': 'QSL Card Designer…', 'tools.winkeyer': 'WinKeyer CW keyer', 'tools.dvk': 'Digital Voice Keyer', 'tools.cwDecoder': 'CW decoder (RX audio)', 'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode', + 'alert.tuneHint': 'Click to tune the rig to this spot (freq + mode) and fill the call', 'alert.dismiss': 'Dismiss', 'menu.help': 'Help', 'help.about': 'About OpsLog', 'tools.duplicates': 'Find duplicates…', // Duplicates modal 'dup.title': 'Find duplicates', 'dup.scanning': 'Scanning the log…', 'dup.none': 'No duplicates found. 🎉', @@ -228,6 +229,7 @@ const fr: Dict = { 'tools.qslManager': 'Gestionnaire QSL…', 'tools.qslDesigner': 'Créateur de carte QSL…', 'tools.winkeyer': 'Manipulateur CW WinKeyer', 'tools.dvk': 'Manipulateur vocal numérique', 'tools.cwDecoder': 'Décodeur CW (audio RX)', 'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest', + 'alert.tuneHint': 'Cliquer pour accorder la radio sur ce spot (fréq + mode) et remplir l\'indicatif', 'alert.dismiss': 'Fermer', 'menu.help': 'Aide', 'help.about': 'À propos d\'OpsLog', 'tools.duplicates': 'Trouver les doublons…', 'dup.title': 'Trouver les doublons', 'dup.scanning': 'Analyse du journal…', 'dup.none': 'Aucun doublon trouvé. 🎉', 'dup.hint': 'Chaque groupe est le même contact enregistré plusieurs fois (indicatif + bande + mode). Le premier (le plus ancien) est laissé décoché ; coche ceux à supprimer.',