fix(ui): the opening badge must say what is open, not that something is

It read "6M OPEN" and kept the rest in a tooltip nobody hovers — an alarm with
no content, announcing that something was happening and refusing to say what.
Band, direction and typical distance are on the badge now: the three facts that
decide whether to point an antenna, all readable without touching anything. Out
of season gets a mark there too, since that is the one an operator must not
learn last.

The compass point is computed in Go and carried on the Opening rather than
derived in the UI. Sector() already gets the wrap round north right, and it got
it wrong once — a second copy of that arithmetic is how a badge ends up naming
the opposite direction to the log line beside it.
This commit is contained in:
2026-08-11 17:58:51 +02:00
parent 740f007395
commit 78e99afc63
4 changed files with 34 additions and 14 deletions
+2 -2
View File
@@ -7,7 +7,7 @@
"CW over CAT now works on a Kenwood. The KY command was sent in Elecraft's variable-length form; a Kenwood needs exactly 24 characters, so every message was refused.", "CW over CAT now works on a Kenwood. The KY command was sent in Elecraft's variable-length form; a Kenwood needs exactly 24 characters, so every message was refused.",
"Shared CAT is steadier and now diagnoses itself. It survives a rig answering \"busy\" just after transmit instead of dropping the link, stops repeating a PTT state the client never changed, and logs a plain explanation when another program has taken its port or when a client is set to a rig model instead of Hamlib NET rigctl. It also answers the lock-mode and stop-morse commands some clients send around every transmit, instead of refusing them.", "Shared CAT is steadier and now diagnoses itself. It survives a rig answering \"busy\" just after transmit instead of dropping the link, stops repeating a PTT state the client never changed, and logs a plain explanation when another program has taken its port or when a client is set to a rig model instead of Hamlib NET rigctl. It also answers the lock-mode and stop-morse commands some clients send around every transmit, instead of refusing them.",
"Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — from a searchable dropdown, with the chosen columns listed above it in publication order. Choose carefully: the page is public and the list includes addresses and e-mail.", "Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — from a searchable dropdown, with the chosen columns listed above it in publication order. Choose carefully: the page is public and the list includes addresses and e-mail.",
"Band-opening detection now works on the bands it was meant for. Switching on \"Watch for band openings\" (Settings DX Cluster) subscribes to the PSK Reporter feed and adds the two RBN nodes it needs — it was reading a handful of VHF cluster spots while thousands of stations were reporting. It watches 10, 6, 4 and 2 m, counts only reports collected within 300 km of you so an opening means an opening HERE, no longer throws away paths beyond 2400 km, and looks for the busiest direction instead of demanding that every station heard sit in one sector. An opening under way shows as a blinking badge beside the clock until the band goes quiet, and the direction it names is right — a sector crossing north used to be announced as its opposite.", "Band-opening detection now works on the bands it was meant for. Switching on \"Watch for band openings\" (Settings DX Cluster) subscribes to the PSK Reporter feed and adds the two RBN nodes it needs — it was reading a handful of VHF cluster spots while thousands of stations were reporting. It watches 10, 6, 4 and 2 m, counts only reports collected within 300 km of you so an opening means an opening HERE, no longer throws away paths beyond 2400 km, and looks for the busiest direction instead of demanding that every station heard sit in one sector. An opening under way shows as a blinking badge beside the clock — band, direction and typical distance, with a mark when it is unusual for the season — and stays until the band goes quiet, and the direction it names is right — a sector crossing north used to be announced as its opposite.",
"Test connection now actually tests. Club Log checked that the three fields were not empty and reported success without contacting anyone, so a wrong password looked exactly like a right one; it now signs in for real, through the read-only endpoint so a test can never add a record. LoTW reports its two credentials separately: TQSL signs uploads and never uses the website password, so a wrong one used to break nothing until the day confirmations were downloaded.", "Test connection now actually tests. Club Log checked that the three fields were not empty and reported success without contacting anyone, so a wrong password looked exactly like a right one; it now signs in for real, through the read-only endpoint so a test can never add a record. LoTW reports its two credentials separately: TQSL signs uploads and never uses the website password, so a wrong one used to break nothing until the day confirmations were downloaded.",
"The Ultrabeam / SteppIR widget in Station Control can now drive the antenna, not just describe it: a button per configured band tunes it there, up and down move it 25 kHz at a time, and tracking can be switched on or off with its 25 / 50 / 100 kHz threshold beside it. The band you are on lights up, and the commanded frequency is shown in the header.", "The Ultrabeam / SteppIR widget in Station Control can now drive the antenna, not just describe it: a button per configured band tunes it there, up and down move it 25 kHz at a time, and tracking can be switched on or off with its 25 / 50 / 100 kHz threshold beside it. The band you are on lights up, and the commanded frequency is shown in the header.",
"The QSO filter can express a real question. Every condition was joined by one global AND or OR, so \"2 m or 70 cm, in FT8, since January\" had no form: AND killed the two bands, OR let every FT8 QSO through. Two operators — is one of / is none of — take a comma-separated list, so the OR sits inside one condition and the rest keeps ANDing.", "The QSO filter can express a real question. Every condition was joined by one global AND or OR, so \"2 m or 70 cm, in FT8, since January\" had no form: AND killed the two bands, OR let every FT8 QSO through. Two operators — is one of / is none of — take a comma-separated list, so the OR sits inside one condition and the rest keeps ANDing.",
@@ -18,7 +18,7 @@
"Le CW par CAT fonctionne sur Kenwood. La commande KY partait sous la forme Elecraft à longueur libre ; un Kenwood exige exactement 24 caractères, donc chaque message était refusé.", "Le CW par CAT fonctionne sur Kenwood. La commande KY partait sous la forme Elecraft à longueur libre ; un Kenwood exige exactement 24 caractères, donc chaque message était refusé.",
"Le CAT partagé est plus solide et se diagnostique tout seul. Il survit à un rig qui répond « occupé » juste après une émission au lieu de lâcher le lien, cesse de répéter un état PTT que le client n a pas changé, et écrit une explication claire quand un autre programme lui a pris son port ou qu un client est réglé sur un modèle de rig au lieu de Hamlib NET rigctl. Il répond aussi aux commandes de verrouillage et d arrêt du morse que certains logiciels envoient à chaque émission, au lieu de les refuser.", "Le CAT partagé est plus solide et se diagnostique tout seul. Il survit à un rig qui répond « occupé » juste après une émission au lieu de lâcher le lien, cesse de répéter un état PTT que le client n a pas changé, et écrit une explication claire quand un autre programme lui a pris son port ou qu un client est réglé sur un modèle de rig au lieu de Hamlib NET rigctl. Il répond aussi aux commandes de verrouillage et d arrêt du morse que certains logiciels envoient à chaque émission, au lieu de les refuser.",
"La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — depuis une liste déroulante cherchable, les colonnes choisies étant listées au-dessus dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails.", "La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — depuis une liste déroulante cherchable, les colonnes choisies étant listées au-dessus dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails.",
"La détection d ouverture de bande fonctionne enfin sur les bandes visées. Activer « Surveiller les ouvertures de bande » (Paramètres Cluster DX) souscrit au flux PSK Reporter et ajoute les deux nœuds RBN nécessaires — elle lisait quelques spots VHF du cluster alors que des milliers de stations rapportaient. Elle surveille le 10, 6, 4 et 2 m, ne retient que les reports collectés à moins de 300 km de toi pour qu une ouverture veuille dire une ouverture ICI, ne jette plus les chemins au-delà de 2400 km, et cherche la direction la plus dense au lieu d exiger que toutes les stations entendues soient dans un même secteur. Une ouverture en cours s affiche en pastille clignotante à côté de l heure jusqu à ce que la bande se taise, et la direction annoncée est juste — un secteur traversant le nord était auparavant annoncé à l opposé.", "La détection d ouverture de bande fonctionne enfin sur les bandes visées. Activer « Surveiller les ouvertures de bande » (Paramètres Cluster DX) souscrit au flux PSK Reporter et ajoute les deux nœuds RBN nécessaires — elle lisait quelques spots VHF du cluster alors que des milliers de stations rapportaient. Elle surveille le 10, 6, 4 et 2 m, ne retient que les reports collectés à moins de 300 km de toi pour qu une ouverture veuille dire une ouverture ICI, ne jette plus les chemins au-delà de 2400 km, et cherche la direction la plus dense au lieu d exiger que toutes les stations entendues soient dans un même secteur. Une ouverture en cours s affiche en pastille clignotante à côté de l heure — bande, direction et distance typique, avec une marque si elle est inhabituelle pour la saison — et y reste jusqu à ce que la bande se taise, et la direction annoncée est juste — un secteur traversant le nord était auparavant annoncé à l opposé.",
"Le bouton Tester la connexion teste vraiment. Club Log vérifiait que les trois champs n étaient pas vides et annonçait la réussite sans contacter personne : un mauvais mot de passe ressemblait exactement à un bon. Il s authentifie maintenant pour de vrai, via le point d accès en lecture seule pour qu un test ne puisse jamais ajouter un enregistrement. LoTW annonce ses deux identifiants séparément : TQSL signe les envois et n utilise jamais le mot de passe du site, donc un mauvais ne cassait rien jusqu au jour du téléchargement des confirmations.", "Le bouton Tester la connexion teste vraiment. Club Log vérifiait que les trois champs n étaient pas vides et annonçait la réussite sans contacter personne : un mauvais mot de passe ressemblait exactement à un bon. Il s authentifie maintenant pour de vrai, via le point d accès en lecture seule pour qu un test ne puisse jamais ajouter un enregistrement. LoTW annonce ses deux identifiants séparément : TQSL signe les envois et n utilise jamais le mot de passe du site, donc un mauvais ne cassait rien jusqu au jour du téléchargement des confirmations.",
"Le widget Ultrabeam / SteppIR de Station Control pilote enfin l antenne au lieu de seulement la décrire : un bouton par bande configurée l accorde dessus, haut et bas la déplacent par pas de 25 kHz, et le suivi s active ou se coupe avec son seuil de 25 / 50 / 100 kHz à côté. La bande courante s allume, et la fréquence commandée s affiche dans l en-tête.", "Le widget Ultrabeam / SteppIR de Station Control pilote enfin l antenne au lieu de seulement la décrire : un bouton par bande configurée l accorde dessus, haut et bas la déplacent par pas de 25 kHz, et le suivi s active ou se coupe avec son seuil de 25 / 50 / 100 kHz à côté. La bande courante s allume, et la fréquence commandée s affiche dans l en-tête.",
"Le filtre des QSO sait exprimer une vraie question. Toutes les conditions étaient jointes par un seul ET ou OU global, donc « 2 m ou 70 cm, en FT8, depuis janvier » n avait aucune forme : le ET tuait les deux bandes, le OU laissait passer tous les QSO FT8. Deux opérateurs — est parmi / n est pas parmi — acceptent une liste séparée par des virgules, le OU tient donc dans une seule condition et le reste continue de se combiner en ET.", "Le filtre des QSO sait exprimer une vraie question. Toutes les conditions étaient jointes par un seul ET ou OU global, donc « 2 m ou 70 cm, en FT8, depuis janvier » n avait aucune forme : le ET tuait les deux bandes, le OU laissait passer tous les QSO FT8. Deux opérateurs — est parmi / n est pas parmi — acceptent une liste séparée par des virgules, le OU tient donc dans une seule condition et le reste continue de se combiner en ET.",
+14 -3
View File
@@ -6969,13 +6969,24 @@ export default function App() {
onClick={() => { setActiveTab('bandmap'); }} onClick={() => { setActiveTab('bandmap'); }}
title={t('bo.liveTip', { title={t('bo.liveTip', {
band: String(o.band).toUpperCase(), n: o.calls, km: o.median_km, band: String(o.band).toUpperCase(), n: o.calls, km: o.median_km,
sector: o.sector ?? '', season: o.in_season ? '' : ' — ' + t('bmp.openUnusual'), sector: `${o.compass ?? ''} ${o.bearing_min}${o.bearing_max}°`,
season: o.in_season ? '' : ' — ' + t('bmp.openUnusual'),
})} })}
className="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider shrink-0 className="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider shrink-0
border-warning-border bg-warning-muted text-warning-muted-foreground hover:bg-warning-muted/70 animate-pulse" border-warning-border bg-warning-muted text-warning-muted-foreground hover:bg-warning-muted/70 animate-pulse"
> >
<Radio className="size-3" /> <Radio className="size-3 shrink-0" />
{String(o.band).toUpperCase()} {t('bo.open')} {/* The three facts that decide whether to act: which band, which
way to point, how far it reaches. They were in a tooltip
nobody hovers, which made the badge an alarm with no content
it said something was happening and refused to say what. */}
<span>{String(o.band).toUpperCase()}</span>
<span className="font-mono">{o.compass}</span>
<span className="font-mono opacity-80">{o.median_km >= 1000
? `${(o.median_km / 1000).toFixed(1)}k` : o.median_km}km</span>
{/* Out of season is the one an operator must not learn last, so it
earns a mark on the badge rather than a line in the tooltip. */}
{!o.in_season && <span className="opacity-90">!</span>}
</button> </button>
))} ))}
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground shrink-0" title="UTC"> <span className="inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground shrink-0" title="UTC">
+2
View File
@@ -685,6 +685,7 @@ export namespace bandopen {
median_km: number; median_km: number;
bearing_min: number; bearing_min: number;
bearing_max: number; bearing_max: number;
compass: string;
in_season: boolean; in_season: boolean;
// Go type: time // Go type: time
at: any; at: any;
@@ -701,6 +702,7 @@ export namespace bandopen {
this.median_km = source["median_km"]; this.median_km = source["median_km"];
this.bearing_min = source["bearing_min"]; this.bearing_min = source["bearing_min"];
this.bearing_max = source["bearing_max"]; this.bearing_max = source["bearing_max"];
this.compass = source["compass"];
this.in_season = source["in_season"]; this.in_season = source["in_season"];
this.at = this.convertValues(source["at"], null); this.at = this.convertValues(source["at"], null);
this.examples = source["examples"]; this.examples = source["examples"];
+8 -1
View File
@@ -88,6 +88,11 @@ type Opening struct {
MedianKm int `json:"median_km"` // typical hop length MedianKm int `json:"median_km"` // typical hop length
BearingMin int `json:"bearing_min"` // sector, degrees BearingMin int `json:"bearing_min"` // sector, degrees
BearingMax int `json:"bearing_max"` BearingMax int `json:"bearing_max"`
// Compass is the sector as a point of the compass ("NE"), computed here so
// the UI never has to redo the wrap-round-north arithmetic that Sector()
// already gets right — two answers to that question is how a badge ends up
// naming the opposite direction.
Compass string `json:"compass"`
InSeason bool `json:"in_season"` // false = unusual for the time of year InSeason bool `json:"in_season"` // false = unusual for the time of year
At time.Time `json:"at"` At time.Time `json:"at"`
Examples []string `json:"examples"` // a few callsigns, for the announcement Examples []string `json:"examples"` // a few callsigns, for the announcement
@@ -204,10 +209,12 @@ func evaluate(band string, spots []Spot, cfg Config) *Opening {
if len(calls) > 5 { if len(calls) > 5 {
calls = calls[:5] calls = calls[:5]
} }
return &Opening{ op := &Opening{
Band: band, Calls: len(seen), MedianKm: dists[len(dists)/2], Band: band, Calls: len(seen), MedianKm: dists[len(dists)/2],
BearingMin: lo, BearingMax: hi, Examples: calls, BearingMin: lo, BearingMax: hi, Examples: calls,
} }
op.Compass = compass(midBearing(lo, hi))
return op
} }
// arc returns the smallest compass sector containing every bearing, coping with // arc returns the smallest compass sector containing every bearing, coping with