feat(bandopen): read PSK Reporter, and arrange the sources the watch needs
The detection shipped reading whatever the operator's cluster nodes happened to carry. On VHF that is a few hundred skimmers, nearly all of them on HF: a 6 m opening carrying 869 stations reached OpsLog as a handful of spots or none, and Nexus flagged it on the same PC while OpsLog stayed silent. internal/pskr subscribes to pskr/filter/v2/<band>/# on PSK Reporter's MQTT broker. Every ordinary station running WSJT-X reports what it decodes, so the difference is two orders of magnitude rather than a threshold. The feed's shape suits us exactly: BOTH grids are in each message, so distance and bearing are arithmetic — no lookup, and no DXCC-centre approximation, which is what made the cluster path's bearings coarse. The geometry is injected from app.go so it stays the same arithmetic the cluster path uses; two answers to one question is how a bearing quietly becomes wrong. Volume was the design constraint, not the protocol. Six metres open is thousands of messages a minute and this runs on some very old PCs, so nothing is kept or persisted in the watcher: each message is parsed, measured and handed on or dropped, and the detector's existing window does the deciding. And the part that was the real bug: enabling the watch now ARRANGES ITS OWN SOURCES. It adds the two RBN nodes when missing and brings the feed up. A feature that silently depends on sources nobody can know are needed does not look unconfigured, it looks broken. Matched on host and port, not name, so an operator who renamed theirs does not get a duplicate — which the detector would read as twice as many stations, and announce an opening that is not there. Turning it off leaves the nodes alone: they may have been wanted for their own sake, and removing a node someone is using is worse than leaving one they are not.
This commit is contained in:
@@ -52,6 +52,7 @@ import {
|
||||
GetADIFMonitor, SaveADIFMonitor, PickADIFMonitorFile,
|
||||
GetRelayAuto, SaveRelayAuto, GetStationDevices,
|
||||
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
|
||||
GetBandOpenSettings, SaveBandOpenSettings, GetPSKReporterStatus,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -1540,6 +1541,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// blur, not per keystroke, or typing "10" would be rewritten to "5" the moment
|
||||
// the "1" landed and the field would fight the operator.
|
||||
const SELF_SPOT_MIN_MIN = 5;
|
||||
// Band-opening watch. Saved through the backend rather than as a UI pref: it
|
||||
// has side effects there — adding the RBN nodes, bringing the PSK Reporter
|
||||
// feed up or down — so the write has to go where those live.
|
||||
const [bandOpen, setBandOpen] = useState<any>({ enabled: false, bands: [], available: [] });
|
||||
const [pskrStatus, setPskrStatus] = useState<any>(null);
|
||||
const saveBandOpen = async (next: any) => {
|
||||
setBandOpen(next);
|
||||
try { await SaveBandOpenSettings(next); } catch { /* the status line shows the result */ }
|
||||
};
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try { setBandOpen(await GetBandOpenSettings()); } catch { /* defaults stand */ }
|
||||
})();
|
||||
// Poll the feed while the panel is open: a live count is the only thing that
|
||||
// distinguishes "connected" from "connected and receiving nothing".
|
||||
const t = window.setInterval(async () => {
|
||||
try { setPskrStatus(await GetPSKReporterStatus()); } catch { /* ignore */ }
|
||||
}, 3000);
|
||||
return () => window.clearInterval(t);
|
||||
}, []);
|
||||
const [selfSpot, setSelfSpot] = useState({ enabled: false, minutes: SELF_SPOT_MIN_MIN });
|
||||
const [selfSpotText, setSelfSpotText] = useState(String(SELF_SPOT_MIN_MIN));
|
||||
const [clusterStatuses, setClusterStatuses] = useState<ClusterServerStatus[]>([]);
|
||||
@@ -4148,6 +4169,44 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
things set up once. A preferences dialog you reopen every ten minutes
|
||||
is a filter in the wrong place. */}
|
||||
|
||||
{/* Band-opening watch. It lives HERE, with the cluster nodes, because
|
||||
switching it on adds two of them — the operator should see that
|
||||
happen where it happens rather than find nodes they did not add. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={bandOpen.enabled} className="mt-0.5"
|
||||
onCheckedChange={(c) => saveBandOpen({ ...bandOpen, enabled: !!c })} />
|
||||
<span>{t('bo.enable')} <span className="text-xs text-muted-foreground">{t('bo.enableHint')}</span></span>
|
||||
</label>
|
||||
{bandOpen.enabled && (
|
||||
<div className="pl-6 space-y-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{(bandOpen.available ?? []).map((b: string) => {
|
||||
const on = (bandOpen.bands ?? []).includes(b);
|
||||
return (
|
||||
<button key={b} type="button"
|
||||
onClick={() => saveBandOpen({
|
||||
...bandOpen,
|
||||
bands: on ? bandOpen.bands.filter((x: string) => x !== b) : [...(bandOpen.bands ?? []), b],
|
||||
})}
|
||||
className={cn('px-2 py-0.5 rounded-md border text-[11px] font-bold tracking-wider font-mono',
|
||||
on ? 'bg-primary text-primary-foreground border-primary' : 'text-muted-foreground border-border hover:bg-muted')}>
|
||||
{b}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* A live count, because a feed that is connected but silent looks
|
||||
exactly like one that is broken until a number moves. */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pskrStatus?.running
|
||||
? t('bo.feedUp', { n: pskrStatus.received ?? 0 })
|
||||
: t('bo.feedDown')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Self-spot. The interval only shows once it's on — an interval for
|
||||
something switched off is just a question the operator can't act on. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user