Compare commits

..
1 Commits
Author SHA1 Message Date
rouggy 3da1d71323 chore: release v0.23.1 2026-08-02 20:29:30 +02:00
8 changed files with 81 additions and 17 deletions
+23
View File
@@ -15476,6 +15476,12 @@ type SpotStatus struct {
// when that database has been downloaded); POTA from the spot's tagged park. // when that database has been downloaded); POTA from the spot's tagged park.
NewCounty bool `json:"new_county"` NewCounty bool `json:"new_county"`
NewPOTA bool `json:"new_pota"` NewPOTA bool `json:"new_pota"`
// NewPfx flags a CQ WPX prefix never worked before, and Pfx is that prefix.
// Also orthogonal: a common entity on a worked band can still carry a prefix
// that has never been in the log, which is exactly what a WPX chaser is
// scanning the cluster for.
NewPfx bool `json:"new_pfx"`
Pfx string `json:"pfx,omitempty"`
} }
// ClusterSpotStatuses takes a batch of spots and returns slot status for // ClusterSpotStatuses takes a batch of spots and returns slot status for
@@ -15531,6 +15537,16 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
// lookup) and worked POTA parks. Both built once per batch. // lookup) and worked POTA parks. Both built once per batch.
workedCounties, _ := a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey) workedCounties, _ := a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey)
workedPOTA, _ := a.qso.WorkedPOTARefs(a.ctx) workedPOTA, _ := a.qso.WorkedPOTARefs(a.ctx)
// Worked WPX prefixes, derived from the callsigns we already loaded — no
// extra query. Derived rather than read from the stored PFX column: that
// column is only filled when an import supplied it, and deriving keeps this
// in step with the WPX award, which does the same thing.
workedPfx := make(map[string]struct{}, len(workedCalls))
for c := range workedCalls {
if p := award.WPXPrefix(c); p != "" {
workedPfx[p] = struct{}{}
}
}
for i, q := range spots { for i, q := range spots {
out[i] = SpotStatus{ out[i] = SpotStatus{
Call: q.Call, Call: q.Call,
@@ -15540,6 +15556,13 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
if _, ok := workedCalls[strings.ToUpper(q.Call)]; ok { if _, ok := workedCalls[strings.ToUpper(q.Call)]; ok {
out[i].WorkedCall = true out[i].WorkedCall = true
} }
// NEW PFX: the spot's CQ WPX prefix, never worked before.
if p := award.WPXPrefix(q.Call); p != "" {
out[i].Pfx = p
if _, done := workedPfx[p]; !done {
out[i].NewPfx = true
}
}
// NEW POTA: the spot's tagged park, never worked before. // NEW POTA: the spot's tagged park, never worked before.
if ref := strings.ToUpper(strings.TrimSpace(q.POTARef)); ref != "" { if ref := strings.ToUpper(strings.TrimSpace(q.POTARef)); ref != "" {
if _, done := workedPOTA[ref]; !done { if _, done := workedPOTA[ref]; !done {
+16 -2
View File
@@ -1,4 +1,16 @@
[ [
{
"version": "0.23.1",
"date": "",
"en": [
"DX cluster: NEW COUNTY is green, like NEW POTA.",
"CW: pressing ESC during a macro no longer turns the report from 599 into 59 when \"ESC clears the callsign too\" is on."
],
"fr": [
"Cluster DX : NOUVEAU COMTÉ passe en vert, comme NOUVEAU POTA.",
"CW : appuyer sur Échap pendant une macro ne fait plus passer le report de 599 à 59 quand l'option « Échap efface aussi l'indicatif » est cochée."
]
},
{ {
"version": "0.23.0", "version": "0.23.0",
"date": "", "date": "",
@@ -6,13 +18,15 @@
"Station Control: Dingtian IOT relay boards (2 to 32 relays, LAN or WiFi) can now be added.", "Station Control: Dingtian IOT relay boards (2 to 32 relays, LAN or WiFi) can now be added.",
"ACOM amplifiers: the card now shows TUNE while the tuner is running, instead of an unknown state.", "ACOM amplifiers: the card now shows TUNE while the tuner is running, instead of an unknown state.",
"DX cluster: the status pills are gone. The call, band and mode turn yellow when that one is new, and a callsign already in the log turns blue.", "DX cluster: the status pills are gone. The call, band and mode turn yellow when that one is new, and a callsign already in the log turns blue.",
"Reports: starting OpsLog with the rig already in CW no longer leaves the RST fields on 59 — they now follow the mode you are actually on." "Reports: starting OpsLog with the rig already in CW no longer leaves the RST fields on 59 — they now follow the mode you are actually on. (Thanks HB9HBY)",
"DX cluster: NEW PFX marks a spot whose CQ WPX prefix has never been worked, in the status column and as a filter."
], ],
"fr": [ "fr": [
"Contrôle station : les cartes relais Dingtian IOT (2 à 32 relais, LAN ou WiFi) peuvent désormais être ajoutées.", "Contrôle station : les cartes relais Dingtian IOT (2 à 32 relais, LAN ou WiFi) peuvent désormais être ajoutées.",
"Amplificateurs ACOM : la carte affiche TUNE pendant l'accord, au lieu d'un état inconnu.", "Amplificateurs ACOM : la carte affiche TUNE pendant l'accord, au lieu d'un état inconnu.",
"Cluster DX : les pastilles de statut disparaissent. L'indicatif, la bande et le mode passent en jaune quand celui-ci est nouveau, et un indicatif déjà dans le log passe en bleu.", "Cluster DX : les pastilles de statut disparaissent. L'indicatif, la bande et le mode passent en jaune quand celui-ci est nouveau, et un indicatif déjà dans le log passe en bleu.",
"Reports : lancer OpsLog avec le poste déjà en CW ne laisse plus les RST à 59 — ils suivent maintenant le mode réellement en cours." "Reports : lancer OpsLog avec le poste déjà en CW ne laisse plus les RST à 59 — ils suivent maintenant le mode réellement en cours. (Merci HB9HBY)",
"Cluster DX : NOUVEAU PFX signale un spot dont le préfixe CQ WPX n'a jamais été contacté, dans la colonne de statut et en filtre."
] ]
}, },
{ {
+26 -7
View File
@@ -492,6 +492,14 @@ export default function App() {
const [bandRx, setBandRx] = useState('20m'); const [bandRx, setBandRx] = useState('20m');
const [countries, setCountries] = useState<string[]>([]); const [countries, setCountries] = useState<string[]>([]);
const [rstLists, setRstLists] = useState<RSTLists>({ phone: [], cw: [], digital: [] }); const [rstLists, setRstLists] = useState<RSTLists>({ phone: [], cw: [], digital: [] });
// Refs for the report defaults. applyModePreset is called from long-lived
// closures — the global key handler is the one that bit us — whose dependency
// lists deliberately exclude the mode, so reading the state variables there
// gives whatever they were when that closure was built.
const modePresetsRef = useRef(modePresets);
useEffect(() => { modePresetsRef.current = modePresets; }, [modePresets]);
const rstListsRef = useRef(rstLists);
useEffect(() => { rstListsRef.current = rstLists; }, [rstLists]);
const [rstSent, setRstSent] = useState('59'); const [rstSent, setRstSent] = useState('59');
const [rstRcvd, setRstRcvd] = useState('59'); const [rstRcvd, setRstRcvd] = useState('59');
const [grid, setGrid] = useState(''); const [grid, setGrid] = useState('');
@@ -1382,7 +1390,7 @@ export default function App() {
// county or a new park is not a DXCC state, it is another dimension of the // county or a new park is not a DXCC state, it is another dimension of the
// same spot — which is why the grid shows them as separate badges. Filtering // same spot — which is why the grid shows them as separate badges. Filtering
// is an OR across all of them, as it already was for a worked callsign. // is an OR across all of them, as it already was for a worked callsign.
type SpotFilterKey = SpotStatusKey | 'new-pota' | 'new-county'; type SpotFilterKey = SpotStatusKey | 'new-pota' | 'new-county' | 'new-pfx';
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => lsSet<SpotFilterKey>('opslog.clusterStatusFilter')); const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => lsSet<SpotFilterKey>('opslog.clusterStatusFilter'));
// Mode filter chips. Empty set = show every mode. Categories map the // Mode filter chips. Empty set = show every mode. Categories map the
// inferred per-spot mode onto SSB (phone) / CW / DATA (digital). // inferred per-spot mode onto SSB (phone) / CW / DATA (digital).
@@ -1478,7 +1486,7 @@ export default function App() {
// Cached per-call slot status: "new" | "new-band" | "new-slot" | "worked". // Cached per-call slot status: "new" | "new-band" | "new-slot" | "worked".
// Keyed by `${call}|${band}|${mode}` so two spots of the same call on // Keyed by `${call}|${band}|${mode}` so two spots of the same call on
// different slots don't share the same colour. // different slots don't share the same colour.
const [spotStatus, setSpotStatus] = useState<Record<string, { status: string; country?: string; continent?: string; worked_call?: boolean; new_county?: boolean; new_pota?: boolean }>>({}); const [spotStatus, setSpotStatus] = useState<Record<string, { status: string; country?: string; continent?: string; worked_call?: boolean; new_county?: boolean; new_pota?: boolean; new_pfx?: boolean; pfx?: string }>>({});
// Live mirror of spotStatus so the incoming-spot buffer can tell which slots // Live mirror of spotStatus so the incoming-spot buffer can tell which slots
// still need resolving without re-subscribing the cluster:spot listener. // still need resolving without re-subscribing the cluster:spot listener.
const spotStatusRef = useRef(spotStatus); const spotStatusRef = useRef(spotStatus);
@@ -2187,8 +2195,9 @@ export default function App() {
// Prefer the user's configured preset RST; otherwise fall back to the mode // Prefer the user's configured preset RST; otherwise fall back to the mode
// category default (CW/RTTY/PSK → 599, phone → 59, digital → first option) // category default (CW/RTTY/PSK → 599, phone → 59, digital → first option)
// so switching SSB→CW flips 59→599 even without a configured preset. // so switching SSB→CW flips 59→599 even without a configured preset.
const p = modePresets.find((x) => x.name === m); // Read through the refs, never the state: see their declaration.
const fallback = rstOptions(m, rstLists)[0] || ''; const p = modePresetsRef.current.find((x) => x.name === m);
const fallback = rstOptions(m, rstListsRef.current)[0] || '';
setRstSent(p?.default_rst_sent || fallback); setRstSent(p?.default_rst_sent || fallback);
setRstRcvd(p?.default_rst_rcvd || fallback); setRstRcvd(p?.default_rst_rcvd || fallback);
} }
@@ -2461,6 +2470,8 @@ export default function App() {
worked_call: !!(r as any).worked_call, worked_call: !!(r as any).worked_call,
new_county: !!(r as any).new_county, new_county: !!(r as any).new_county,
new_pota: !!(r as any).new_pota, new_pota: !!(r as any).new_pota,
new_pfx: !!(r as any).new_pfx,
pfx: (r as any).pfx,
}; };
} }
return next; return next;
@@ -2916,6 +2927,8 @@ export default function App() {
worked_call: !!(r as any).worked_call, worked_call: !!(r as any).worked_call,
new_county: !!(r as any).new_county, new_county: !!(r as any).new_county,
new_pota: !!(r as any).new_pota, new_pota: !!(r as any).new_pota,
new_pfx: !!(r as any).new_pfx,
pfx: (r as any).pfx,
}; };
} }
return next; return next;
@@ -3024,7 +3037,11 @@ export default function App() {
setWb(null); // clear the Worked-before grid for the just-cleared callsign setWb(null); // clear the Worked-before grid for the just-cleared callsign
setLookupError(''); setLookupError('');
rstUserEditedRef.current = false; rstUserEditedRef.current = false;
applyModePreset(mode); // modeRef, not `mode`: resetEntry is reached from the global key handler,
// whose effect does not re-run on a mode change. Pressing ESC during a CW
// macro therefore reset the report using the mode as it was when that
// handler was built — 'SSB' — and turned 599 into 59.
applyModePreset(modeRef.current);
setDetails((d) => ({ setDetails((d) => ({
...d, ...d,
state: '', cnty: '', address: '', lat: undefined, lon: undefined, state: '', cnty: '', address: '', lat: undefined, lon: undefined,
@@ -4351,7 +4368,8 @@ export default function App() {
const matches = clusterStatusFilter.has(st) const matches = clusterStatusFilter.has(st)
|| (!!e?.worked_call && clusterStatusFilter.has('worked')) || (!!e?.worked_call && clusterStatusFilter.has('worked'))
|| (!!e?.new_pota && clusterStatusFilter.has('new-pota')) || (!!e?.new_pota && clusterStatusFilter.has('new-pota'))
|| (!!e?.new_county && clusterStatusFilter.has('new-county')); || (!!e?.new_county && clusterStatusFilter.has('new-county'))
|| (!!e?.new_pfx && clusterStatusFilter.has('new-pfx'));
if (!matches) return false; if (!matches) return false;
} }
if (clusterHideWorked) { if (clusterHideWorked) {
@@ -4470,7 +4488,8 @@ export default function App() {
// Same colours as the badges in the grid — a filter that does not // Same colours as the badges in the grid — a filter that does not
// look like what it selects has to be learned twice. // look like what it selects has to be learned twice.
{ k: 'new-pota' as SpotFilterKey, label: 'NEW POTA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' }, { k: 'new-pota' as SpotFilterKey, label: 'NEW POTA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
{ k: 'new-county' as SpotFilterKey, label: 'NEW COUNTY', cls: 'bg-info-muted text-info-muted-foreground border-info-border' }, { k: 'new-county' as SpotFilterKey, label: 'NEW COUNTY', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
{ k: 'new-pfx' as SpotFilterKey, label: 'NEW PFX', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
// (no WORKED chip — use the "Hide worked" checkbox to drop dupes.) // (no WORKED chip — use the "Hide worked" checkbox to drop dupes.)
]).map((s) => { ]).map((s) => {
const on = clusterStatusFilter.has(s.k); const on = clusterStatusFilter.has(s.k);
+8 -4
View File
@@ -53,6 +53,8 @@ export type SpotStatusEntry = {
worked_call?: boolean; worked_call?: boolean;
new_county?: boolean; new_county?: boolean;
new_pota?: boolean; new_pota?: boolean;
new_pfx?: boolean;
pfx?: string;
}; };
type Props = { type Props = {
@@ -123,9 +125,9 @@ function cellText(value: any, color: string | null): any {
return <span style={color ? { color, fontWeight: 700 } : undefined}>{txt}</span>; return <span style={color ? { color, fontWeight: 700 } : undefined}>{txt}</span>;
} }
// statusColor is the colour for a resolved status in the Status column. County // statusColor is the colour for a resolved status in the Status column. County,
// and POTA keep their own tokens: they are orthogonal to the band/mode/DXCC // POTA and prefix keep their own tokens: they are orthogonal to the band/mode/
// story and an operator filters on them separately. // DXCC story and an operator filters on them separately.
function statusColor(s: SpotStatusEntry | undefined): string | null { function statusColor(s: SpotStatusEntry | undefined): string | null {
switch (s?.status) { switch (s?.status) {
case 'new': case 'new':
@@ -191,6 +193,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
else if (s?.worked_call) parts.push(t('clg2.wkdCall')); else if (s?.worked_call) parts.push(t('clg2.wkdCall'));
if (s?.new_county) parts.push(t('clg2.newCounty')); if (s?.new_county) parts.push(t('clg2.newCounty'));
if (s?.new_pota) parts.push(t('clg2.newPota')); if (s?.new_pota) parts.push(t('clg2.newPota'));
if (s?.new_pfx) parts.push(t('clg2.newPfx'));
return parts.join(' '); return parts.join(' ');
}, },
cellRenderer: (p: any) => { cellRenderer: (p: any) => {
@@ -205,8 +208,9 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
: t('clg2.wkdCall'); : t('clg2.wkdCall');
parts.push({ text: label, color: main }); parts.push({ text: label, color: main });
} }
if (s?.new_county) parts.push({ text: t('clg2.newCounty'), color: 'var(--info)' }); if (s?.new_county) parts.push({ text: t('clg2.newCounty'), color: 'var(--success)' });
if (s?.new_pota) parts.push({ text: t('clg2.newPota'), color: 'var(--success)' }); if (s?.new_pota) parts.push({ text: t('clg2.newPota'), color: 'var(--success)' });
if (s?.new_pfx) parts.push({ text: t('clg2.newPfx'), color: 'var(--caution)' });
if (parts.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}></span>; if (parts.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}></span>;
return ( return (
<span style={{ whiteSpace: 'nowrap' }}> <span style={{ whiteSpace: 'nowrap' }}>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
// Single source of truth for the app version shown in the UI (header + About). // Single source of truth for the app version shown in the UI (header + About).
// Bump this on a release (the release script updates it alongside telemetry.go). // Bump this on a release (the release script updates it alongside telemetry.go).
export const APP_VERSION = '0.23.0'; export const APP_VERSION = '0.23.1';
// Author / credits, shown in Help -> About. // Author / credits, shown in Help -> About.
export const APP_AUTHOR = 'F4BPO'; export const APP_AUTHOR = 'F4BPO';
+4
View File
@@ -2904,6 +2904,8 @@ export namespace main {
worked_call: boolean; worked_call: boolean;
new_county: boolean; new_county: boolean;
new_pota: boolean; new_pota: boolean;
new_pfx: boolean;
pfx?: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new SpotStatus(source); return new SpotStatus(source);
@@ -2920,6 +2922,8 @@ export namespace main {
this.worked_call = source["worked_call"]; this.worked_call = source["worked_call"];
this.new_county = source["new_county"]; this.new_county = source["new_county"];
this.new_pota = source["new_pota"]; this.new_pota = source["new_pota"];
this.new_pfx = source["new_pfx"];
this.pfx = source["pfx"];
} }
} }
export class StartupStatus { export class StartupStatus {
+1 -1
View File
@@ -21,7 +21,7 @@ import (
const ( const (
// appVersion is stamped on every heartbeat (and could feed the About box). // appVersion is stamped on every heartbeat (and could feed the About box).
appVersion = "0.23.0" appVersion = "0.23.1"
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change // posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
// to https://us.i.posthog.com for a US project. // to https://us.i.posthog.com for a US project.