From 20dc3e83a63c3500a6d7c38d5547111090139a7a Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Thu, 6 Aug 2026 10:30:34 +0200 Subject: [PATCH] ui(stats): prettier slot drill-down + click a callsign to edit the QSO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pop-up listing the QSOs behind a Stats band/mode square gets a cleaner header (band chip, hint), banded/hover rows and roomier spacing. Callsigns are now clickable and open the QSO editor (App.openEdit threaded through DetailsPanel → BandSlotGrid → SlotQSOModal as onEditQso), matching the double-click-to-edit of the log grids. --- changelog.json | 8 ++- frontend/src/App.tsx | 1 + frontend/src/components/BandSlotGrid.tsx | 73 ++++++++++++++---------- frontend/src/components/DetailsPanel.tsx | 5 +- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/changelog.json b/changelog.json index 32ac636..6a702e1 100644 --- a/changelog.json +++ b/changelog.json @@ -7,14 +7,16 @@ "PowerGenius XL: the fan mode (Standard / Contest / Broadcast) changes on the amp again. A previous fix dropped the \"setup\" prefix the amp requires (\"setup fanmode=…\"), so the amp rejected the command and the mode snapped straight back. The correct command is restored.", "Performance: fixed a slowdown introduced in 0.23.6. To dim the \"represents nothing\" spots, the DX Cluster grid was redrawing EVERY row on each spot-status update, which pegged the CPU on a busy cluster (some PCs became sluggish). The dimming now updates with a light per-cell refresh instead — same look, no more churn.", "DX Cluster: after you log a QSO, the spots update — a callsign, entity, POTA, prefix or band/mode slot you just worked stops showing its NEW badge, instead of staying \"new\" until a restart. Kept fast: it re-evaluates only while the cluster is actually on screen (nothing runs otherwise — a QSO logged while it's hidden refreshes once when you next open it), and it's debounced so a quick run doesn't re-scan on every QSO.", - "Station Control relays (WebSwitch / KMTronic / Dingtian): the Host field now accepts a full URL, so you can reach a network relay board through an HTTPS reverse proxy — e.g. https://relay.yourdomain.com fronted by Nginx Proxy Manager. Before, OpsLog always used plain http://, so a board on the LAN behind a proxy (when your 80/443 already go elsewhere) was unreachable from outside." + "Station Control relays (WebSwitch / KMTronic / Dingtian): the Host field now accepts a full URL, so you can reach a network relay board through an HTTPS reverse proxy — e.g. https://relay.yourdomain.com.", + "Stats slot drill-down: the pop-up listing the QSOs behind a band/mode square looks tidier (banded rows, cleaner header), and you can now click a callsign in it to open that QSO for editing." ], "fr": [ "Édition de QSO : ajout du bouton QRZ ↗ à côté de l'indicatif, comme dans le formulaire de saisie — un clic ouvre le profil qrz.com de la station.", "PowerGenius XL : le mode ventilateur (Standard / Contest / Broadcast) change de nouveau sur l'ampli. Un correctif précédent avait retiré le préfixe « setup » exigé par l'ampli (« setup fanmode=… »), qui rejetait donc la commande et le mode revenait aussitôt en arrière. La bonne commande est rétablie.", "Performance : correction d'un ralentissement apparu en 0.23.6. Pour atténuer les spots « qui ne représentent rien », la grille du DX Cluster redessinait TOUTES les lignes à chaque mise à jour de statut, ce qui saturait le CPU sur un cluster actif (des PC devenaient lents). L'atténuation se met désormais à jour via un rafraîchissement léger par cellule — même rendu, sans le brassage.", "DX Cluster : après avoir loggué un QSO, les spots se mettent à jour — un indicatif, une entité, un POTA, un préfixe ou un slot bande/mode que vous venez de contacter cesse d'afficher son badge NEW, au lieu de rester « new » jusqu'au redémarrage. Reste rapide : la réévaluation n'a lieu que si le cluster est affiché (sinon rien ne tourne — un QSO loggué cluster caché se rafraîchit une fois à sa réouverture), et c'est débouncé pour ne pas re-scanner à chaque QSO en série.", - "Relais du Contrôle station (WebSwitch / KMTronic / Dingtian) : le champ Hôte accepte désormais une URL complète, pour joindre une carte relais réseau via un reverse proxy HTTPS — ex. https://relais.tondomaine.com derrière Nginx Proxy Manager. Avant, OpsLog forçait http://, donc une carte sur le LAN derrière un proxy (quand vos 80/443 vont déjà ailleurs) était injoignable de l'extérieur." + "Relais du Contrôle station (WebSwitch / KMTronic / Dingtian) : le champ Hôte accepte désormais une URL complète ex. https://relais.tondomaine.com derrière Nginx Proxy Manager. Avant, OpsLog forçait http://, donc une carte sur le LAN derrière un proxy (quand vos 80/443 vont déjà ailleurs) était injoignable de l'extérieur.", + "Détail d’un slot Stats : la fenêtre listant les QSO derrière une case bande/mode est plus soignée (lignes alternées, en-tête plus propre), et vous pouvez maintenant cliquer un indicatif pour ouvrir ce QSO en édition." ] }, { @@ -885,4 +887,4 @@ "Ce résumé « Nouveautés » s'affiche désormais au premier lancement après chaque mise à jour." ] } -] +] \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7f5385e..5101eaa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5653,6 +5653,7 @@ export default function App() { band={band} mode={mode} bands={bands} + onEditQso={openEdit} {...(!callsign.trim() && selQso ? { slotCall: selQso.call, slotBand: selQso.band, slotMode: selQso.mode, slotWb: selWb, slotWbBusy: selWbBusy, diff --git a/frontend/src/components/BandSlotGrid.tsx b/frontend/src/components/BandSlotGrid.tsx index cf34665..40775be 100644 --- a/frontend/src/components/BandSlotGrid.tsx +++ b/frontend/src/components/BandSlotGrid.tsx @@ -22,6 +22,9 @@ interface Props { // Set when the matrix is showing a QSO picked in the log grid rather than the // entry form — labelled so the two can never be confused. forCall?: string; + // Open the QSO editor for a contact — makes callsigns in the cell drill-down + // clickable. Threaded down from App.openEdit. + onEditQso?: (id: number) => void; } // Compact column label for a band tag: keep the classic V/U for 2m/70cm, @@ -93,7 +96,7 @@ function cellTitle(band: string, cls: string, status: string, current: boolean): return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`; } -export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true, lat, lon, forCall }: Props) { +export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true, lat, lon, forCall, onEditQso }: Props) { // Cell drill-down: which band+class the operator clicked, or null. const [slot, setSlot] = useState<{ band: string; cls: string } | null>(null); // Columns from the operator's configured bands (so the matrix shows only the @@ -340,6 +343,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal band={slot.band} cls={slot.cls} onClose={() => setSlot(null)} + onEdit={onEditQso} /> )} @@ -351,8 +355,8 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal // in the entity, because that is the pair of facts the cell's colour encodes. // The call's own QSOs are marked so the two never blur together. -function SlotQSOModal({ call, dxcc, entity, band, cls, onClose }: { - call: string; dxcc: number; entity: string; band: string; cls: string; onClose: () => void; +function SlotQSOModal({ call, dxcc, entity, band, cls, onClose, onEdit }: { + call: string; dxcc: number; entity: string; band: string; cls: string; onClose: () => void; onEdit?: (id: number) => void; }) { const [rows, setRows] = useState(null); const [err, setErr] = useState(''); @@ -376,18 +380,19 @@ function SlotQSOModal({ call, dxcc, entity, band, cls, onClose }: { }, [onClose]); return ( -
-
+
e.stopPropagation()}> -
- - {band} · {cls} - {entity && — {entity}} - - - {rows ? `${rows.length} QSO` : ''} - -
@@ -400,30 +405,40 @@ function SlotQSOModal({ call, dxcc, entity, band, cls, onClose }: { ) : rows.length === 0 ? (

No QSOs.

) : ( - - +
+ - - - - - - + + + + + + {rows.map((q, i) => { const cfm = q.lotw_rcvd === 'Y' || q.eqsl_rcvd === 'Y' || q.qsl_rcvd === 'Y'; + const mine = q.callsign === call; return ( - - + - - - - - + + + + + ); })} diff --git a/frontend/src/components/DetailsPanel.tsx b/frontend/src/components/DetailsPanel.tsx index d9109e3..749809d 100644 --- a/frontend/src/components/DetailsPanel.tsx +++ b/frontend/src/components/DetailsPanel.tsx @@ -85,6 +85,8 @@ interface Props { // When the WinKeyer is active, F1-F12 fire macros, so the tab shortcut is // shown as Ctrl+F1…F5 instead of F1…F5. keyerActive?: boolean; + // Open the QSO editor — makes callsigns clickable in the band-slot drill-down. + onEditQso?: (id: number) => void; } export type TabName = 'stats' | 'info' | 'awards' | 'my' | 'extended'; @@ -147,7 +149,7 @@ function Field({ label, span = 1, className, children }: { label: string; span?: ); } -export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, name, country, comment, note, details, onChange, wb, wbBusy, band, mode, bands, slotCall, slotBand, slotMode, slotWb, slotWbBusy, tab, onTab, keyerActive }: Props) { +export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, name, country, comment, note, details, onChange, wb, wbBusy, band, mode, bands, slotCall, slotBand, slotMode, slotWb, slotWbBusy, tab, onTab, keyerActive, onEditQso }: Props) { const { t } = useI18n(); const [internalOpen, setInternalOpen] = useState('stats'); const open = tab ?? internalOpen; // controlled when `tab` is provided @@ -288,6 +290,7 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth, bands={bands} hasCall={slotCall ? true : callsign.trim() !== ''} forCall={slotCall} + onEditQso={onEditQso} lat={dxLL?.lat} lon={dxLL?.lon} /> )}
Date UTCCallsignModeFreqNameCfmDate UTCCallsignModeFreqNameCfm
+
{String(q.qso_date ?? '').slice(0, 16).replace('T', ' ')} {q.callsign}{q.mode}{q.freq_hz ? (q.freq_hz / 1e6).toFixed(3) : ''}{q.name}{cfm ? : ''} + {onEdit ? ( + + ) : ( + {q.callsign} + )} + {q.mode}{q.freq_hz ? (q.freq_hz / 1e6).toFixed(3) : ''}{q.name}{cfm ? : ·}