feat(chase-new): drop the frequency column, the beacons and the bands you don't have
Three cuts, all of them removing things nobody could act on. The frequency column is gone. Clicking the row tunes the rig to it, so the number was there to be read and never used. It is still carried on the spot — the click is what needs it — and it is in the row's tooltip. The country takes the space. Modes are limited to FT8, FT4, FT2, PSK31 and RTTY. WSPR is the one worth spelling out: it is a beacon, nobody answers a WSPR transmission, so a "new entity on WSPR" is a path report and not a station to work — and on a quiet band it would have been most of the list. Bands are limited to the operator's own list. PSK Reporter carries every band its receivers listen on, and a 13 cm decode is not an opportunity for a station with no 13 cm. Both tests run before the status lookup and both read a cached map: they throw away most of the feed, so nothing downstream pays for it. The band list is re-read when the lists are saved, so ticking a band in Settings applies to the next decode rather than the next restart — which would have looked like the filter not working.
This commit is contained in:
@@ -656,6 +656,11 @@ type App struct {
|
|||||||
// consults it once per message and must not reach the settings store.
|
// consults it once per message and must not reach the settings store.
|
||||||
chaseNew *chaseNewStore
|
chaseNew *chaseNewStore
|
||||||
chaseNewOn atomic.Bool
|
chaseNewOn atomic.Bool
|
||||||
|
// chaseBands is the operator's own band list, cached for the same reason:
|
||||||
|
// it decides whether a decode is an opportunity or a row in the way, and it
|
||||||
|
// is consulted once per message.
|
||||||
|
chaseBandsMu sync.RWMutex
|
||||||
|
chaseBands map[string]bool
|
||||||
// pskr is the PSK Reporter MQTT feed, up only while the opening watch is on.
|
// pskr is the PSK Reporter MQTT feed, up only while the opening watch is on.
|
||||||
// It is the source that makes VHF detection work at all: the cluster and RBN
|
// It is the source that makes VHF detection work at all: the cluster and RBN
|
||||||
// carry a handful of 6 m spots where PSK Reporter carries hundreds.
|
// carry a handful of 6 m spots where PSK Reporter carries hundreds.
|
||||||
@@ -13975,6 +13980,10 @@ func (a *App) SaveListsSettings(l ListsSettings) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The Chase New feed filters on this list per message and reads it from a
|
||||||
|
// cache, so a band ticked here has to reach that cache now — otherwise it
|
||||||
|
// takes effect at the next restart, which looks like the option not working.
|
||||||
|
a.refreshChaseBands()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,13 +5,13 @@
|
|||||||
"en": [
|
"en": [
|
||||||
"Send Spot: the comment now carries the award references after the mode — the ones you assigned (POTA, SOTA, IOTA…), not the DXCC, zone and prefix every reader works out from the callsign. A self-spot carries your OWN activation references instead.",
|
"Send Spot: the comment now carries the award references after the mode — the ones you assigned (POTA, SOTA, IOTA…), not the DXCC, zone and prefix every reader works out from the callsign. A self-spot carries your OWN activation references instead.",
|
||||||
"Modes: a fresh install now starts with SSB, CW, FT8, FT4, FT2, RTTY, PSK31 and FM. AM and DIGITALVOICE stay in the available list but are no longer selected by default.",
|
"Modes: a fresh install now starts with SSB, CW, FT8, FT4, FT2, RTTY, PSK31 and FM. AM and DIGITALVOICE stay in the available list but are no longer selected by default.",
|
||||||
"Chase new: a panel listing the stations PSK Reporter is hearing within about 300 km of you that are new against your log. One badge per row, the most valuable first — entity, band, mode, slot, prefix, square — with a filter for each, a button in the toolbar to show it and a cross to close it. Click a row to put the callsign in the entry and tune the rig. Digital modes only.",
|
"Chase new: a panel listing the stations PSK Reporter is hearing within about 300 km of you that are new against your log — on FT8, FT4, FT2, PSK31 and RTTY, and only on the bands you have selected. One badge per row, the most valuable first (entity, band, mode, slot, prefix, square), with a filter for each, a toolbar button to show it and a cross to close it. Click a row to put the callsign in the entry and tune the rig.",
|
||||||
"Serial ports: a port claimed by two devices in the Windows port map was listed twice in every port dropdown, and showed as “COM3COM3”. Listed once now, and in natural order — COM4 between COM3 and COM8, not after COM9."
|
"Serial ports: a port claimed by two devices in the Windows port map was listed twice in every port dropdown, and showed as “COM3COM3”. Listed once now, and in natural order — COM4 between COM3 and COM8, not after COM9."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Envoi de spot : le commentaire porte désormais les références de diplôme après le mode — celles que vous avez attribuées (POTA, SOTA, IOTA…), pas le DXCC, la zone et le préfixe que chacun déduit de l’indicatif. Un auto-spot porte VOS références d’activation.",
|
"Envoi de spot : le commentaire porte désormais les références de diplôme après le mode — celles que vous avez attribuées (POTA, SOTA, IOTA…), pas le DXCC, la zone et le préfixe que chacun déduit de l’indicatif. Un auto-spot porte VOS références d’activation.",
|
||||||
"Modes : une installation neuve démarre avec SSB, CW, FT8, FT4, FT2, RTTY, PSK31 et FM. AM et DIGITALVOICE restent dans la liste disponible mais ne sont plus sélectionnés par défaut.",
|
"Modes : une installation neuve démarre avec SSB, CW, FT8, FT4, FT2, RTTY, PSK31 et FM. AM et DIGITALVOICE restent dans la liste disponible mais ne sont plus sélectionnés par défaut.",
|
||||||
"Chasse au nouveau : un panneau listant les stations que PSK Reporter entend à moins de 300 km de chez vous et qui sont nouvelles par rapport à votre log. Une seule indication par ligne, la plus précieuse d’abord — entité, bande, mode, créneau, préfixe, carré — avec un filtre pour chacune, un bouton dans la barre pour l’afficher et une croix pour le fermer. Un clic met l’indicatif en saisie et accorde la radio. Modes numériques uniquement.",
|
"Chasse au nouveau : un panneau listant les stations que PSK Reporter entend à moins de 300 km de chez vous et qui sont nouvelles par rapport à votre log — en FT8, FT4, FT2, PSK31 et RTTY, et seulement sur les bandes que vous avez sélectionnées. Une seule indication par ligne, la plus précieuse d’abord (entité, bande, mode, créneau, préfixe, carré), avec un filtre pour chacune, un bouton dans la barre pour l’afficher et une croix pour le fermer. Un clic met l’indicatif en saisie et accorde la radio.",
|
||||||
"Ports série : un port revendiqué par deux périphériques dans la table Windows apparaissait en double dans toutes les listes, et s’affichait « COM3COM3 ». Une seule fois désormais, et dans l’ordre naturel — COM4 entre COM3 et COM8, pas après COM9."
|
"Ports série : un port revendiqué par deux périphériques dans la table Windows apparaissait en double dans toutes les listes, et s’affichait « COM3COM3 ». Une seule fois désormais, et dans l’ordre naturel — COM4 entre COM3 et COM8, pas après COM9."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -182,16 +182,21 @@ export function ChaseNewPanel({ onPick, onClose }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPick?.(s)}
|
onClick={() => onPick?.(s)}
|
||||||
className="flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] hover:bg-accent/40"
|
className="flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] hover:bg-accent/40"
|
||||||
title={[s.country, s.grid, s.dist_km ? `${s.dist_km} km` : ''].filter(Boolean).join(' · ')}
|
title={[
|
||||||
|
s.country,
|
||||||
|
s.grid,
|
||||||
|
s.dist_km ? `${s.dist_km} km` : '',
|
||||||
|
s.freq_hz ? `${(s.freq_hz / 1000).toFixed(1)} kHz` : '',
|
||||||
|
].filter(Boolean).join(' · ')}
|
||||||
>
|
>
|
||||||
<span className="w-[84px] shrink-0 truncate font-mono font-bold">{s.call}</span>
|
<span className="w-[84px] shrink-0 truncate font-mono font-bold">{s.call}</span>
|
||||||
<span className="w-9 shrink-0 font-mono text-muted-foreground">{s.band}</span>
|
<span className="w-9 shrink-0 font-mono text-muted-foreground">{s.band}</span>
|
||||||
<span className="w-10 shrink-0 truncate text-muted-foreground">{s.mode}</span>
|
<span className="w-10 shrink-0 truncate text-muted-foreground">{s.mode}</span>
|
||||||
<span className="w-[52px] shrink-0 text-right font-mono text-muted-foreground">
|
{/* No frequency column: clicking the row tunes the rig to it,
|
||||||
{s.freq_hz ? (s.freq_hz / 1000).toFixed(1) : '—'}
|
so the number was a thing to read and never to use. It is
|
||||||
</span>
|
still carried on the spot — the click is what needs it —
|
||||||
{/* The country gets whatever is left, and there is more of it
|
and shows in the row's tooltip for anyone who wants it.
|
||||||
now that a row carries one badge instead of three. */}
|
The country takes the space it freed. */}
|
||||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">{s.country ?? ''}</span>
|
<span className="min-w-0 flex-1 truncate text-muted-foreground">{s.country ?? ''}</span>
|
||||||
{def && (
|
{def && (
|
||||||
<span className="shrink-0 text-[9px] font-bold" style={{ color: def.colour }}>
|
<span className="shrink-0 text-[9px] font-bold" style={{ color: def.colour }}>
|
||||||
|
|||||||
+61
@@ -129,14 +129,69 @@ func (s *chaseNewStore) clear() {
|
|||||||
s.seen = make(map[string]time.Time, 512)
|
s.seen = make(map[string]time.Time, 512)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chaseModes is what the panel will show. PSK Reporter reports everything its
|
||||||
|
// receivers decode, and most of it is not a contact waiting to happen:
|
||||||
|
//
|
||||||
|
// - WSPR is a beacon. Nobody answers a WSPR transmission, so a "new entity on
|
||||||
|
// WSPR" is a path report, not a station to work — and on a quiet band it
|
||||||
|
// would be most of the list.
|
||||||
|
// - JT65, JT9, FST4W, Q65 and the rest are real but rare enough that they
|
||||||
|
// would only dilute what an operator scans.
|
||||||
|
//
|
||||||
|
// The list is the modes an operator actually calls on. Deliberately not a
|
||||||
|
// setting: a widget with its own mode list is a second place to get the answer
|
||||||
|
// wrong, and this one is short enough to read.
|
||||||
|
var chaseModes = map[string]bool{
|
||||||
|
"FT8": true, "FT4": true, "FT2": true, "PSK31": true, "RTTY": true,
|
||||||
|
}
|
||||||
|
|
||||||
// chaseNewEnabled reads the option. Called per message, so it reads the cached
|
// chaseNewEnabled reads the option. Called per message, so it reads the cached
|
||||||
// atomic rather than the settings store.
|
// atomic rather than the settings store.
|
||||||
func (a *App) chaseNewEnabled() bool { return a.chaseNewOn.Load() }
|
func (a *App) chaseNewEnabled() bool { return a.chaseNewOn.Load() }
|
||||||
|
|
||||||
|
// chaseBandAllowed reports whether a band is one the operator uses.
|
||||||
|
//
|
||||||
|
// PSK Reporter carries every band its receivers listen on, including microwave
|
||||||
|
// segments nobody in the region is equipped for. A 13 cm decode is not an
|
||||||
|
// opportunity for a station with no 13 cm — it is a row in the way.
|
||||||
|
//
|
||||||
|
// The answer is the operator's own band list (Settings → Modes & bands), read
|
||||||
|
// once and cached: this is consulted per message, and re-reading a JSON setting
|
||||||
|
// at that rate on the MQTT goroutine is exactly what must not happen.
|
||||||
|
func (a *App) chaseBandAllowed(band string) bool {
|
||||||
|
a.chaseBandsMu.RLock()
|
||||||
|
m := a.chaseBands
|
||||||
|
a.chaseBandsMu.RUnlock()
|
||||||
|
if m == nil {
|
||||||
|
return true // list not loaded yet — better to show than to swallow
|
||||||
|
}
|
||||||
|
return m[strings.ToLower(strings.TrimSpace(band))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshChaseBands re-reads the operator's band list into the cache. Called
|
||||||
|
// when the widget is switched on and whenever the lists are saved.
|
||||||
|
func (a *App) refreshChaseBands() {
|
||||||
|
s, _ := a.GetListsSettings()
|
||||||
|
m := make(map[string]bool, len(s.Bands))
|
||||||
|
for _, b := range s.Bands {
|
||||||
|
if b = strings.ToLower(strings.TrimSpace(b)); b != "" {
|
||||||
|
m[b] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.chaseBandsMu.Lock()
|
||||||
|
a.chaseBands = m
|
||||||
|
a.chaseBandsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// refreshChaseNew re-reads the option into the atomic the feed consults.
|
// refreshChaseNew re-reads the option into the atomic the feed consults.
|
||||||
func (a *App) refreshChaseNew() {
|
func (a *App) refreshChaseNew() {
|
||||||
on := a.settingOr(keyChaseNew, "") == "1"
|
on := a.settingOr(keyChaseNew, "") == "1"
|
||||||
a.chaseNewOn.Store(on)
|
a.chaseNewOn.Store(on)
|
||||||
|
if on {
|
||||||
|
// Read the band list here rather than per message. Saving the lists calls
|
||||||
|
// back into this, so a band ticked in Settings applies to the next decode.
|
||||||
|
a.refreshChaseBands()
|
||||||
|
}
|
||||||
if !on && a.chaseNew != nil {
|
if !on && a.chaseNew != nil {
|
||||||
// Drop the list rather than leave it on screen: it would go stale with no
|
// Drop the list rather than leave it on screen: it would go stale with no
|
||||||
// feed behind it, and a frozen list of "new" stations is worse than none.
|
// feed behind it, and a frozen list of "new" stations is worse than none.
|
||||||
@@ -159,6 +214,12 @@ func (a *App) feedChaseNew(sp pskr.Spot) {
|
|||||||
}
|
}
|
||||||
band := strings.ToLower(strings.TrimSpace(sp.Band))
|
band := strings.ToLower(strings.TrimSpace(sp.Band))
|
||||||
mode := strings.ToUpper(strings.TrimSpace(sp.Mode))
|
mode := strings.ToUpper(strings.TrimSpace(sp.Mode))
|
||||||
|
// Both tests before the status lookup: they are map reads on short keys and
|
||||||
|
// they throw away most of the feed, so nothing further down pays for a band
|
||||||
|
// this station cannot work or a mode nobody answers.
|
||||||
|
if !chaseModes[mode] || !a.chaseBandAllowed(band) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The same verdict the cluster grid computes, from the same cached index:
|
// The same verdict the cluster grid computes, from the same cached index:
|
||||||
// map lookups per spot, no query.
|
// map lookups per spot, no query.
|
||||||
|
|||||||
@@ -71,3 +71,41 @@ func TestChaseNewStoreIsBounded(t *testing.T) {
|
|||||||
t.Errorf("kept %d rows, want the %d cap", got, chaseNewMax)
|
t.Errorf("kept %d rows, want the %d cap", got, chaseNewMax)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The panel is for stations an operator can call. WSPR is a beacon — nobody
|
||||||
|
// answers one — and on a quiet band it would be most of the list.
|
||||||
|
func TestChaseModesExcludeBeaconsAndOddities(t *testing.T) {
|
||||||
|
for _, m := range []string{"FT8", "FT4", "FT2", "PSK31", "RTTY"} {
|
||||||
|
if !chaseModes[m] {
|
||||||
|
t.Errorf("%s should be listed", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range []string{"WSPR", "JT65", "JT9", "FST4W", "Q65", "JS8", "MSK144", "OLIVIA", ""} {
|
||||||
|
if chaseModes[m] {
|
||||||
|
t.Errorf("%s should not be listed", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A decode on a band the operator does not have is not an opportunity, it is a
|
||||||
|
// row in the way — PSK Reporter carries microwave segments nobody in the region
|
||||||
|
// is equipped for.
|
||||||
|
func TestChaseBandAllowed(t *testing.T) {
|
||||||
|
a := &App{}
|
||||||
|
// Nothing loaded yet: show rather than swallow, or the panel would look
|
||||||
|
// broken for the first seconds after it is switched on.
|
||||||
|
if !a.chaseBandAllowed("13cm") {
|
||||||
|
t.Error("with no band list loaded, nothing should be filtered")
|
||||||
|
}
|
||||||
|
a.chaseBands = map[string]bool{"20m": true, "40m": true, "6m": true}
|
||||||
|
for _, b := range []string{"20m", "40m", "6m", " 20M "} {
|
||||||
|
if !a.chaseBandAllowed(b) {
|
||||||
|
t.Errorf("%q is in the operator's list and was dropped", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, b := range []string{"13cm", "23cm", "2m", "630m", ""} {
|
||||||
|
if a.chaseBandAllowed(b) {
|
||||||
|
t.Errorf("%q is not in the operator's list and was kept", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user