feat(cluster): show the grid, and flag a new one

Finishes the half of this that was already computing on the backend and reaching
nobody. A Grid column in the Geo group, NEW GRID as a badge in Status, a filled
cell in the marker's own colour, and a filter chip.

new_grid joins lib/spotMarkers rather than getting colours of its own, so the
badge, the cell fill and the chip cannot drift apart - and the per-marker colour
setting will drive it with the rest from one table. Magenta: the last hue in the
categorical set not already spoken for, and one that does not read as a status,
because a new square is never urgent the way a new entity is.

The band map leaves it to the cluster, as it already leaves the prefix. The pill
is 22 px tall and its accent strip stops being readable past three segments.

A row carrying a new grid is no longer "dull", or the dimming would grey out the
one thing worth looking at.

The column is off by default, like the other Geo columns: it is only ever filled
for stations this receiver decoded over the UDP link, so for an operator who
does not run digital it would be a permanently empty column.
This commit is contained in:
2026-08-10 20:05:34 +02:00
parent d273d21f1a
commit 4f87cedc2c
7 changed files with 36 additions and 11 deletions
+5 -2
View File
@@ -1487,7 +1487,7 @@ export default function App() {
// is an OR across all of them, as it already was for a worked callsign.
// 'new-call' is a DISPLAY status, produced by the slot-highlight option rather
// than by the backend, so it is named here and not in SpotStatusKey.
type SpotFilterKey = SpotStatusKey | 'new-pota' | 'new-county' | 'new-pfx' | 'new-call';
type SpotFilterKey = SpotStatusKey | 'new-pota' | 'new-county' | 'new-pfx' | 'new-call' | 'new-grid';
// Display options, shared with the band map through lib/spotDisplay. Kept in
// localStorage (mirrored to settings by writeUiPref) so both panels and the
// filter predicate read one source without prop-drilling through three levels.
@@ -4713,7 +4713,8 @@ export default function App() {
|| (!!e?.worked_call && clusterStatusFilter.has('worked'))
|| (!!e?.new_pota && clusterStatusFilter.has('new-pota'))
|| (!!e?.new_county && clusterStatusFilter.has('new-county'))
|| (!!e?.new_pfx && clusterStatusFilter.has('new-pfx'));
|| (!!e?.new_pfx && clusterStatusFilter.has('new-pfx'))
|| (!!e?.new_grid && clusterStatusFilter.has('new-grid'));
if (!matches) return false;
}
// LoTW only, and the spotter's continent. Both are properties of the
@@ -4893,6 +4894,8 @@ export default function App() {
{ k: 'new-pota' as SpotFilterKey, label: 'NEW POTA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
{ k: 'new-county' as SpotFilterKey, label: 'NEW COUNTY', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
{ k: 'new-pfx' as SpotFilterKey, label: 'NEW PFX', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
// Only ever set for a station this receiver decoded over the UDP link.
{ k: 'new-grid' as SpotFilterKey, label: 'NEW GRID', cls: 'bg-muted text-foreground border-border' },
// Blue, like the already-worked call in the grid. Selecting it keeps
// worked spots; the separate "Hide worked" checkbox drops them — they
// are opposite controls, so don't use both at once.
+4 -3
View File
@@ -48,12 +48,13 @@ type SpotStatusEntry = {
//
// Their colours come from lib/spotMarkers, shared with the cluster list: the
// same fact must not be violet in one panel and green in the next.
// The map shows three of the four. A new PREFIX is left to the cluster list: the
// The map shows three of the five. A new PREFIX and a new GRID are left to the
// cluster list, which has the width to name them: the
// pill is 22 px tall, and a fourth segment turns the strip into a colour code
// nobody can read at a glance. Add it here the day the strip earns more room.
const BMP_MARKERS = SPOT_MARKERS.filter((m) => m.key !== 'new_pfx');
const BMP_MARKERS = SPOT_MARKERS.filter((m) => m.key !== 'new_pfx' && m.key !== 'new_grid');
const markersFor = (e: SpotStatusEntry | undefined) =>
activeMarkers(e).filter((m) => m.key !== 'new_pfx');
activeMarkers(e).filter((m) => m.key !== 'new_pfx' && m.key !== 'new_grid');
// The legend spells the markers out; the cluster list's badges are abbreviated
// ("NEW CTY") because they sit in a narrow cell, and there is room here.
+15 -1
View File
@@ -193,7 +193,7 @@ function statusColor(s: SpotStatusEntry | undefined): string | null {
function isDull(s: SpotStatusEntry | undefined): boolean {
if (!s || !s.status) return false;
if (s.status === 'new' || s.status === 'new-band' || s.status === 'new-mode' || s.status === 'new-slot' || s.status === 'new-call') return false;
return !(s.worked_call || s.new_pota || s.new_county || s.new_pfx);
return !(s.worked_call || s.new_pota || s.new_county || s.new_pfx || s.new_grid);
}
const makeColCatalog = (t: TFn): ColEntry[] => [
@@ -266,6 +266,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
if (s?.new_county) parts.push(t('clg2.newCounty'));
if (s?.new_pota) parts.push(t('clg2.newPota'));
if (s?.new_pfx) parts.push(t('clg2.newPfx'));
if (s?.new_grid) parts.push(t('clg2.newGrid'));
return parts.join(' ');
},
cellRenderer: (p: any) => {
@@ -286,6 +287,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
if (s?.new_county) parts.push({ text: t('clg2.newCounty'), color: markerColour('new_county') });
if (s?.new_pota) parts.push({ text: t('clg2.newPota'), color: markerColour('new_pota') });
if (s?.new_pfx) parts.push({ text: t('clg2.newPfx'), color: markerColour('new_pfx') });
if (s?.new_grid) parts.push({ text: t('clg2.newGrid'), color: markerColour('new_grid') });
if (parts.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}></span>;
return (
<span style={{ whiteSpace: 'nowrap' }}>
@@ -362,6 +364,18 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
: { color: 'var(--muted-foreground)' }) as any,
tooltipValueGetter: (p: any) => (statusFor(p)?.new_pfx ? t('clg2.tipNewPfx') : undefined),
},
{
group: 'Geo', label: t('clg2.c.grid'), colId: 'grid',
headerName: t('clg2.c.grid'), width: 70, cellClass: 'font-mono',
// Not on the spot: the square a station announced in a CQ that THIS receiver
// decoded over the WSJT-X link, so it is filled for the digital watering
// hole being monitored and empty everywhere else. A cluster line carries the
// spotter's grid at best, never the DX's.
valueGetter: (p: any) => statusFor(p)?.grid ?? '',
cellStyle: (p: any) => (statusFor(p)?.new_grid ? fillStyle(markerColour('new_grid')) : null) as any,
cellRenderer: (p: any) => cellText(p.value, null),
tooltipValueGetter: (p: any) => (statusFor(p)?.new_grid ? t('clg2.tipNewGrid') : undefined),
},
{
group: 'Geo', label: t('clg2.c.county'), colId: 'county',
headerName: t('clg2.c.county'), width: 130, cellClass: 'font-mono',
File diff suppressed because one or more lines are too long
+1
View File
@@ -31,6 +31,7 @@ type Entry = {
new_county?: boolean;
new_pota?: boolean;
new_pfx?: boolean;
new_grid?: boolean;
} | undefined;
// applySpotDisplay rewrites a status entry per the options, so every consumer —
+5 -1
View File
@@ -15,7 +15,10 @@
// statuses, blue is "callsign worked", green is POTA
// worked blue — matches the WKD-CALL badge the cluster list has always used
// prefix yellow — unchanged
export type SpotMarkerKey = 'new_pota' | 'new_county' | 'new_pfx' | 'worked_call';
// 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 SpotMarker = {
key: SpotMarkerKey;
@@ -28,6 +31,7 @@ export const SPOT_MARKERS: SpotMarker[] = [
{ key: 'new_pota', colour: 'var(--success)', labelKey: 'clg2.newPota' },
{ 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: 'worked_call', colour: 'var(--info)', labelKey: 'clg2.wkdCall' },
];