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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 l’horizon', 'sat.tipNoPass': 'aucun passage dans la fenêtre de prévision',
|
||||
'sat.approaching': 'se rapproche', 'sat.receding': 's’éloigne', 'sat.below': 'sous l’horizon',
|
||||
|
||||
Reference in New Issue
Block a user