revert(dxped): no chase badge on the news pane

Tried it, looked at it, took it out. A headline names no band, so the
only honest verdict is entity-level — and a pane where every row reads
NEW DXCC or worked says nothing an operator can act on. Two words
repeated forty times is noise wearing the clothes of information.

The announcements pane keeps its badge, where the announced bands and
modes make the verdict specific enough to be worth a colour. The news
pane keeps what it is good at: what is happening, and a Watch button.
Changelog entry withdrawn with it — it never shipped.
This commit is contained in:
2026-08-31 19:00:44 +02:00
parent 2b6f1ba9d7
commit 2f1d592497
5 changed files with 45 additions and 111 deletions
+10 -62
View File
@@ -119,22 +119,16 @@ 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 // GetDXWorldNews returns the DX-World headlines, with the callsigns mined out
// of each one and judged against the log. // of each one so the reader can watch them.
func (a *App) GetDXWorldNews() ([]DXNews, error) { //
// Deliberately NOT judged against the log. A headline names no band, so the
// only verdict available is entity-level, and a pane of "NEW DXCC" and
// "worked" badges turned out to say nothing an operator could act on — the
// same two words on every row is noise wearing the clothes of information.
// The announcements pane, which knows the bands and modes, is where a chase
// verdict is worth drawing.
func (a *App) GetDXWorldNews() ([]dxped.News, error) {
if a.dxped == nil { if a.dxped == nil {
a.dxped = dxped.New() a.dxped = dxped.New()
} }
@@ -145,53 +139,7 @@ func (a *App) GetDXWorldNews() ([]DXNews, error) {
return nil, err return nil, err
} }
} }
out := make([]DXNews, 0, len(news)) return news, nil
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. // RefreshDXpeditions drops both caches so the next read goes to the network.
+2 -4
View File
@@ -14,8 +14,7 @@
"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: 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.", "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.", "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": [ "fr": [
"Changer de base de réglages naffiche plus « OpsLog is already running » : la relance automatique attend désormais que linstance qui se ferme libère son verrou au lieu de la prendre de vitesse.", "Changer de base de réglages naffiche plus « OpsLog is already running » : la relance automatique attend désormais que linstance qui se ferme libère son verrou au lieu de la prendre de vitesse.",
@@ -29,8 +28,7 @@
"É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 : 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.", "É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é dune session à lautre.", "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é dune session à lautre.",
"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.", "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."
] ]
}, },
{ {
@@ -26,10 +26,6 @@ type DXped = {
type News = { type News = {
title: string; link: string; pub_date: string; excerpt: string; title: string; link: string; pub_date: string; excerpt: string;
creator: string; image_url: string; tag: string; calls?: 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 // One badge per expedition, the strongest verdict winning — the same palette
@@ -184,7 +180,6 @@ export function DXpeditionsPanel() {
)} )}
{news.map((n, i) => { {news.map((n, i) => {
const newsCalls = n.calls ?? []; const newsCalls = n.calls ?? [];
const newsBadge = CHASE_BADGE[n.status_chase ?? ''];
const newsAllWatched = newsCalls.length > 0 && newsCalls.every((c) => watched.has(c.toUpperCase())); const newsAllWatched = newsCalls.length > 0 && newsCalls.every((c) => watched.has(c.toUpperCase()));
return ( return (
<article key={`${n.link}-${i}`} className="rounded-md border border-border bg-muted/20 p-2"> <article key={`${n.link}-${i}`} className="rounded-md border border-border bg-muted/20 p-2">
@@ -198,15 +193,6 @@ export function DXpeditionsPanel() {
{n.tag && ( {n.tag && (
<span className="px-1 py-px rounded text-[9px] font-bold uppercase bg-primary/15 text-primary">{n.tag}</span> <span className="px-1 py-px rounded text-[9px] font-bold uppercase bg-primary/15 text-primary">{n.tag}</span>
)} )}
{newsBadge && (
<span className="px-1 py-px rounded text-[9px] font-bold uppercase tracking-wide border shrink-0"
title={n.unconfirmed ? t('dec.unconfTip') : undefined}
style={n.unconfirmed
? { color: newsBadge.colour, borderColor: newsBadge.colour, borderStyle: 'dashed', opacity: 0.6 }
: { color: newsBadge.colour, borderColor: newsBadge.colour }}>
{t(newsBadge.label)}
</span>
)}
<button type="button" onClick={() => n.link && BrowserOpenURL(n.link)} <button type="button" onClick={() => n.link && BrowserOpenURL(n.link)}
className="text-xs font-medium text-left hover:underline">{n.title}</button> className="text-xs font-medium text-left hover:underline">{n.title}</button>
</div> </div>
+2 -1
View File
@@ -12,6 +12,7 @@ import {award} from '../models';
import {awardref} from '../models'; import {awardref} from '../models';
import {bandopen} from '../models'; import {bandopen} from '../models';
import {cluster} from '../models'; import {cluster} from '../models';
import {dxped} from '../models';
import {extsvc} from '../models'; import {extsvc} from '../models';
import {powergenius} from '../models'; import {powergenius} from '../models';
import {pskr} from '../models'; import {pskr} from '../models';
@@ -476,7 +477,7 @@ export function GetDVKMessages():Promise<Array<main.DVKMessage>>;
export function GetDVKStatus():Promise<main.DVKStatus>; export function GetDVKStatus():Promise<main.DVKStatus>;
export function GetDXWorldNews():Promise<Array<main.DXNews>>; export function GetDXWorldNews():Promise<Array<dxped.News>>;
export function GetDXpeditions():Promise<Array<main.DXpedition>>; export function GetDXpeditions():Promise<Array<main.DXpedition>>;
+31 -30
View File
@@ -1466,6 +1466,37 @@ 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 namespace extsvc {
export class ServiceConfig { export class ServiceConfig {
@@ -2674,36 +2705,6 @@ export namespace main {
this.rec_slot = source["rec_slot"]; 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 { export class DXpedition {
dxcc: string; dxcc: string;
callsign: string; callsign: string;