From 2b6f1ba9d7f35e6a86fb69705a6b48b107053d4a Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 31 Aug 2026 18:57:44 +0200 Subject: [PATCH] feat(bandmap,dxped): drop the ctrl+wheel zoom, badge the news MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ctrl+wheel is the WINDOW zoom everywhere else in OpsLog, and one gesture that resizes the whole app over one panel and one band map over another is a gesture nobody can trust. The + and - buttons keep the zoom, and the footer hint stops advertising what is gone. The DX-World headlines now carry a chase badge like the announcements — but ENTITY-LEVEL only. A headline names no band, and asking the slot question without one answers 'new band' for every entity ever worked: the one mistake that costs a QSO. So the news says NEW DXCC or worked, and the sharper verdicts stay where the bands and modes are known. --- app_dxped.go | 65 +++++++++++++++++++- changelog.json | 8 ++- frontend/src/components/BandMap.tsx | 17 ++--- frontend/src/components/DXpeditionsPanel.tsx | 14 +++++ frontend/src/lib/i18n.tsx | 4 +- frontend/wailsjs/go/main/App.d.ts | 3 +- frontend/wailsjs/go/models.ts | 61 +++++++++--------- 7 files changed, 119 insertions(+), 53 deletions(-) diff --git a/app_dxped.go b/app_dxped.go index 599e189..a975859 100644 --- a/app_dxped.go +++ b/app_dxped.go @@ -119,9 +119,22 @@ func (a *App) judgeDXpeditions(list []DXpedition) { } } +// DXNews is one DX-World post plus what its callsigns are worth here. +type DXNews struct { + dxped.News + // Status is ENTITY-LEVEL only, and deliberately so: a headline announces no + // bands or modes, and asking the slot question without them would answer + // "new band" for every entity ever worked — the one mistake that costs a + // QSO. So it says "new" (entity never worked) or "worked", nothing finer; + // the announcements pane, which HAS the bands and modes, is where the + // sharper verdicts belong. + Status string `json:"status_chase"` + Unconfirmed bool `json:"unconfirmed"` +} + // GetDXWorldNews returns the DX-World headlines, with the callsigns mined out -// of each one so the reader can act on them. -func (a *App) GetDXWorldNews() ([]dxped.News, error) { +// of each one and judged against the log. +func (a *App) GetDXWorldNews() ([]DXNews, error) { if a.dxped == nil { a.dxped = dxped.New() } @@ -132,7 +145,53 @@ func (a *App) GetDXWorldNews() ([]dxped.News, error) { return nil, err } } - return news, nil + out := make([]DXNews, 0, len(news)) + for _, n := range news { + out = append(out, DXNews{News: n}) + } + a.judgeNews(out) + return out, nil +} + +// judgeNews fills the entity-level verdict for each headline. +// +// One query per callsign with NO band and NO mode: the only verdict that +// means anything in that state is "this entity has never been worked". Every +// finer answer the spot judge can give without a band is an artefact of the +// missing band, so it is flattened to "worked" rather than repeated. +func (a *App) judgeNews(list []DXNews) { + if a.qso == nil || len(list) == 0 { + return + } + var queries []SpotQuery + var owners []int + for i, n := range list { + for _, call := range n.Calls { + queries = append(queries, SpotQuery{Call: call}) + owners = append(owners, i) + } + } + if len(queries) == 0 { + return + } + res := a.ClusterSpotStatuses(queries) + for i, r := range res { + if i >= len(owners) { + break + } + row := owners[i] + st := "" + switch { + case r.Status == "new": + st = "new" + case r.Status != "": + st = "worked" + } + if chaseRank[st] > chaseRank[list[row].Status] { + list[row].Status = st + list[row].Unconfirmed = r.UnconfStatus + } + } } // RefreshDXpeditions drops both caches so the next read goes to the network. diff --git a/changelog.json b/changelog.json index 88bbc9b..efedb3f 100644 --- a/changelog.json +++ b/changelog.json @@ -13,7 +13,9 @@ "Watchlist: adding or removing a callsign now raises the same kind of notification as a new version, instead of a message inside the watchlist page — a call can be added from the cluster or the DXpeditions tab, where that message was never seen.", "QSO editor, QSL Info: a HamQTH channel and its row in the status table — sent only, the received column showing a dash since the site publishes no confirmations.", "QSO editor, QSL Info: the confirmation channels are listed paper QSL and LoTW first — the two that carry an ARRL award — then alphabetically, in both the picker and the status table.", - "Band map: a chevron in the footer folds the colour legend away and brings it back — four lines of a short screen, remembered between sessions." + "Band map: a chevron in the footer folds the colour legend away and brings it back — four lines of a short screen, remembered between sessions.", + "Band map: ctrl+wheel no longer zooms it — that gesture is the window zoom everywhere else in OpsLog. The + and − buttons keep the zoom.", + "DXpeditions: the news headlines carry a chase badge too — NEW DXCC or worked. Entity-level only, since a headline names no band: the finer verdicts stay in the announcements pane, which knows the bands and modes." ], "fr": [ "Changer de base de réglages n’affiche plus « OpsLog is already running » : la relance automatique attend désormais que l’instance qui se ferme libère son verrou au lieu de la prendre de vitesse.", @@ -26,7 +28,9 @@ "Watchlist : ajouter ou retirer un indicatif déclenche désormais une notification du même type que celle des nouvelles versions, au lieu d’un message dans la page watchlist — un indicatif peut être ajouté depuis le cluster ou l’onglet DXpéditions, où ce message n’était jamais vu.", "Éditeur de QSO, onglet QSL : un canal HamQTH et sa ligne dans le tableau des statuts — envoi seulement, la colonne reçu affichant un tiret puisque le site ne publie aucune confirmation.", "Éditeur de QSO, onglet QSL : les canaux de confirmation sont classés QSL papier puis LoTW — les deux qui comptent pour un diplôme ARRL — puis par ordre alphabétique, dans le sélecteur comme dans le tableau.", - "Band map : un chevron dans le pied de page replie la légende des couleurs et la fait revenir — quatre lignes gagnées sur un petit écran, mémorisé d’une session à l’autre." + "Band map : un chevron dans le pied de page replie la légende des couleurs et la fait revenir — quatre lignes gagnées sur un petit écran, mémorisé d’une session à l’autre.", + "Band map : ctrl+molette ne zoome plus la carte — ce geste est le zoom de la fenêtre partout ailleurs dans OpsLog. Les boutons + et − gardent le zoom.", + "DXpéditions : les actualités portent aussi un badge de chasse — NOUVEAU DXCC ou contacté. Au niveau entité seulement, une manchette ne nommant aucune bande : les verdicts plus fins restent dans le volet des annonces, qui connaît bandes et modes." ] }, { diff --git a/frontend/src/components/BandMap.tsx b/frontend/src/components/BandMap.tsx index 8cb4926..f0e0c60 100644 --- a/frontend/src/components/BandMap.tsx +++ b/frontend/src/components/BandMap.tsx @@ -471,19 +471,10 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, // eslint-disable-next-line react-hooks/exhaustive-deps }, [band, containerH, currentFreqHz, range, lo, hi, pxPerKHz, fitToBand]); - useEffect(() => { - const el = scrollerRef.current; - if (!el) return; - const onWheel = (e: WheelEvent) => { - if (!range) return; - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - changeZoom(e.deltaY > 0 ? -1 : 1); - } - }; - el.addEventListener('wheel', onWheel, { passive: false }); - return () => el.removeEventListener('wheel', onWheel); - }, [range]); + // No ctrl+wheel zoom here any more: ctrl+wheel is the WINDOW zoom everywhere + // else in OpsLog (View ▸ Zoom in/out), and one gesture that resizes the whole + // app over one panel and one band map over another is a gesture nobody can + // trust. The + / − buttons keep the zoom, deliberately and visibly. // Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune // to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot. diff --git a/frontend/src/components/DXpeditionsPanel.tsx b/frontend/src/components/DXpeditionsPanel.tsx index e461bd8..e86bca0 100644 --- a/frontend/src/components/DXpeditionsPanel.tsx +++ b/frontend/src/components/DXpeditionsPanel.tsx @@ -26,6 +26,10 @@ type DXped = { type News = { title: string; link: string; pub_date: string; excerpt: string; creator: string; image_url: string; tag: string; calls?: string[]; + // Entity-level only — a headline names no band, so the backend answers + // 'new' or 'worked' and nothing finer. + status_chase?: string; + unconfirmed?: boolean; }; // One badge per expedition, the strongest verdict winning — the same palette @@ -180,6 +184,7 @@ export function DXpeditionsPanel() { )} {news.map((n, i) => { const newsCalls = n.calls ?? []; + const newsBadge = CHASE_BADGE[n.status_chase ?? '']; const newsAllWatched = newsCalls.length > 0 && newsCalls.every((c) => watched.has(c.toUpperCase())); return (
@@ -193,6 +198,15 @@ export function DXpeditionsPanel() { {n.tag && ( {n.tag} )} + {newsBadge && ( + + {t(newsBadge.label)} + + )} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 4a295ab..0e5f03f 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -436,7 +436,7 @@ const en: Dict = { 'bmp.statusWorked': 'Entity worked on this band and mode', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map', 'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.ftx': 'FTx', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.widthTip': 'Drag to resize — double-click to reset', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge', - 'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.openUnusual': 'unusual for the season', 'bmp.legendWorked': 'Entity worked', 'bmp.legendNewPota': 'New POTA', 'bmp.legendNewCounty': 'New county', 'bmp.legendWorkedCall': 'Callsign already worked', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Data', 'bmp.legendPhone': 'Phone', 'bmp.legendHide': 'Hide the legend', 'bmp.legendShow': 'Show the legend', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)', + 'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.openUnusual': 'unusual for the season', 'bmp.legendWorked': 'Entity worked', 'bmp.legendNewPota': 'New POTA', 'bmp.legendNewCounty': 'New county', 'bmp.legendWorkedCall': 'Callsign already worked', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Data', 'bmp.legendPhone': 'Phone', 'bmp.legendHide': 'Hide the legend', 'bmp.legendShow': 'Show the legend', 'bmp.footerHint': 'scroll · ◎ = go to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)', 'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).', 'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name', 'frm.awardRefs': 'Award reference lists', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — names & totals for those awards (optional, can take a minute).', 'frm.downloading': 'Downloading…', 'frm.reDownload': 'Re-download', 'frm.download': 'Download', 'frm.required': 'Callsign and locator are required.', 'frm.saving': 'Saving…', 'frm.startLogging': 'Start logging', @@ -943,7 +943,7 @@ const fr: Dict = { 'bmp.statusWorked': 'Entité contactée sur cette bande et ce mode', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande', 'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.ftx': 'FTx', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.widthTip': 'Glisser pour redimensionner — double-clic pour réinitialiser', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande', - 'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.openUnusual': 'inhabituel pour la saison', 'bmp.legendWorked': 'Entité contactée', 'bmp.legendNewPota': 'Nouveau POTA', 'bmp.legendNewCounty': 'Nouveau comté', 'bmp.legendWorkedCall': 'Indicatif déjà contacté', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Numérique', 'bmp.legendPhone': 'Phonie', 'bmp.legendHide': 'Masquer la légende', 'bmp.legendShow': 'Afficher la légende', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)', + 'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.openUnusual': 'inhabituel pour la saison', 'bmp.legendWorked': 'Entité contactée', 'bmp.legendNewPota': 'Nouveau POTA', 'bmp.legendNewCounty': 'Nouveau comté', 'bmp.legendWorkedCall': 'Indicatif déjà contacté', 'bmp.legendCW': 'CW', 'bmp.legendData': 'Numérique', 'bmp.legendPhone': 'Phonie', 'bmp.legendHide': 'Masquer la légende', 'bmp.legendShow': 'Afficher la légende', 'bmp.footerHint': 'défiler · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)', 'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).', 'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom', 'frm.awardRefs': 'Listes de références des diplômes', 'frm.awardRefsHint': 'IOTA · POTA · WWFF · SOTA — noms et totaux pour ces diplômes (optionnel, peut prendre une minute).', 'frm.downloading': 'Téléchargement…', 'frm.reDownload': 'Retélécharger', 'frm.download': 'Télécharger', 'frm.required': "L'indicatif et le locator sont obligatoires.", 'frm.saving': 'Enregistrement…', 'frm.startLogging': 'Commencer à logger', diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index e6513a2..3743b9f 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -12,7 +12,6 @@ import {award} from '../models'; import {awardref} from '../models'; import {bandopen} from '../models'; import {cluster} from '../models'; -import {dxped} from '../models'; import {extsvc} from '../models'; import {powergenius} from '../models'; import {pskr} from '../models'; @@ -477,7 +476,7 @@ export function GetDVKMessages():Promise>; export function GetDVKStatus():Promise; -export function GetDXWorldNews():Promise>; +export function GetDXWorldNews():Promise>; export function GetDXpeditions():Promise>; diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index d5023ae..764eaed 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1466,37 +1466,6 @@ export namespace contest { } -export namespace dxped { - - export class News { - title: string; - link: string; - pub_date: string; - excerpt: string; - creator: string; - image_url: string; - tag: string; - calls: string[]; - - static createFrom(source: any = {}) { - return new News(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.title = source["title"]; - this.link = source["link"]; - this.pub_date = source["pub_date"]; - this.excerpt = source["excerpt"]; - this.creator = source["creator"]; - this.image_url = source["image_url"]; - this.tag = source["tag"]; - this.calls = source["calls"]; - } - } - -} - export namespace extsvc { export class ServiceConfig { @@ -2705,6 +2674,36 @@ export namespace main { this.rec_slot = source["rec_slot"]; } } + export class DXNews { + title: string; + link: string; + pub_date: string; + excerpt: string; + creator: string; + image_url: string; + tag: string; + calls: string[]; + status_chase: string; + unconfirmed: boolean; + + static createFrom(source: any = {}) { + return new DXNews(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.title = source["title"]; + this.link = source["link"]; + this.pub_date = source["pub_date"]; + this.excerpt = source["excerpt"]; + this.creator = source["creator"]; + this.image_url = source["image_url"]; + this.tag = source["tag"]; + this.calls = source["calls"]; + this.status_chase = source["status_chase"]; + this.unconfirmed = source["unconfirmed"]; + } + } export class DXpedition { dxcc: string; callsign: string;