diff --git a/app.go b/app.go index 5a1da8f..9e77fc8 100644 --- a/app.go +++ b/app.go @@ -19819,9 +19819,12 @@ type SpotStatus struct { // County and State are that resolved county, so the cluster can show it as a // column instead of only flagging it. Free: the ULS lookup that decides // NewCounty already has them in hand. - County string `json:"county,omitempty"` - State string `json:"state,omitempty"` - NewPOTA bool `json:"new_pota"` + County string `json:"county,omitempty"` + State string `json:"state,omitempty"` + // NewState says this US state has never been worked — the WAS gap the + // decode panel's filter chases. Orthogonal to Status, like NewCounty. + NewState bool `json:"new_state"` + NewPOTA bool `json:"new_pota"` // Grid is the 4-character square this station announced in a CQ on the UDP // link, and NewGrid says that square has never been worked. Both are empty / // false for any station this receiver has not decoded — a DX-cluster line @@ -19869,6 +19872,7 @@ type clusterStatusCache struct { workedCallSlots map[string]struct{} workedCallSlotsDig map[string]struct{} // same set, digital modes folded to DIG // nil unless the "same slot" option is on workedCounties map[string]struct{} + workedStates map[string]struct{} // callCounties holds callsign → "STATE,County" for stations already logged // with a county, so a spot shows the county the entry panel showed rather // than the one derived from the licence ZIP. See qso.CallCounties. @@ -19967,6 +19971,7 @@ func (a *App) clusterStatusMaps() *clusterStatusCache { // Orthogonal dimensions: worked US counties (for the ULS callsign→county // lookup) and worked POTA parks. c.workedCounties, _ = a.qso.WorkedCountyKeys(a.ctx, award.USCountyKey) + c.workedStates, _ = a.qso.WorkedStateKeys(a.ctx) c.callCounties, _ = a.qso.CallCounties(a.ctx) c.workedPOTA, _ = a.qso.WorkedPOTARefs(a.ctx) // One more DISTINCT scan when the snapshot is rebuilt, then pure map lookups @@ -20409,6 +20414,11 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus { if cnty, ok := idx.callCounties[q.Call]; ok { st, name, _ := strings.Cut(cnty, ",") out[i].State, out[i].County = st, name + if st != "" { + if _, done := idx.workedStates[strings.ToUpper(st)]; !done { + out[i].NewState = true + } + } // Logged means worked, so this can never be a new county — but say // so through the same key the flag below uses, not by assumption. if key := award.USCountyKey(st, name); key != "" { @@ -20419,6 +20429,11 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus { } else if a.uls != nil { if loc, ok := a.uls.Resolve(q.Call); ok { out[i].County, out[i].State = loc.County, loc.State + if loc.State != "" { + if _, done := idx.workedStates[strings.ToUpper(loc.State)]; !done { + out[i].NewState = true + } + } if key := award.USCountyKey(loc.State, loc.County); key != "" { if _, done := workedCounties[key]; !done { out[i].NewCounty = true diff --git a/changelog.json b/changelog.json index e34db9e..cd4465a 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.27.4", "date": "", "en": [ - "FT decodes: a State column between the locator and the country — the two-letter badge plus the full name — for the WAS chasers." + "FT decodes: a State column between the locator and the country — the two-letter badge plus the full name — for the WAS chasers.", + "FT decodes: a NEW STATE badge and filter — a US state never worked lights up in the status column, and the filter chip shows only those. The WAS chase is complete." ], "fr": [ - "FT decodes : une colonne État entre le locator et le pays — le badge deux lettres plus le nom complet — pour les chasseurs de WAS." + "FT decodes : une colonne État entre le locator et le pays — le badge deux lettres plus le nom complet — pour les chasseurs de WAS.", + "FT decodes : un badge et un filtre NOUVEL ÉTAT — un état US jamais contacté s’allume dans la colonne statut, et la puce de filtre ne montre que ceux-là. La chasse WAS est complète." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4ee688c..67ff032 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2088,7 +2088,7 @@ export default function App() { // worked_slot must be carried explicitly like every other field: this map is // assembled field by field, so a backend flag that nobody copies here simply // never reaches the panels — silently, since the extra key is just dropped. - const [spotStatus, setSpotStatus] = useState>({}); + const [spotStatus, setSpotStatus] = useState>({}); // Live mirror of spotStatus so the incoming-spot buffer can tell which slots // still need resolving without re-subscribing the cluster:spot listener. const spotStatusRef = useRef(spotStatus); @@ -3639,6 +3639,7 @@ export default function App() { worked_call: !!(r as any).worked_call, worked_slot: !!(r as any).worked_slot, new_county: !!(r as any).new_county, lotw: !!(r as any).lotw, spotter_continent: (r as any).spotter_continent, grid: (r as any).grid, new_grid: !!(r as any).new_grid, county: (r as any).county, state: (r as any).state, + new_state: !!(r as any).new_state, new_pota: !!(r as any).new_pota, new_pfx: !!(r as any).new_pfx, pfx: (r as any).pfx, @@ -3745,6 +3746,7 @@ export default function App() { new_county: !!(r as any).new_county, lotw: !!(r as any).lotw, grid: (r as any).grid, new_grid: !!(r as any).new_grid, county: (r as any).county, state: (r as any).state, + new_state: !!(r as any).new_state, new_pota: !!(r as any).new_pota, new_pfx: !!(r as any).new_pfx, pfx: (r as any).pfx, }; diff --git a/frontend/src/components/DecodesPanel.tsx b/frontend/src/components/DecodesPanel.tsx index 361c97b..cbd41a6 100644 --- a/frontend/src/components/DecodesPanel.tsx +++ b/frontend/src/components/DecodesPanel.tsx @@ -72,6 +72,7 @@ type StatusEntry = { new_pota?: boolean; new_pfx?: boolean; new_grid?: boolean; + new_state?: boolean; // "new" = never worked, "unconf" = worked and awaiting a confirmation. grid_state?: string; state?: string; @@ -113,7 +114,7 @@ interface Props { // // All off means no filtering at all: this is a decode LOG first, and a panel // that starts by hiding most of the band would be lying about what is on it. -type NewCat = 'dxcc' | 'band' | 'mode' | 'slot' | 'pfx' | 'grid' | 'pota' | 'cty'; +type NewCat = 'dxcc' | 'band' | 'mode' | 'slot' | 'pfx' | 'grid' | 'pota' | 'cty' | 'state'; const NEW_CATS: { key: NewCat; label: string; colour: string }[] = [ { key: 'dxcc', label: 'dec.stNew', colour: 'var(--success)' }, @@ -124,6 +125,7 @@ const NEW_CATS: { key: NewCat; label: string; colour: string }[] = [ { key: 'grid', label: 'dec.bgGrid', colour: markerColour('new_grid') }, { key: 'pfx', label: 'dec.bgPfx', colour: markerColour('new_pfx') }, { key: 'cty', label: 'dec.bgCounty', colour: markerColour('new_county') }, + { key: 'state', label: 'dec.bgState', colour: markerColour('new_state') }, ]; // catsOf lists everything a decode is new for. A station can be several at once @@ -144,6 +146,7 @@ function catsOf(e: StatusEntry | undefined): Set { if (e.new_grid) out.add('grid'); if (e.new_pfx) out.add('pfx'); if (e.new_county) out.add('cty'); + if (e.new_state) out.add('state'); return out; } @@ -361,6 +364,7 @@ function entityBadgesFor(status: string): { label: string; cls: string }[] { const EXTRA_BADGES: { key: keyof StatusEntry; marker: SpotMarkerKey; label: string }[] = [ { key: 'new_pota', marker: 'new_pota', label: 'dec.bgPota' }, { key: 'new_grid', marker: 'new_grid', label: 'dec.bgGrid' }, + { key: 'new_state', marker: 'new_state', label: 'dec.bgState' }, { key: 'new_pfx', marker: 'new_pfx', label: 'dec.bgPfx' }, { key: 'new_county', marker: 'new_county', label: 'dec.bgCounty' }, ]; diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index ad061bb..7605288 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -177,7 +177,7 @@ const en: Dict = { 'dec.txNow': 'Transmitting', 'dec.txIdle': 'Transmit', 'dec.working': 'calling', 'dec.toYou': 'to you', 'dec.txUnknown': 'transmitting — text not reported', 'dec.txNothing': 'nothing being sent', 'dec.colTime': 'Time', 'dec.colSnr': 'SNR', 'dec.colMsg': 'Message', 'dec.bgGridUnconf': 'GRID?', 'dec.bgGridUnconfTip': 'This square is worked but not yet confirmed — a QSL to chase, not a QSO to make.', 'dec.colGrid': 'Grid', 'dec.colState': 'State', 'dec.colCountry': 'Country', 'dec.colBand': 'Band', 'dec.colMode': 'Mode', 'dec.colStatus': 'Status', 'dec.stateTip': 'US state', 'dec.wkd': 'Wkd', - 'dec.bgPota': 'POTA', 'dec.bgGrid': 'GRID', 'dec.bgPfx': 'PFX', 'dec.bgCounty': 'CTY', + 'dec.bgPota': 'POTA', 'dec.bgGrid': 'GRID', 'dec.bgPfx': 'PFX', 'dec.bgState': 'New State', 'dec.bgCounty': 'CTY', 'dec.stNew': 'NEW', 'dec.stBand': 'BAND', 'dec.stMode': 'MODE', 'dec.stSlot': 'SLOT', 'dec.stCall': 'CALL', 'dec.empty': 'Nothing decoded yet. Decodes arrive from WSJT-X, JTDX or MSHV over the inbound UDP link (Settings -> UDP).', 'dec.emptyFiltered': 'No decode matches these filters.', @@ -521,7 +521,7 @@ const en: Dict = { 'wbg.awardTip': '{name} — reference this QSO counts for', 'wbg.typeCall': 'Type a callsign in the entry strip to see prior contacts.', 'wbg.checking': 'checking…', 'wbg.new': 'NEW', 'wbg.noPriorPre': 'No prior QSO with ', 'wbg.noPriorPost': '.', 'wbg.workedBefore': 'Worked before', 'wbg.first': 'First:', 'wbg.last': 'Last:', 'wbg.dxcc': 'DXCC:', 'wbg.entityQsos': '{n} entity QSOs', 'wbg.clearFiltersTitle': 'Clear all column filters', 'wbg.clearFilters': 'Clear filters', 'wbg.columns': 'Columns', 'wbg.olderQsos': '+ {n} older QSOs (not shown — capped for performance)', 'wbg.pickerTitle': 'Worked-before columns', 'wbg.pickerDesc': 'Pick the columns you want visible in the Worked-before table.', 'wbg.allGroups': 'All groups:', 'wbg.all': 'all', 'wbg.none': 'none', 'wbg.grpAwards': 'Awards', 'wbg.resetDefaults': 'Reset to defaults', 'wbg.done': 'Done', 'chn.title': 'Chase new', 'chn.close': 'Hide the panel', 'chn.filterHint': 'Show or hide this kind', 'chn.allFiltered': 'Everything heard is filtered out — turn a category back on above.', 'chn.toggle': 'Chase new', 'chn.count': '{n} heard', 'chn.loading': 'Waiting for the feed…', 'chn.empty': 'Nothing new being decoded near you right now.', 'chn.digitalOnly': 'PSK Reporter — digital modes only, heard within ~300 km of you.', 'chn.option': 'Chase new (PSK Reporter)', 'chn.optionHelp': 'Lists stations being decoded near you that are new against your log — new entity, band, mode, slot, prefix or square. Digital modes only.', 'chn.show': 'Chase new panel', 'clg2.allFiltered': '{n} spots received, none shown — your filters are hiding them all.', 'clg2.activeFilters': 'Active:', 'clg2.clearAllFilters': 'Clear every filter', 'clg2.fBandLock': 'band locked to the rig', 'clg2.fBands': 'bands {list}', 'clg2.fModeLock': 'mode locked to the rig', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'status chips', 'clg2.fHideWorked': 'hide worked', 'clg2.fLotwOnly': 'LoTW users only', 'clg2.fSpotterCont': 'spotter continent', 'clg2.fSource': 'one source node', 'clg2.fSearch': 'search “{q}”', - 'clg2.c.time': 'Time', 'clg2.c.call': 'Call', 'clg2.c.status': 'Status', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Freq', 'clg2.c.band': 'Band', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Pfx', 'clg2.c.cqz': 'CQ Zone', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'ITU Zone', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Short path (°)', 'clg2.h.sp_deg': 'SP°', 'clg2.c.lp_deg': 'Long path (°)', 'clg2.h.lp_deg': 'LP°', 'clg2.c.country': 'Country', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Spotter locator', 'clg2.h.locator': 'Spotter loc', 'clg2.c.county': 'US County', 'clg2.tipNewCounty': 'NEW COUNTY — never worked', 'clg2.tipNewPfx': 'NEW PREFIX — this WPX prefix has never been worked', 'clg2.c.comment': 'Comment', 'clg2.c.received_at': 'Received at', 'clg2.h.received_at': 'Received UTC', 'clg2.c.raw': 'Raw', 'clg2.newDxcc': 'NEW DXCC', 'clg2.newBandMode': 'NEW B+M', 'clg2.newBand': 'NEW BAND', 'clg2.newMode': 'NEW MODE', 'clg2.newSlot': 'NEW SLOT', 'clg2.newCall': 'NEW CALL', 'clg2.wkdCall': 'WKD CALL', 'clg2.newCounty': 'NEW CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newGrid': 'NEW GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NEW GRID — this square has never been worked (grid heard in a CQ on the UDP link)', 'clg2.newPfx': "NEW PFX", 'clg2.newPota': 'NEW POTA', 'clg2.tipNewDxcc': 'NEW DXCC: {country}', 'clg2.tipWorkedCall': 'Already worked this call', 'clg2.tipNewBandMode': 'NEW BAND AND NEW MODE for this entity — neither has been worked with it', 'clg2.tipNewBand': 'NEW BAND for this entity', 'clg2.tipNewSlotBand': 'NEW SLOT (mode not yet worked on this band)', 'clg2.tipNewMode': 'NEW MODE (this mode never worked on this entity)', 'clg2.tipNewSlot': 'NEW SLOT (this band+mode not yet worked)', 'clg2.tipNewCall': 'NEW CALL — this callsign has never been worked on this band and mode (the entity has)', 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Geo', 'clg2.clearFiltersTitle': 'Clear all column filters', 'clg2.clearFilters': 'Clear filters', 'clg2.newSpots': '{n} new spots — click to resume', 'clg2.columns': 'Columns', 'clg2.pickerTitle': 'Cluster columns', 'clg2.pickerDesc': 'Pick the columns you want visible in the Cluster table.', 'clg2.allGroups': 'All groups:', 'clg2.all': 'all', 'clg2.none': 'none', 'clg2.resetDefaults': 'Reset to defaults', 'clg2.done': 'Done', + 'clg2.c.time': 'Time', 'clg2.c.call': 'Call', 'clg2.c.status': 'Status', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Freq', 'clg2.c.band': 'Band', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Pfx', 'clg2.c.cqz': 'CQ Zone', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'ITU Zone', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Short path (°)', 'clg2.h.sp_deg': 'SP°', 'clg2.c.lp_deg': 'Long path (°)', 'clg2.h.lp_deg': 'LP°', 'clg2.c.country': 'Country', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Spotter locator', 'clg2.h.locator': 'Spotter loc', 'clg2.c.county': 'US County', 'clg2.tipNewCounty': 'NEW COUNTY — never worked', 'clg2.tipNewPfx': 'NEW PREFIX — this WPX prefix has never been worked', 'clg2.c.comment': 'Comment', 'clg2.c.received_at': 'Received at', 'clg2.h.received_at': 'Received UTC', 'clg2.c.raw': 'Raw', 'clg2.newDxcc': 'NEW DXCC', 'clg2.newBandMode': 'NEW B+M', 'clg2.newBand': 'NEW BAND', 'clg2.newMode': 'NEW MODE', 'clg2.newSlot': 'NEW SLOT', 'clg2.newCall': 'NEW CALL', 'clg2.wkdCall': 'WKD CALL', 'clg2.newCounty': 'NEW CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newState': 'New State', 'clg2.newGrid': 'NEW GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NEW GRID — this square has never been worked (grid heard in a CQ on the UDP link)', 'clg2.newPfx': "NEW PFX", 'clg2.newPota': 'NEW POTA', 'clg2.tipNewDxcc': 'NEW DXCC: {country}', 'clg2.tipWorkedCall': 'Already worked this call', 'clg2.tipNewBandMode': 'NEW BAND AND NEW MODE for this entity — neither has been worked with it', 'clg2.tipNewBand': 'NEW BAND for this entity', 'clg2.tipNewSlotBand': 'NEW SLOT (mode not yet worked on this band)', 'clg2.tipNewMode': 'NEW MODE (this mode never worked on this entity)', 'clg2.tipNewSlot': 'NEW SLOT (this band+mode not yet worked)', 'clg2.tipNewCall': 'NEW CALL — this callsign has never been worked on this band and mode (the entity has)', 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Geo', 'clg2.clearFiltersTitle': 'Clear all column filters', 'clg2.clearFilters': 'Clear filters', 'clg2.newSpots': '{n} new spots — click to resume', 'clg2.columns': 'Columns', 'clg2.pickerTitle': 'Cluster columns', 'clg2.pickerDesc': 'Pick the columns you want visible in the Cluster table.', 'clg2.allGroups': 'All groups:', 'clg2.all': 'all', 'clg2.none': 'none', 'clg2.resetDefaults': 'Reset to defaults', 'clg2.done': 'Done', // Audio devices & voice keyer (Preferences → Audio devices). 'aud.refreshDevices': 'Refresh devices', 'aud.fromRadio': 'From Radio (RX in)', 'aud.toRadio': 'To Radio (TX out)', 'aud.recMic': 'Recording mic', 'aud.listening': 'Listening (preview)', 'aud.phFromRadio': 'Rig audio output → soundcard input', 'aud.phToRadio': 'Soundcard output → rig mic/data in', 'aud.phRecMic': 'Your microphone (record voice-keyer messages)', 'aud.phListening': 'Local speakers for preview', @@ -697,7 +697,7 @@ const fr: Dict = { 'dec.txNow': 'En emission', 'dec.txIdle': 'Emission', 'dec.working': 'appelle', 'dec.toYou': 'pour toi', 'dec.txUnknown': 'en émission — texte non communiqué', 'dec.txNothing': 'rien en cours d’émission', 'dec.colTime': 'Heure', 'dec.colSnr': 'SNR', 'dec.colMsg': 'Message', 'dec.bgGridUnconf': 'GRID?', 'dec.bgGridUnconfTip': 'Ce carré est contacté mais pas encore confirmé — une QSL à relancer, pas un QSO à faire.', 'dec.colGrid': 'Locator', 'dec.colState': 'État', 'dec.colCountry': 'Pays', 'dec.colBand': 'Bande', 'dec.colMode': 'Mode', 'dec.colStatus': 'Statut', 'dec.stateTip': 'État US', 'dec.wkd': 'Fait', - 'dec.bgPota': 'POTA', 'dec.bgGrid': 'LOC', 'dec.bgPfx': 'PFX', 'dec.bgCounty': 'CTY', + 'dec.bgPota': 'POTA', 'dec.bgGrid': 'LOC', 'dec.bgPfx': 'PFX', 'dec.bgState': 'Nouvel État', 'dec.bgCounty': 'CTY', 'dec.stNew': 'NOUV', 'dec.stBand': 'BANDE', 'dec.stMode': 'MODE', 'dec.stSlot': 'SLOT', 'dec.stCall': 'IND', 'dec.empty': "Aucun decode pour l'instant. Ils arrivent de WSJT-X, JTDX ou MSHV par le lien UDP entrant (Reglages -> UDP).", 'dec.emptyFiltered': 'Aucun decode ne correspond a ces filtres.', @@ -1023,7 +1023,7 @@ const fr: Dict = { 'wbg.awardTip': '{name} — référence comptée pour ce QSO', 'wbg.typeCall': 'Saisissez un indicatif dans la barre pour voir les contacts précédents.', 'wbg.checking': 'vérification…', 'wbg.new': 'NOUVEAU', 'wbg.noPriorPre': 'Aucun QSO précédent avec ', 'wbg.noPriorPost': '.', 'wbg.workedBefore': 'Déjà contacté', 'wbg.first': 'Premier :', 'wbg.last': 'Dernier :', 'wbg.dxcc': 'DXCC :', 'wbg.entityQsos': '{n} QSO avec cette entité', 'wbg.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'wbg.clearFilters': 'Effacer les filtres', 'wbg.columns': 'Colonnes', 'wbg.olderQsos': '+ {n} QSO plus anciens (non affichés — limités pour la performance)', 'wbg.pickerTitle': 'Colonnes « Déjà contacté »', 'wbg.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau « Déjà contacté ».', 'wbg.allGroups': 'Tous les groupes :', 'wbg.all': 'tout', 'wbg.none': 'aucun', 'wbg.grpAwards': 'Diplômes', 'wbg.resetDefaults': 'Réinitialiser', 'wbg.done': 'Terminé', 'chn.title': 'Chasse au nouveau', 'chn.close': 'Masquer le panneau', 'chn.filterHint': 'Afficher ou masquer ce type', 'chn.allFiltered': 'Tout ce qui est entendu est filtré — réactivez une catégorie ci-dessus.', 'chn.toggle': 'Chasse au nouveau', 'chn.count': '{n} entendus', 'chn.loading': 'En attente du flux…', 'chn.empty': 'Rien de nouveau décodé près de vous pour le moment.', 'chn.digitalOnly': 'PSK Reporter — modes numériques uniquement, entendus à moins de ~300 km.', 'chn.option': 'Chasse au nouveau (PSK Reporter)', 'chn.optionHelp': 'Liste les stations décodées près de chez vous qui sont nouvelles par rapport à votre log — entité, bande, mode, créneau, préfixe ou carré. Modes numériques uniquement.', 'chn.show': 'Panneau chasse au nouveau', 'clg2.allFiltered': '{n} spots reçus, aucun affiché — vos filtres les masquent tous.', 'clg2.activeFilters': 'Actifs :', 'clg2.clearAllFilters': 'Effacer tous les filtres', 'clg2.fBandLock': 'bande verrouillée sur la radio', 'clg2.fBands': 'bandes {list}', 'clg2.fModeLock': 'mode verrouillé sur la radio', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'pastilles de statut', 'clg2.fHideWorked': 'masquer les contactés', 'clg2.fLotwOnly': 'utilisateurs LoTW uniquement', 'clg2.fSpotterCont': 'continent du spotteur', 'clg2.fSource': 'un seul nœud source', 'clg2.fSearch': 'recherche « {q} »', - 'clg2.c.time': 'Heure', 'clg2.c.call': 'Indicatif', 'clg2.c.status': 'Statut', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Fréq', 'clg2.c.band': 'Bande', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Préf.', 'clg2.c.cqz': 'Zone CQ', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'Zone ITU', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Chemin court (°)', 'clg2.h.sp_deg': 'CC°', 'clg2.c.lp_deg': 'Chemin long (°)', 'clg2.h.lp_deg': 'CL°', 'clg2.c.country': 'Pays', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Locator du spotter', 'clg2.h.locator': 'Loc spotter', 'clg2.c.county': 'Comté US', 'clg2.tipNewCounty': 'NOUVEAU COMTÉ — jamais contacté', 'clg2.tipNewPfx': "NOUVEAU PRÉFIXE — ce préfixe WPX n'a jamais été contacté", 'clg2.c.comment': 'Commentaire', 'clg2.c.received_at': 'Reçu le', 'clg2.h.received_at': 'Reçu UTC', 'clg2.c.raw': 'Brut', 'clg2.newDxcc': 'NOUV DXCC', 'clg2.newBandMode': 'NOUV B+M', 'clg2.newBand': 'NOUV BANDE', 'clg2.newMode': 'NOUV MODE', 'clg2.newSlot': 'NOUV SLOT', 'clg2.newCall': 'CALL NEUF', 'clg2.wkdCall': 'DÉJÀ QSO', 'clg2.newCounty': 'NOUV CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newGrid': 'NOUV GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NOUVEAU GRID — ce carré n a jamais été contacté (grid entendu dans un CQ sur le lien UDP)', 'clg2.newPfx': "NOUVEAU PFX", 'clg2.newPota': 'NOUV POTA', 'clg2.tipNewDxcc': 'NOUVEAU DXCC : {country}', 'clg2.tipWorkedCall': 'Indicatif déjà contacté', 'clg2.tipNewBandMode': 'NOUVELLE BANDE ET NOUVEAU MODE pour cette entité — aucun des deux n’a été fait avec elle', 'clg2.tipNewBand': 'NOUVELLE BANDE pour cette entité', 'clg2.tipNewSlotBand': 'NOUVEAU SLOT (mode pas encore contacté sur cette bande)', 'clg2.tipNewMode': 'NOUVEAU MODE (ce mode jamais contacté sur cette entité)', 'clg2.tipNewSlot': 'NOUVEAU SLOT (cette bande+mode pas encore contactée)', 'clg2.tipNewCall': "CALL NEUF — cet indicatif n a jamais été contacté sur cette bande et ce mode (l entité, si)", 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Géo', 'clg2.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'clg2.clearFilters': 'Effacer les filtres', 'clg2.newSpots': '{n} nouveaux spots — cliquer pour reprendre', 'clg2.columns': 'Colonnes', 'clg2.pickerTitle': 'Colonnes du cluster', 'clg2.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau du cluster.', 'clg2.allGroups': 'Tous les groupes :', 'clg2.all': 'tout', 'clg2.none': 'aucun', 'clg2.resetDefaults': 'Réinitialiser', 'clg2.done': 'Terminé', + 'clg2.c.time': 'Heure', 'clg2.c.call': 'Indicatif', 'clg2.c.status': 'Statut', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Fréq', 'clg2.c.band': 'Bande', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Préf.', 'clg2.c.cqz': 'Zone CQ', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'Zone ITU', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Chemin court (°)', 'clg2.h.sp_deg': 'CC°', 'clg2.c.lp_deg': 'Chemin long (°)', 'clg2.h.lp_deg': 'CL°', 'clg2.c.country': 'Pays', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Locator du spotter', 'clg2.h.locator': 'Loc spotter', 'clg2.c.county': 'Comté US', 'clg2.tipNewCounty': 'NOUVEAU COMTÉ — jamais contacté', 'clg2.tipNewPfx': "NOUVEAU PRÉFIXE — ce préfixe WPX n'a jamais été contacté", 'clg2.c.comment': 'Commentaire', 'clg2.c.received_at': 'Reçu le', 'clg2.h.received_at': 'Reçu UTC', 'clg2.c.raw': 'Brut', 'clg2.newDxcc': 'NOUV DXCC', 'clg2.newBandMode': 'NOUV B+M', 'clg2.newBand': 'NOUV BANDE', 'clg2.newMode': 'NOUV MODE', 'clg2.newSlot': 'NOUV SLOT', 'clg2.newCall': 'CALL NEUF', 'clg2.wkdCall': 'DÉJÀ QSO', 'clg2.newCounty': 'NOUV CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newState': 'Nouvel État', 'clg2.newGrid': 'NOUV GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NOUVEAU GRID — ce carré n a jamais été contacté (grid entendu dans un CQ sur le lien UDP)', 'clg2.newPfx': "NOUVEAU PFX", 'clg2.newPota': 'NOUV POTA', 'clg2.tipNewDxcc': 'NOUVEAU DXCC : {country}', 'clg2.tipWorkedCall': 'Indicatif déjà contacté', 'clg2.tipNewBandMode': 'NOUVELLE BANDE ET NOUVEAU MODE pour cette entité — aucun des deux n’a été fait avec elle', 'clg2.tipNewBand': 'NOUVELLE BANDE pour cette entité', 'clg2.tipNewSlotBand': 'NOUVEAU SLOT (mode pas encore contacté sur cette bande)', 'clg2.tipNewMode': 'NOUVEAU MODE (ce mode jamais contacté sur cette entité)', 'clg2.tipNewSlot': 'NOUVEAU SLOT (cette bande+mode pas encore contactée)', 'clg2.tipNewCall': "CALL NEUF — cet indicatif n a jamais été contacté sur cette bande et ce mode (l entité, si)", 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Géo', 'clg2.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'clg2.clearFilters': 'Effacer les filtres', 'clg2.newSpots': '{n} nouveaux spots — cliquer pour reprendre', 'clg2.columns': 'Colonnes', 'clg2.pickerTitle': 'Colonnes du cluster', 'clg2.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau du cluster.', 'clg2.allGroups': 'Tous les groupes :', 'clg2.all': 'tout', 'clg2.none': 'aucun', 'clg2.resetDefaults': 'Réinitialiser', 'clg2.done': 'Terminé', // Périphériques audio et manipulateur vocal (Préférences → Périphériques audio). 'aud.refreshDevices': 'Actualiser les périphériques', 'aud.fromRadio': 'Depuis la radio (entrée RX)', 'aud.toRadio': 'Vers la radio (sortie TX)', 'aud.recMic': "Micro d'enregistrement", 'aud.listening': 'Écoute (pré-écoute)', 'aud.phFromRadio': 'Sortie audio du poste → entrée carte son', 'aud.phToRadio': 'Sortie carte son → entrée micro/data du poste', 'aud.phRecMic': 'Votre microphone (enregistrer les messages vocaux)', 'aud.phListening': 'Haut-parleurs locaux pour la pré-écoute', diff --git a/frontend/src/lib/spotMarkers.ts b/frontend/src/lib/spotMarkers.ts index b15baf6..d522087 100644 --- a/frontend/src/lib/spotMarkers.ts +++ b/frontend/src/lib/spotMarkers.ts @@ -18,7 +18,7 @@ // grid magenta — the last hue in the categorical set that is not already // spoken for here and does not read as a status; a grid is // never urgent the way a new entity is -export type SpotMarkerKey = 'new_pota' | 'new_county' | 'new_pfx' | 'worked_call' | 'new_grid'; +export type SpotMarkerKey = 'new_pota' | 'new_county' | 'new_pfx' | 'worked_call' | 'new_grid' | 'new_state'; export type SpotMarker = { key: SpotMarkerKey; @@ -32,6 +32,7 @@ export const SPOT_MARKERS: SpotMarker[] = [ { key: 'new_county', colour: 'var(--chart-5)', labelKey: 'clg2.newCounty' }, { key: 'new_pfx', colour: 'var(--caution)', labelKey: 'clg2.newPfx' }, { key: 'new_grid', colour: 'var(--chart-7)', labelKey: 'clg2.newGrid' }, + { key: 'new_state', colour: 'var(--chart-3)', labelKey: 'clg2.newState' }, { key: 'worked_call', colour: 'var(--info)', labelKey: 'clg2.wkdCall' }, ]; diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 3ef7003..fc344e5 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -3957,6 +3957,7 @@ export namespace main { new_county: boolean; county?: string; state?: string; + new_state: boolean; new_pota: boolean; grid?: string; new_grid: boolean; @@ -3983,6 +3984,7 @@ export namespace main { this.new_county = source["new_county"]; this.county = source["county"]; this.state = source["state"]; + this.new_state = source["new_state"]; this.new_pota = source["new_pota"]; this.grid = source["grid"]; this.new_grid = source["new_grid"]; diff --git a/internal/qso/qso.go b/internal/qso/qso.go index a65ba6a..ea14ec9 100644 --- a/internal/qso/qso.go +++ b/internal/qso/qso.go @@ -2752,6 +2752,29 @@ func (r *Repo) WorkedCountyKeys(ctx context.Context, keyFn func(state, cnty stri return out, rows.Err() } +// WorkedStateKeys returns the set of US states already worked, uppercased — +// the WAS ledger the cluster and decode panels judge NEW STATE against. +func (r *Repo) WorkedStateKeys(ctx context.Context) (map[string]struct{}, error) { + rows, err := r.db.QueryContext(ctx, + `SELECT DISTINCT UPPER(COALESCE(state,'')) FROM qso + WHERE dxcc IN (291,110,6) AND state IS NOT NULL AND state != ''`) + if err != nil { + return nil, err + } + defer rows.Close() + out := make(map[string]struct{}, 64) + for rows.Next() { + var st string + if err := rows.Scan(&st); err != nil { + return nil, err + } + if st != "" { + out[st] = struct{}{} + } + } + return out, rows.Err() +} + // CountQSLViaRouting counts the QSOs whose qsl_via holds a routing method // instead of a manager, per isRouting. //