fix(cluster): muting a worked spot must not brighten it
"No colour on worked" blanked the spot status as well as the worked-call flag, on the theory that a spot bringing no novelty should stop painting entirely. But the status is exactly what the cluster list reads to DIM a row. Blanking it made isDull() fall through its "unresolved, never dim" guard, so every quiet grey row came back at full brightness. The option meant to calm the list was the one making it shout. It also forced a `muted` flag to exist purely so tooltips could say that an empty status did not, this time, mean "entity not resolved". It now removes the blue already-worked mark and nothing else, which is all it ever needed to do: a worked entity already renders with no colour of its own and is already dimmed. The flag, its two tooltip strings and the special-cased band map styling all go with it.
This commit is contained in:
+4
-2
@@ -7,14 +7,16 @@
|
||||
"DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing.",
|
||||
"A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index.",
|
||||
"Station information gains an IOTA field (ADIF MY_IOTA, e.g. EU-005), stamped on every QSO like the SOTA and POTA references beside it. The QSO table has carried the field since the first release and both ADIF import and export already handled it — only the station profile could not supply it, so an island activation meant typing the reference on every contact.",
|
||||
"PowerGenius XL: fixed the amplifier link dropping and reconnecting every few seconds, and the stall while transmitting. The amp pushes status frames on the same socket it answers commands on, and it pushes them constantly once in OPERATE; OpsLog read one line per command and took whatever arrived first as its answer, so the stream slipped permanently one reply behind until a command timed out and the connection was dropped. Replies are now matched to the command that asked for them. Only ever visible over a remote link with the amplifier in OPERATE."
|
||||
"PowerGenius XL: fixed the amplifier link dropping and reconnecting every few seconds, and the stall while transmitting. The amp pushes status frames on the same socket it answers commands on, and it pushes them constantly once in OPERATE; OpsLog read one line per command and took whatever arrived first as its answer, so the stream slipped permanently one reply behind until a command timed out and the connection was dropped. Replies are now matched to the command that asked for them. Only ever visible over a remote link with the amplifier in OPERATE.",
|
||||
"No colour on worked stations now does only that: it removes the blue already-worked mark and leaves everything else alone. It used to blank the spot status as well, and the status is what dims a quiet row — so turning the option on made every grey row bright white, which is the opposite of what it is for."
|
||||
],
|
||||
"fr": [
|
||||
"Cluster DX : un badge L à côté de l indicatif signale une station qui utilise LoTW, avec le filtre Utilisateurs LoTW seulement qui va avec, et un filtre Continent du spotter restreint selon l origine du spot — un report JA sur 20 m dit peu de chose à un Européen sur son propre chemin. Le panneau de filtres a été remis d aplomb selon une seule règle : un interrupteur est un comportement qu on active ou non, les pastilles choisissent dans un ensemble. Plus rien n existe sous les deux formes, les boutons de verrouillage sont dans le titre de la section qu ils verrouillent, chaque section s efface de la même façon, et le panneau est enfin traduit.",
|
||||
"Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien.",
|
||||
"Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index.",
|
||||
"Les informations station gagnent un champ IOTA (MY_IOTA en ADIF, par exemple EU-005), estampillé sur chaque QSO comme les références SOTA et POTA à côté. La table des QSO portait le champ depuis la première version et l import comme l export ADIF le géraient déjà — seul le profil station ne savait pas le fournir, donc une activation d île obligeait à retaper la référence à chaque contact.",
|
||||
"PowerGenius XL : corrigé le lien avec l ampli qui tombait et se reconnectait toutes les quelques secondes, et le blocage en émission. L ampli pousse des trames d état sur la socket même où il répond aux commandes, et il en pousse en permanence dès qu il est en OPERATE ; OpsLog lisait une ligne par commande et prenait la première arrivée pour sa réponse, donc le flux glissait définitivement d une réponse de retard jusqu à expiration et fermeture du lien. Les réponses sont désormais appariées à la commande qui les a demandées. Visible uniquement en liaison distante avec l ampli en OPERATE."
|
||||
"PowerGenius XL : corrigé le lien avec l ampli qui tombait et se reconnectait toutes les quelques secondes, et le blocage en émission. L ampli pousse des trames d état sur la socket même où il répond aux commandes, et il en pousse en permanence dès qu il est en OPERATE ; OpsLog lisait une ligne par commande et prenait la première arrivée pour sa réponse, donc le flux glissait définitivement d une réponse de retard jusqu à expiration et fermeture du lien. Les réponses sont désormais appariées à la commande qui les a demandées. Visible uniquement en liaison distante avec l ampli en OPERATE.",
|
||||
"Aucune couleur sur les stations déjà contactées ne fait plus que ça : la marque bleue disparaît, le reste ne bouge pas. L option vidait aussi le statut du spot, or c est le statut qui estompe une ligne sans intérêt — l activer rendait donc toutes les lignes grises blanches et éclatantes, soit l inverse du but recherché."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,9 +38,6 @@ type SpotStatusEntry = {
|
||||
new_county?: boolean;
|
||||
new_pota?: boolean;
|
||||
new_pfx?: boolean;
|
||||
// muted: the status was emptied on purpose by the "no colour on worked
|
||||
// stations" option, so an empty status here is a choice, not an unknown.
|
||||
muted?: boolean;
|
||||
};
|
||||
|
||||
// The extra markers are ORTHOGONAL to the entity status: a spot can be a worked
|
||||
@@ -152,7 +149,7 @@ function LegendDot({ cls, colour, label }: { cls?: string; colour?: string; labe
|
||||
|
||||
// Human-readable label for a spot status — used in the pill hover tooltip
|
||||
// so the operator can see WHY a spot is coloured the way it is.
|
||||
function statusLabel(s: string, t: (k: string) => string, muted = false): string {
|
||||
function statusLabel(s: string, t: (k: string) => string): string {
|
||||
switch (s) {
|
||||
case 'new': return t('bmp.statusNew');
|
||||
case 'new-band': return t('bmp.statusNewBand');
|
||||
@@ -160,11 +157,10 @@ function statusLabel(s: string, t: (k: string) => string, muted = false): string
|
||||
case 'new-slot': return t('bmp.statusNewSlot');
|
||||
case 'new-call': return t('bmp.statusNewCall');
|
||||
case 'worked': return t('bmp.statusWorked');
|
||||
// An empty status means the entity could not be resolved — EXCEPT when the
|
||||
// "no colour on worked stations" option emptied it on purpose. Saying
|
||||
// "entity not resolved" there was a flat contradiction of the country
|
||||
// printed two words earlier in the same tooltip.
|
||||
default: return muted ? t('bmp.statusMuted') : t('bmp.statusUnresolved');
|
||||
// An empty status means the entity could not be resolved. Nothing else
|
||||
// empties it: the mute option leaves the status alone and takes only the
|
||||
// already-worked-callsign mark.
|
||||
default: return t('bmp.statusUnresolved');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +173,7 @@ const QUIET_STYLE = {
|
||||
dot: 'fill-border',
|
||||
};
|
||||
|
||||
function statusStyle(s: string, muted = false): { pill: string; bar: string; line: string; dot: string } {
|
||||
// A muted spot must land on QUIET, never on the default branch below. That
|
||||
// default paints bg-primary/60 — the theme's burnt orange at 60 % over a dark
|
||||
// card, i.e. brown — because it was written for the rare unresolved entity and
|
||||
// orange is this app's "look here" colour. With the mute option on, most of
|
||||
// the map is muted, so the whole band map turned brown and the quietest spots
|
||||
// shouted the loudest.
|
||||
if (muted) return QUIET_STYLE;
|
||||
function statusStyle(s: string): { pill: string; bar: string; line: string; dot: string } {
|
||||
// pill = full pill background+text+border
|
||||
// bar = thick left accent inside the pill
|
||||
// line = SVG leader stroke (visible on hover)
|
||||
@@ -581,7 +570,7 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
|
||||
{placed.map((p, i) => {
|
||||
const k = spotStatusKey(p.spot.dx_call, p.spot.band ?? '', p.spot.comment ?? '', p.spot.freq_hz);
|
||||
const st = spotStatus[k]?.status ?? '';
|
||||
const style = statusStyle(st, spotStatus[k]?.muted);
|
||||
const style = statusStyle(st);
|
||||
const labelMidY = p.labelY + PILL_H / 2;
|
||||
const bumped = Math.abs(p.freqY - labelMidY) > 0.5;
|
||||
return (
|
||||
@@ -629,7 +618,7 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
|
||||
const k = spotStatusKey(p.spot.dx_call, p.spot.band ?? '', p.spot.comment ?? '', p.spot.freq_hz);
|
||||
const entry = spotStatus[k];
|
||||
const st = entry?.status ?? '';
|
||||
const style = statusStyle(st, entry?.muted);
|
||||
const style = statusStyle(st);
|
||||
const mode = inferSpotMode(p.spot.comment ?? '', p.spot.freq_hz);
|
||||
return (
|
||||
<button
|
||||
@@ -642,7 +631,7 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
|
||||
'hover:translate-x-0.5 hover:shadow',
|
||||
style.pill,
|
||||
)}
|
||||
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t, entry?.muted)}${markersFor(entry).map((m) => ' · ' + t(BMP_MARKER_LABEL[m.key])).join('')}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
|
||||
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t)}${markersFor(entry).map((m) => ' · ' + t(BMP_MARKER_LABEL[m.key])).join('')}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
|
||||
>
|
||||
{/* Left accent strip. With no extra marker it repeats the status
|
||||
colour, exactly as before; otherwise it splits into one
|
||||
|
||||
@@ -343,7 +343,7 @@ const en: Dict = {
|
||||
'chp.workedBefore': 'Worked before', 'chp.typeCallsign': 'Type a callsign to see prior contacts.', 'chp.checking': 'checking…', 'chp.new': 'NEW',
|
||||
'chp.noPriorQso': 'No prior QSO with this callsign.', 'chp.first': 'First', 'chp.last': 'Last', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Band', 'chp.mode': 'Mode',
|
||||
'chp.lotwRcvd': 'LoTW rcvd', 'chp.bureauRcvd': 'Bureau rcvd', 'chp.olderQsos': '+ {n} older QSOs',
|
||||
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)', 'bmp.statusNewCall': 'NEW CALL (this callsign never worked on this band and mode)', 'bmp.statusNewMode': 'NEW MODE (mode never worked for this entity)', 'bmp.statusMuted': 'Nothing new here — entity already worked (muted by your display option)',
|
||||
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)', 'bmp.statusNewCall': 'NEW CALL (this callsign never worked on this band and mode)', 'bmp.statusNewMode': 'NEW MODE (mode never worked for this entity)',
|
||||
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
||||
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
||||
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.widthTip': 'Drag to resize — double-click to reset', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
||||
@@ -755,7 +755,7 @@ const fr: Dict = {
|
||||
'chp.workedBefore': 'Déjà contacté', 'chp.typeCallsign': 'Saisis un indicatif pour voir les contacts précédents.', 'chp.checking': 'vérification…', 'chp.new': 'NOUVEAU',
|
||||
'chp.noPriorQso': 'Aucun QSO antérieur avec cet indicatif.', 'chp.first': 'Premier', 'chp.last': 'Dernier', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Bande', 'chp.mode': 'Mode',
|
||||
'chp.lotwRcvd': 'LoTW reçue', 'chp.bureauRcvd': 'Bureau reçue', 'chp.olderQsos': '+ {n} QSO plus anciens',
|
||||
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)', 'bmp.statusNewCall': "CALL NEUF (indicatif jamais contacté sur cette bande et ce mode)", 'bmp.statusNewMode': 'NOUVEAU MODE (mode jamais contacté pour cette entité)', 'bmp.statusMuted': 'Rien de nouveau ici — entité déjà contactée (masqué par ton option d affichage)',
|
||||
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)', 'bmp.statusNewCall': "CALL NEUF (indicatif jamais contacté sur cette bande et ce mode)", 'bmp.statusNewMode': 'NOUVEAU MODE (mode jamais contacté pour cette entité)',
|
||||
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
||||
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
||||
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.widthTip': 'Glisser pour redimensionner — double-clic pour réinitialiser', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
||||
|
||||
@@ -31,27 +31,8 @@ type Entry = {
|
||||
new_county?: boolean;
|
||||
new_pota?: boolean;
|
||||
new_pfx?: boolean;
|
||||
muted?: boolean;
|
||||
} | undefined;
|
||||
|
||||
// bringsNothingNew: the ENTITY is resolved and worked, and no other dimension
|
||||
// (county, park, prefix) is new. Same test the cluster list already used to dim
|
||||
// a row — muting reuses it rather than inventing a second notion of "done".
|
||||
//
|
||||
// Entity-level, NOT callsign-level: an operator can be on their 421st Bulgarian
|
||||
// and still have never worked that particular station. That spot is muted here
|
||||
// by design — it brings nothing to an award — and turning on the slot option is
|
||||
// what brings it back, which is why the promotion above runs first.
|
||||
//
|
||||
// Note what is NOT muted: a status of new-band / new-slot survives, because
|
||||
// having worked that callsign once on another band says nothing about the band
|
||||
// in front of you.
|
||||
function bringsNothingNew(s: Entry): boolean {
|
||||
if (!s || !s.status) return false; // unresolved — never hide, it would flicker
|
||||
if (s.status === 'new' || s.status === 'new-band' || s.status === 'new-mode' || s.status === 'new-slot' || s.status === 'new-call') return false;
|
||||
return !(s.new_pota || s.new_county || s.new_pfx);
|
||||
}
|
||||
|
||||
// applySpotDisplay rewrites a status entry per the options, so every consumer —
|
||||
// colour, badge, status text — follows from one decision instead of each panel
|
||||
// re-deriving it.
|
||||
@@ -59,27 +40,26 @@ export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions):
|
||||
if (!s) return s;
|
||||
let e = s;
|
||||
|
||||
// ORDER MATTERS, and it is the whole difference between the two options
|
||||
// composing and one cancelling the other.
|
||||
//
|
||||
// Slot promotion runs FIRST. A callsign not yet worked on this band and mode
|
||||
// is not "done", so it must never be swallowed by the mute below — yet the
|
||||
// mute test only looks at the entity, and an unworked callsign inside a worked
|
||||
// entity is precisely the spot the second option exists to surface. Promoting
|
||||
// first protects it for free: bringsNothingNew() returns false on new-slot.
|
||||
// Slot promotion runs first: a callsign not yet worked on this band and mode
|
||||
// is not done, whatever the entity says, so it earns a status before the mute
|
||||
// below can take its colour away.
|
||||
if (o.slotHighlight && e.worked_slot === false && (!e.status || e.status === 'worked')) {
|
||||
e = { ...e, status: 'new-call' } as NonNullable<T>;
|
||||
}
|
||||
|
||||
if (o.muteWorked && bringsNothingNew(e)) {
|
||||
// Strip everything that paints: the row keeps its data, loses its emphasis.
|
||||
// Mute drops the blue already-worked-callsign mark, and NOTHING else.
|
||||
//
|
||||
// muted says WHY the status went empty. An empty status already meant
|
||||
// "entity not resolved" in the band map, so without this flag every muted
|
||||
// spot claimed its entity was unknown — with the country printed right next
|
||||
// to it. Blanking is still what drives the colour, the badges and the
|
||||
// ranking; muted only lets the tooltip stay honest.
|
||||
return { ...e, status: '', worked_call: false, muted: true } as T;
|
||||
// It used to blank the status as well, on the theory that a spot bringing no
|
||||
// novelty should stop painting entirely. That was wrong twice over. The status
|
||||
// is what the cluster list reads to DIM a row, so blanking it turned every
|
||||
// quiet grey row bright white — the option made the list louder, not quieter.
|
||||
// And an empty status means "entity not resolved" everywhere else, so muted
|
||||
// spots had to carry a flag saying they did not really mean that.
|
||||
//
|
||||
// Leaving the status alone costs nothing: a worked entity already renders with
|
||||
// no colour and gets dimmed, so removing the blue is the entire job.
|
||||
if (o.muteWorked) {
|
||||
e = { ...e, worked_call: false } as NonNullable<T>;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user