feat: Ultrabeam over USB, Paper QSL from the awards grid, per-role schemas
Ultrabeam on a serial port never worked, and three faults were stacked so
each hid the next:
- Stop() did not wait for the poll loop, so a stopped client kept the COM
port. Every later client then failed with "Serial port busy" — the
program holding it being OpsLog itself.
- startUltrabeam tore the old client down CONCURRENTLY with starting the
new one, and "Test connection" built a second client on a port already
ours. Harmless over TCP, fatal on a port with one owner.
- A silent serial port returns (0, nil) and bufio retries that a hundred
times: a 4 s timeout became ~7 minutes of a frozen poll loop logging
nothing.
The controller then answered at once. Confirmed on hardware: the USB cable
presents TWO COM ports, only the second reaches the controller, and only at
19200 baud — so the speed is pinned in code (an FTDI cable opens at any
speed, and a wrong one is indistinguishable from a dead controller) and the
port field says which one to pick. The first exchange after each connect is
hex-dumped, which separates silence from a wrong baud from a misread frame.
Databases now carry only the tables their role needs. Every target used to
get the whole migration set, so a shared MySQL logbook grew settings and
station_profiles tables nothing ever wrote to — an operator inspecting the
server could not tell which copy was authoritative. Statements are filtered
by role, unknown tables are kept in both (fail-safe), and existing databases
are cleaned once, dropping only EMPTY tables. Settings → Database gains a
Compact button, since SQLite frees pages inside the file and never shrinks it.
Also:
- Awards: the callsigns behind a cell open the QSL Manager on Paper QSL,
searched, ready for the card dates.
- The record button no longer goes missing after an update: whether manual
recording is possible is a per-profile question that was asked once, at
startup, before the profile was known.
- Alert rules and filter presets confirm that they were saved.
- Spot clicks on the radio panadapter carry the POTA park into F3.
- The build gate is re-checked wherever the active callsign can change; it
ran at startup alone, and a fresh install has no callsign then.
This commit is contained in:
@@ -134,9 +134,14 @@ export type GridActions = {
|
||||
onDelete?: (ids: number[]) => void;
|
||||
};
|
||||
|
||||
export function QSLManagerPanel({ onEditQSO, actions }: {
|
||||
export function QSLManagerPanel({ onEditQSO, actions, paperRequest }: {
|
||||
onEditQSO?: (id: number) => void;
|
||||
actions?: GridActions;
|
||||
// A callsign to open the Paper QSL view on, sent from elsewhere in the app
|
||||
// (the awards grid). Carries a counter rather than being a bare string: the
|
||||
// panel is force-mounted and keeps its state, so asking twice for the SAME
|
||||
// callsign has to be two requests, not one unchanged prop.
|
||||
paperRequest?: { call: string; n: number };
|
||||
} = {}) {
|
||||
const { t } = useI18n();
|
||||
const [service, setService] = useState('lotw');
|
||||
@@ -198,6 +203,27 @@ export function QSLManagerPanel({ onEditQSO, actions }: {
|
||||
}, [paperCall]);
|
||||
|
||||
|
||||
// Honour an incoming request: switch to Paper QSL, fill the callsign, search.
|
||||
// The search runs from the effect below once paperCall has actually changed —
|
||||
// searchPaper closes over paperCall, so calling it here would search the
|
||||
// PREVIOUS callsign.
|
||||
// The pending request's callsign, so the search fires for THAT callsign and
|
||||
// for nothing else: the effect below also wakes on every keystroke in the
|
||||
// field, and searching on each of them would query the log letter by letter.
|
||||
const pendingPaperCall = useRef('');
|
||||
useEffect(() => {
|
||||
const call = paperRequest?.call?.trim().toUpperCase();
|
||||
if (!call) return;
|
||||
setService('paper');
|
||||
setPaperCall(call);
|
||||
pendingPaperCall.current = call;
|
||||
}, [paperRequest?.call, paperRequest?.n]);
|
||||
useEffect(() => {
|
||||
if (!pendingPaperCall.current || paperCall !== pendingPaperCall.current) return;
|
||||
pendingPaperCall.current = '';
|
||||
searchPaper();
|
||||
}, [paperCall, searchPaper]);
|
||||
|
||||
async function applyPaper() {
|
||||
const ids = paperRows.filter((r) => paperSel.has(r.id)).map((r) => r.id);
|
||||
if (ids.length === 0) return;
|
||||
|
||||
Reference in New Issue
Block a user