feat(sat): the pass table is the whole list

Asked why the satellite dropdown exists when the passes are on the right, and
the honest answer was that the table was incomplete. Three kinds of satellite
never appeared in it: QO-100, which has no pass because it never sets; one whose
elements have not arrived, which cannot be predicted at all; and one whose next
pass falls beyond the prediction window. Following a satellite and not finding it
in the list reads as OpsLog having lost it.

They are listed at the end now, each saying which of the three it is, and
clicking one selects it exactly like a pass row. The table stops being "what is
coming" and becomes "what you follow, soonest first" — which is what an operator
was scanning it for anyway.

The dropdown stays, and now for one reason rather than by default: it is the
only selector left when the readout column is hidden, which is what an operator
does when they want the map full width during a pass.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-09 12:01:08 +02:00
co-authored by Claude Opus 5
parent b15055ba4a
commit 660b1bfa1f
3 changed files with 44 additions and 4 deletions
+38 -2
View File
@@ -246,6 +246,19 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
return birds.filter((b) => b.has_elements && (b.transponders?.length ?? 0) > 0);
}, [birds]);
const bird = useMemo(() => birds.find((b) => b.name === sel) ?? null, [birds, sel]);
// The satellites you follow that have NO pass in the table.
//
// They are the reason the table was not the whole list: QO-100 never has a
// pass because it never sets, a bird whose elements have not arrived cannot
// be predicted at all, and one whose next pass falls outside the prediction
// window is simply beyond it. Left out, those three looked like satellites
// OpsLog had lost — so they are listed at the end, each saying which of the
// three it is, and clicking one selects it exactly like a pass row.
const idle = useMemo(() => {
const withPass = new Set(passes.map((p) => p.name));
return shown.filter((b) => !withPass.has(b.name));
}, [shown, passes]);
const tp = bird?.transponders?.[tpIdx] ?? null;
// The mode each satellite is worked in, for the pass table's dot. Its first
// transponder: on a bird that has two, the first is the one it is known for.
@@ -881,10 +894,10 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
{t('sat.nextPasses')}
</div>
<div className="flex-1 min-h-0 overflow-auto">
{passes.length === 0 && (
{passes.length === 0 && idle.length === 0 && (
<div className="p-2 text-[11px] text-muted-foreground">{t('sat.noPasses')}</div>
)}
{passes.length > 0 && (
{(passes.length > 0 || idle.length > 0) && (
// A real table, so the name column takes the width the longest
// name needs — "ZHUHAI-1 OVS-1A" was cut to eight characters in
// a fixed one — and the rest keeps its columns lined up under
@@ -933,6 +946,29 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
</tr>
);
})}
{/* The rest of what you follow, so the table IS the list:
nothing you can select is missing from it. */}
{idle.map((b) => {
const why = b.geostationary ? t('sat.alwaysUp')
: !b.has_elements ? t('sat.noElements')
: t('sat.noPassWindow');
return (
<tr
key={`idle-${b.name}`}
onClick={() => setSel(b.name)}
className={cn('cursor-pointer hover:bg-accent/50 border-t border-border/40',
b.name === sel && 'bg-accent/40')}
>
<td className="px-2 py-1 whitespace-nowrap">
<span className={cn('font-medium', !b.has_elements && 'text-muted-foreground')}>{b.name}</span>
<ModeDot mode={modeOf(b.name)} />
</td>
<td colSpan={4} className="px-2 py-1 text-right text-muted-foreground whitespace-nowrap">
{why}
</td>
</tr>
);
})}
</tbody>
</table>
)}
+2
View File
@@ -606,6 +606,7 @@ const en: Dict = {
'sat.tipEl': 'Elevation', 'sat.tipAz': 'Azimuth', 'sat.tipRange': 'Distance', 'sat.tipAlt': 'Altitude',
'sat.tone': 'Tone', 'sat.toneHint': 'CTCSS on the uplink', 'sat.toneNone': 'no tone needed',
'sat.rotAzOnly': 'azimuth only',
'sat.alwaysUp': 'always up', 'sat.noPassWindow': 'no pass in the window',
'sat.tipAos': 'Rises', 'sat.tipLos': 'Sets', 'sat.tipMaxEl': 'Peak',
'sat.tipBelow': 'below the horizon', 'sat.tipNoPass': 'no pass in the prediction window',
'sat.approaching': 'approaching', 'sat.receding': 'receding', 'sat.below': 'below the horizon',
@@ -1224,6 +1225,7 @@ const fr: Dict = {
'sat.tipEl': 'Élévation', 'sat.tipAz': 'Azimut', 'sat.tipRange': 'Distance', 'sat.tipAlt': 'Altitude',
'sat.tone': 'Tonalité', 'sat.toneHint': 'CTCSS sur la montée', 'sat.toneNone': 'aucune tonalité requise',
'sat.rotAzOnly': 'azimut seul',
'sat.alwaysUp': 'toujours visible', 'sat.noPassWindow': 'aucun passage dans la fenêtre',
'sat.tipAos': 'Lever', 'sat.tipLos': 'Coucher', 'sat.tipMaxEl': 'Culmination',
'sat.tipBelow': 'sous lhorizon', 'sat.tipNoPass': 'aucun passage dans la fenêtre de prévision',
'sat.approaching': 'se rapproche', 'sat.receding': 's’éloigne', 'sat.below': 'sous lhorizon',