feat(cluster): a SOTA column, and a spot click that credits the summit

The SOTA feeds put the summit in the spot's own text, so there is nothing to
poll: a regex on the comment gives the reference, and clicking the spot writes
SOTA@<ref> into the award references the way a park already wrote POTA@<ref>.

The regex is narrow on purpose — association/region-NNN, anchored — because
POTA (US-4475), WWFF (DLFF-0001) and portable callsigns (DL/SP9DPM/P) share
that field and none of them is a summit. Pinned by a table test.

Off by default: the reference is only there on the summit feeds, and an empty
column for every operator who does not chase them is worse than a checkbox.
This commit is contained in:
2026-08-27 21:33:17 +02:00
parent b8796369ba
commit 08d316b1e0
9 changed files with 108 additions and 36 deletions
+24 -20
View File
@@ -44,30 +44,30 @@ type ServerConfig struct {
// is emitted to the UI, so the table never has empty country cells
// flickering in for a few hundred ms.
type Spot struct {
SourceID int64 `json:"source_id"` // ID of the cluster server this came from
SourceName string `json:"source_name"` // display name (handy in the UI when multiple servers)
Spotter string `json:"spotter"` // DE field
SourceID int64 `json:"source_id"` // ID of the cluster server this came from
SourceName string `json:"source_name"` // display name (handy in the UI when multiple servers)
Spotter string `json:"spotter"` // DE field
// SpotterContinent belongs to the SPOT, not to the DX station: one call is
// spotted by dozens of skimmers on every continent within a minute. It is
// resolved per spot at ingest for exactly that reason — see the note on the
// spotter-continent filter in App.tsx.
SpotterContinent string `json:"spotter_continent,omitempty"`
DXCall string `json:"dx_call"` // the DX station heard
FreqKHz float64 `json:"freq_khz"`
FreqHz int64 `json:"freq_hz"`
Band string `json:"band,omitempty"`
Comment string `json:"comment,omitempty"`
Locator string `json:"locator,omitempty"` // spotter grid (optional)
TimeUTC string `json:"time_utc,omitempty"`
Country string `json:"country,omitempty"` // DXCC entity name (cty.dat)
Continent string `json:"continent,omitempty"` // 2-letter continent
CQZone int `json:"cqz,omitempty"` // DXCC entity CQ zone
ITUZone int `json:"ituz,omitempty"` // DXCC entity ITU zone
DistanceKm int `json:"distance_km,omitempty"` // great-circle km from operator's grid
ShortPath int `json:"sp_deg,omitempty"` // azimuth (deg) short path from operator
LongPath int `json:"lp_deg,omitempty"` // azimuth (deg) long path = SP + 180 mod 360
ReceivedAt time.Time `json:"received_at"`
Raw string `json:"raw"`
SpotterContinent string `json:"spotter_continent,omitempty"`
DXCall string `json:"dx_call"` // the DX station heard
FreqKHz float64 `json:"freq_khz"`
FreqHz int64 `json:"freq_hz"`
Band string `json:"band,omitempty"`
Comment string `json:"comment,omitempty"`
Locator string `json:"locator,omitempty"` // spotter grid (optional)
TimeUTC string `json:"time_utc,omitempty"`
Country string `json:"country,omitempty"` // DXCC entity name (cty.dat)
Continent string `json:"continent,omitempty"` // 2-letter continent
CQZone int `json:"cqz,omitempty"` // DXCC entity CQ zone
ITUZone int `json:"ituz,omitempty"` // DXCC entity ITU zone
DistanceKm int `json:"distance_km,omitempty"` // great-circle km from operator's grid
ShortPath int `json:"sp_deg,omitempty"` // azimuth (deg) short path from operator
LongPath int `json:"lp_deg,omitempty"` // azimuth (deg) long path = SP + 180 mod 360
ReceivedAt time.Time `json:"received_at"`
Raw string `json:"raw"`
// Historical marks a spot recovered from a SH/DX table rather than heard live.
// It belongs in the grid, but must NOT fire alerts or reach the panadapter:
// replaying 100 past spots would spam both, and a station spotted three hours
@@ -75,6 +75,10 @@ type Spot struct {
Historical bool `json:"historical,omitempty"`
POTARef string `json:"pota_ref,omitempty"` // park id if this station is activating (api.pota.app)
POTAName string `json:"pota_name,omitempty"` // park name
// SOTARef comes from the COMMENT, not from an API: the SOTA clusters put the
// summit in the text of the spot they send ("W9/WI-001"), and there is no
// per-callsign endpoint to ask the way POTA has one.
SOTARef string `json:"sota_ref,omitempty"`
}
// State enumerates the per-server lifecycle.