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:
@@ -153,6 +153,16 @@ func pruneForeignTables(conn *sql.DB, role Role, label string) {
|
||||
if role != RoleLogbook || conn == nil {
|
||||
return
|
||||
}
|
||||
// Once per database, recorded like a migration.
|
||||
//
|
||||
// The check itself is a COUNT(*) per settings table — twelve round trips,
|
||||
// which is nothing locally and is paid on EVERY open of a remote MySQL
|
||||
// logbook, including every profile switch. There is nothing to find after the
|
||||
// first pass: the role filter means no settings table is ever created in a
|
||||
// logbook again.
|
||||
if _, err := conn.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, prunedMarker); err != nil {
|
||||
return // already recorded (primary key), or the table is not writable
|
||||
}
|
||||
dropped := 0
|
||||
for _, t := range settingsTables {
|
||||
var n int
|
||||
@@ -175,6 +185,12 @@ func pruneForeignTables(conn *sql.DB, role Role, label string) {
|
||||
}
|
||||
}
|
||||
|
||||
// prunedMarker records, in schema_migrations, that a logbook has had its unused
|
||||
// settings tables removed. Named like a migration and stored beside them because
|
||||
// that is exactly what it is: a one-off schema step, and the applied set is
|
||||
// already read in a single query at every open.
|
||||
const prunedMarker = "_opslog_pruned_settings_tables"
|
||||
|
||||
// quoteIdent quotes one of OUR OWN table names for either dialect. Backticks
|
||||
// work in MySQL and in SQLite alike, which is why the migrations use them.
|
||||
func quoteIdent(s string) string { return "`" + s + "`" }
|
||||
|
||||
Reference in New Issue
Block a user