fix: one table per band instead of two

Antennas and power were two tables listing the same bands, one under the other,
so configuring a band meant matching rows by eye between them.

They are now one row per band. That the two settings are stored separately and
applied on different triggers — antennas on band change, power on band or mode
change — is a backend detail, and no reason to split what is a single decision
per band for the person configuring it. A rule in the header keeps the two
groups readable.
This commit is contained in:
2026-07-31 11:52:03 +02:00
parent 62b6fe0a3a
commit 781cf636ea
3 changed files with 84 additions and 124 deletions
+80 -120
View File
@@ -953,158 +953,124 @@ function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
);
}
// FlexBandPowerPanel — TX power per band and per class of mode.
// FlexBandPanel — everything that follows the band, in ONE row per band:
// antennas and TX power.
//
// Three classes, not one row per mode: what decides the power is duty cycle and
// what the amplifier will take of it, not the mode's name. Blank leaves the
// power alone, which is why the boxes are empty rather than showing 0.
function FlexBandPowerPanel({ bands }: { bands: string[] }) {
// They were two tables listing the same bands one under the other, which made
// the operator match rows by eye between them. The two settings are stored and
// applied separately (antennas on band change, power on band or mode change) —
// that is a backend detail, and no reason to split what is one decision per
// band for the person configuring it.
function FlexBandPanel({ bands }: { bands: string[] }) {
const { t } = useI18n();
const [map, setMap] = useState<Record<string, { phone: number; cw: number; digi: number }>>({});
const [rxList, setRxList] = useState<string[]>([]);
const [txList, setTxList] = useState<string[]>([]);
const [ant, setAnt] = useState<Record<string, { rx: string; tx: string }>>({});
const [pwr, setPwr] = useState<Record<string, { phone: number; cw: number; digi: number }>>({});
const [msg, setMsg] = useState('');
useEffect(() => { GetFlexBandPower().then((m: any) => setMap(m ?? {})).catch(() => {}); }, []);
const set = (band: string, kind: 'phone' | 'cw' | 'digi', v: string) => {
useEffect(() => {
GetFlexState().then((s: any) => {
setRxList((s?.ant_list ?? []) as string[]);
setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
}).catch(() => {});
GetFlexBandAntennas().then((m: any) => setAnt(m ?? {})).catch(() => {});
GetFlexBandPower().then((m: any) => setPwr(m ?? {})).catch(() => {});
}, []);
const saved = () => { setMsg(t('flxpw.saved')); window.setTimeout(() => setMsg(''), 1200); };
const setAntenna = (band: string, side: 'rx' | 'tx', v: string) => {
const key = band.toUpperCase();
const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0));
setMap((m) => {
const cur = m[key] ?? { phone: 0, cw: 0, digi: 0 };
const next = { ...m, [key]: { ...cur, [kind]: n } };
SaveFlexBandPower(next as any)
.then(() => { setMsg(t('flxpw.saved')); window.setTimeout(() => setMsg(''), 1200); })
.catch((e: any) => setMsg(String(e?.message ?? e)));
setAnt((m) => {
const next = { ...m, [key]: { ...(m[key] ?? { rx: '', tx: '' }), [side]: v } };
SaveFlexBandAntennas(next as any).then(saved).catch((e: any) => setMsg(String(e?.message ?? e)));
return next;
});
};
const cell = (b: string, kind: 'phone' | 'cw' | 'digi') => {
const e = map[b.toUpperCase()] ?? { phone: 0, cw: 0, digi: 0 };
const setPower = (band: string, kind: 'phone' | 'cw' | 'digi', v: string) => {
const key = band.toUpperCase();
const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0));
setPwr((m) => {
const next = { ...m, [key]: { ...(m[key] ?? { phone: 0, cw: 0, digi: 0 }), [kind]: n } };
SaveFlexBandPower(next as any).then(saved).catch((e: any) => setMsg(String(e?.message ?? e)));
return next;
});
};
const antCell = (b: string, side: 'rx' | 'tx', list: string[]) => {
const e = ant[b.toUpperCase()] ?? { rx: '', tx: '' };
return (
<td className="px-2 py-1">
<td className="px-2 py-1.5">
<select value={e[side] ?? ''} onChange={(ev) => setAntenna(b, side, ev.target.value)}
className="h-8 w-28 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
<option value=""> none </option>
{list.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
</td>
);
};
const pwrCell = (b: string, kind: 'phone' | 'cw' | 'digi') => {
const e = pwr[b.toUpperCase()] ?? { phone: 0, cw: 0, digi: 0 };
return (
<td className="px-2 py-1.5">
<Input
type="number" min={0} max={100}
className="h-7 w-20 text-sm"
className="h-8 w-16 text-xs"
value={e[kind] ? String(e[kind]) : ''}
placeholder="—"
onChange={(ev) => set(b, kind, ev.target.value)}
onChange={(ev) => setPower(b, kind, ev.target.value)}
/>
</td>
);
};
return (
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-foreground">{t('flxpw.title')}</h3>
<p className="text-xs text-muted-foreground">{t('flxpw.hint')}</p>
<h3 className="text-base font-semibold text-foreground">{t('flxb.title')}</h3>
<p className="text-xs text-muted-foreground">{t('flxb.hint')}</p>
</div>
{rxList.length === 0 && (
<div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-2xl">
{t('flxb.noRadio')}
</div>
)}
{bands.length === 0 ? (
<p className="text-xs text-muted-foreground">{t('flxpw.noBands')}</p>
) : (
<div className="rounded-md border border-border overflow-hidden max-w-xl">
<div className="rounded-md border border-border overflow-x-auto max-w-3xl">
<table className="w-full text-sm">
<thead className="bg-muted/40 text-muted-foreground text-xs">
<tr>
<th className="text-left px-3 py-1.5 font-semibold">{t('flxpw.band')}</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.phone')}</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxb.rxAnt')}</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxb.txAnt')}</th>
<th className="text-left px-2 py-1.5 font-semibold border-l border-border/60">{t('flxpw.phone')}</th>
<th className="text-left px-2 py-1.5 font-semibold">CW</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.digi')}</th>
</tr>
</thead>
<tbody>
{bands.map((b) => (
<tr key={b} className="border-t border-border/60">
<td className="px-3 py-1 font-mono">{b}</td>
{cell(b, 'phone')}
{cell(b, 'cw')}
{cell(b, 'digi')}
<tr key={b} className="border-t border-border/50">
<td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
{antCell(b, 'rx', rxList)}
{antCell(b, 'tx', txList)}
{/* The rule separates what follows the BAND from what follows
the MODE two different triggers, side by side. */}
<td className="border-l border-border/60 p-0" />
{pwrCell(b, 'phone')}
{pwrCell(b, 'cw')}
{pwrCell(b, 'digi')}
</tr>
))}
</tbody>
</table>
</div>
)}
{msg && <span className="text-xs text-muted-foreground">{msg}</span>}
</div>
);
}
// FlexBandAntennasPanel — pick the RX/TX antenna per band. Applied automatically
// when the band changes (frequency change / spot click). Antennas come live from
// the connected FlexRadio; bands come from Lists → Bands.
function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
const [rxList, setRxList] = useState<string[]>([]);
const [txList, setTxList] = useState<string[]>([]);
const [map, setMap] = useState<Record<string, { rx: string; tx: string }>>({});
const [msg, setMsg] = useState('');
useEffect(() => {
GetFlexState().then((s: any) => {
setRxList((s?.ant_list ?? []) as string[]);
setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
}).catch(() => {});
GetFlexBandAntennas().then((m: any) => setMap(m ?? {})).catch(() => {});
}, []);
const set = (band: string, side: 'rx' | 'tx', v: string) => {
const key = band.toUpperCase();
setMap((m) => {
const cur = m[key] ?? { rx: '', tx: '' };
const next = { ...m, [key]: { ...cur, [side]: v } };
SaveFlexBandAntennas(next as any)
.then(() => { setMsg('Saved'); window.setTimeout(() => setMsg(''), 1200); })
.catch((e: any) => setMsg(String(e?.message ?? e)));
return next;
});
};
return (
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-foreground">FlexRadio per-band antennas</h3>
<p className="text-xs text-muted-foreground">
Choose the RX and TX antenna for each band. They're applied automatically when the band
changes (frequency change or clicking a spot).
</p>
</div>
{rxList.length === 0 && (
<div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-xl">
No antennas reported yet make sure the FlexRadio is connected (CAT interface), then reopen this panel.
</div>
)}
{bands.length === 0 ? (
<p className="text-xs text-muted-foreground">No bands configured add them in Lists Bands.</p>
) : (
<div className="rounded-md border border-border overflow-hidden max-w-xl">
<table className="w-full text-sm">
<thead className="bg-muted/40 text-muted-foreground text-xs">
<tr>
<th className="text-left px-3 py-1.5 font-semibold">Band</th>
<th className="text-left px-3 py-1.5 font-semibold">RX antenna</th>
<th className="text-left px-3 py-1.5 font-semibold">TX antenna</th>
</tr>
</thead>
<tbody>
{bands.map((b) => {
const e = map[b.toUpperCase()] ?? { rx: '', tx: '' };
return (
<tr key={b} className="border-t border-border/50">
<td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
<td className="px-3 py-1.5">
<select value={e.rx ?? ''} onChange={(ev) => set(b, 'rx', ev.target.value)}
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
<option value=""> none </option>
{rxList.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
</td>
<td className="px-3 py-1.5">
<select value={e.tx ?? ''} onChange={(ev) => set(b, 'tx', ev.target.value)}
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
<option value=""> none </option>
{txList.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{msg && <span className="text-[11px] text-muted-foreground">{msg}</span>}
</div>
);
@@ -5664,13 +5630,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
antgenius: AntGeniusPanelSettings,
tunergenius: TunerGeniusPanelSettings,
pgxl: PGXLPanelSettings,
flex: () => (
<>
<FlexBandAntennasPanel bands={lists.bands ?? []} />
<div className="h-6" />
<FlexBandPowerPanel bands={lists.bands ?? []} />
</>
),
flex: () => <FlexBandPanel bands={lists.bands ?? []} />,
audio: AudioPanel,
};