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:
+24
-1
@@ -891,6 +891,13 @@ export default function App() {
|
||||
QSOAudioPlayOnAir().catch((e: any) => setError(String(e?.message ?? e)));
|
||||
};
|
||||
const refreshManualRecReady = () => { QSOAudioManualReady().then(setManualRecReady).catch(() => {}); };
|
||||
// Asked at mount, and asked AGAIN once the backend is really up (see the
|
||||
// initial grid load) and on every profile change. The audio devices are a
|
||||
// per-profile setting, so this question cannot be answered before the active
|
||||
// profile is known — and a "no" then was final, which is why the record button
|
||||
// went missing for a whole session after an update, when startup does more
|
||||
// work and the first ask loses the race. Opening and closing the audio panel
|
||||
// was the only way back.
|
||||
useEffect(() => { refreshManualRecReady(); }, []);
|
||||
const startManualRecording = () => {
|
||||
QSOAudioManualStart().then((active) => {
|
||||
@@ -1104,6 +1111,17 @@ export default function App() {
|
||||
};
|
||||
// QSL Manager is a closable tab opened on demand from Tools → QSL Manager.
|
||||
const [qslTabOpen, setQslTabOpen] = useState(false);
|
||||
// A callsign sent to the QSL Manager's Paper QSL view from elsewhere (the
|
||||
// awards grid). The counter makes two clicks on the same callsign two
|
||||
// requests — the panel is force-mounted and would otherwise see no change.
|
||||
const [qslPaperReq, setQslPaperReq] = useState<{ call: string; n: number } | undefined>(undefined);
|
||||
function openPaperQSLFor(call: string) {
|
||||
const c = call.trim().toUpperCase();
|
||||
if (!c) return;
|
||||
setQslTabOpen(true);
|
||||
setActiveTab('qsl');
|
||||
setQslPaperReq((r) => ({ call: c, n: (r?.n ?? 0) + 1 }));
|
||||
}
|
||||
const [qslDesignerOpen, setQslDesignerOpen] = useState(false);
|
||||
const [eqslQsoId, setEqslQsoId] = useState<number | null>(null); // QSO being sent as eQSL
|
||||
function closeQslTab() {
|
||||
@@ -3087,6 +3105,7 @@ export default function App() {
|
||||
// Same race, same fix: the toolbar options were read once at mount,
|
||||
// possibly before the profile was active, and a "no" then was final.
|
||||
refreshChaseNew();
|
||||
refreshManualRecReady();
|
||||
} else if (!ok && alive && tries++ < 360) {
|
||||
// Quick retries at first (normal startup connects in ~2 s); then keep
|
||||
// trying for several minutes, because the very first migration against a
|
||||
@@ -3641,6 +3660,8 @@ export default function App() {
|
||||
setGridPrefsProfile(id ?? null);
|
||||
setActiveProfileId(typeof id === 'number' ? id : null);
|
||||
loadStation(); loadLists(); loadCATCfg(); reloadWk(); loadMainPanes(); loadProfileList();
|
||||
// The sound devices are per profile: one may record and the next not.
|
||||
refreshManualRecReady();
|
||||
// The chat is per shared logbook — clear the previous profile's messages
|
||||
// and reload for the new logbook (or hide if it isn't a MySQL log).
|
||||
setChatMsgs([]); chatSeen.current.clear(); setChatOnline([]); setChatUnread(0);
|
||||
@@ -7688,6 +7709,7 @@ export default function App() {
|
||||
<TabsContent value="qsl" forceMount className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||
<QSLManagerPanel
|
||||
onEditQSO={openEdit}
|
||||
paperRequest={qslPaperReq}
|
||||
// The same row actions the Recent QSOs grid offers. Only the
|
||||
// selection-based exports: exporting "the filter" would mean
|
||||
// the Recent-QSOs filter, not the rows shown here.
|
||||
@@ -7793,7 +7815,8 @@ export default function App() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="awards" className="flex-1 min-h-0 p-0">
|
||||
<AwardsPanel onEditQSO={openEdit} onAwardsChanged={() => setAwardsVersion((v) => v + 1)} />
|
||||
<AwardsPanel onEditQSO={openEdit} onAwardsChanged={() => setAwardsVersion((v) => v + 1)}
|
||||
onPaperQSL={openPaperQSLFor} />
|
||||
</TabsContent>
|
||||
|
||||
{statsTabOpen && (
|
||||
|
||||
Reference in New Issue
Block a user