fix(cluster): let the two display options compose, and stop claiming a station was worked

Three defects in the options added in the previous commit, all found on air.

Order. Mute returned early, so with both options on the mute swallowed exactly
the spots the slot option existed to surface: the mute test is entity-level, and
an unworked callsign inside a worked entity passes it. Slot promotion now runs
first, and protects itself for free since bringsNothingNew is false on new-slot.
The two were documented as composing - "mute what is done, light up what is
not" - and they did not.

Empty status. Muting emptied the status, but an empty status already meant
"entity not resolved" in the band map, so every muted spot claimed its entity
was unknown with the country printed two words earlier in the same tooltip. A
muted flag now records WHY it went empty; blanking still drives the colour, the
badges and the ranking.

Wording. The muted tooltip said "already worked" of a station never worked -
LZ8NG on a 421st Bulgarian. What is worked is the ENTITY on this band and mode,
so that is what it says now.

new-mode had no case in the band map's statusLabel and fell through to "entity
not resolved" too. Pre-existing, same one-line switch.
This commit is contained in:
2026-08-10 12:01:31 +02:00
parent a3815c24a1
commit 2cb1add3db
3 changed files with 43 additions and 15 deletions
+30 -10
View File
@@ -31,12 +31,18 @@ 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
// 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.
@@ -51,15 +57,29 @@ function bringsNothingNew(s: Entry): boolean {
// re-deriving it.
export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions): T {
if (!s) return s;
if (o.muteWorked && bringsNothingNew(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.
if (o.slotHighlight && e.worked_slot === false && (!e.status || e.status === 'worked')) {
e = { ...e, status: 'new-slot' } as NonNullable<T>;
}
if (o.muteWorked && bringsNothingNew(e)) {
// Strip everything that paints: the row keeps its data, loses its emphasis.
return { ...s, status: '', worked_call: false } as T;
//
// 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;
}
if (o.slotHighlight && s.worked_slot === false) {
// Not worked on this band+mode. If the entity check found nothing new, say
// so with the slot status rather than leaving the spot colourless — that is
// exactly the row this option exists to surface.
if (!s.status || s.status === 'worked') return { ...s, status: 'new-slot' } as T;
}
return s;
return e;
}