chore: release v0.11.2

This commit is contained in:
2026-06-17 23:15:12 +02:00
parent 8b1609f5ce
commit b2a8b1946f
8 changed files with 81 additions and 25 deletions
+4
View File
@@ -379,6 +379,10 @@ type App struct {
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
catFlexSpots bool // push cluster spots to the FlexRadio panadapter catFlexSpots bool // push cluster spots to the FlexRadio panadapter
liveActMu sync.Mutex // guards the entry-strip activity reported for live status
liveFreqHz int64 // last freq/band/mode the UI reported (fallback when CAT is off)
liveBand string
liveMode string
awardSnapMu sync.Mutex // guards the award QSO snapshot awardSnapMu sync.Mutex // guards the award QSO snapshot
awardSnap []qso.QSO // light-scanned + enriched logbook snapshot reused across award computations awardSnap []qso.QSO // light-scanned + enriched logbook snapshot reused across award computations
awardSnapRev string // logbook revision the snapshot was built at ("" = none) awardSnapRev string // logbook revision the snapshot was built at ("" = none)
+11 -4
View File
@@ -22,22 +22,29 @@ $DB_USER = 'opslog';
$DB_PASS = 'CHANGE_ME'; $DB_PASS = 'CHANGE_ME';
$STALE_SECONDS = 120; // an operator is "active" if seen within this window $STALE_SECONDS = 120; // an operator is "active" if seen within this window
// PHP 8.1+ makes mysqli THROW on errors by default; turn that off so a missing
// `live_status` table (not yet created by OpsLog) just yields an empty list
// instead of a fatal "table doesn't exist".
mysqli_report(MYSQLI_REPORT_OFF);
$mysqli = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME); $mysqli = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
if ($mysqli->connect_errno) { if ($mysqli->connect_errno) {
http_response_code(500); http_response_code(500);
exit('DB error'); exit('DB error');
} }
// The table is created by OpsLog on first publish; tolerate it not existing yet.
$rows = []; $rows = [];
$sql = "SELECT operator, station, freq_hz, band, mode, updated_at $sql = "SELECT operator, station, freq_hz, band, mode, updated_at
FROM live_status FROM live_status
WHERE updated_at >= UTC_TIMESTAMP() - INTERVAL ? SECOND WHERE updated_at >= UTC_TIMESTAMP() - INTERVAL ? SECOND
ORDER BY band, freq_hz"; ORDER BY band, freq_hz";
if ($stmt = $mysqli->prepare($sql)) { if ($stmt = @$mysqli->prepare($sql)) {
$stmt->bind_param('i', $STALE_SECONDS); $stmt->bind_param('i', $STALE_SECONDS);
$stmt->execute(); @$stmt->execute();
$res = $stmt->get_result(); if ($res = $stmt->get_result()) {
while ($r = $res->fetch_assoc()) $rows[] = $r; while ($r = $res->fetch_assoc()) $rows[] = $r;
}
} }
$station = $rows ? $rows[0]['station'] : 'OpsLog'; $station = $rows ? $rows[0]['station'] : 'OpsLog';
+7
View File
@@ -32,6 +32,7 @@ import {
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart, QSOAudioBegin, QSOAudioCancel, QSOAudioRestart,
GetAwardDefs, GetAwardDefs,
GetUIPref, GetUIPref,
ReportLiveActivity,
} from '../wailsjs/go/main/App'; } from '../wailsjs/go/main/App';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { applyAwardRefs } from '@/lib/awardRefs'; import { applyAwardRefs } from '@/lib/awardRefs';
@@ -678,6 +679,12 @@ export default function App() {
setMainPaneRight(valid(r) ? r : 'map2'); setMainPaneRight(valid(r) ? r : 'map2');
}, []); }, []);
useEffect(() => { loadMainPanes(); }, [loadMainPanes]); useEffect(() => { loadMainPanes(); }, [loadMainPanes]);
// Report the current entry-strip band/mode/freq to the backend so the live
// operator status (multi-op) has band/mode/freq even when the CAT is off.
useEffect(() => {
const hz = freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0;
ReportLiveActivity(hz || 0, band || '', mode || '').catch(() => {});
}, [band, mode, freqMhz]);
// Cluster filter sidebar visibility — shared by the Cluster tab and the // Cluster filter sidebar visibility — shared by the Cluster tab and the
// Main-view cluster pane (portable UI pref). Hiding it keeps the filters // Main-view cluster pane (portable UI pref). Hiding it keeps the filters
// active, it just reclaims the width. // active, it just reclaims the width.
+1 -1
View File
@@ -1,6 +1,6 @@
// Single source of truth for the app version shown in the UI (header + About). // Single source of truth for the app version shown in the UI (header + About).
// Bump this on a release (the release script updates it alongside telemetry.go). // Bump this on a release (the release script updates it alongside telemetry.go).
export const APP_VERSION = '0.11.1'; export const APP_VERSION = '0.11.2';
// Author / credits, shown in Help -> About. // Author / credits, shown in Help -> About.
export const APP_AUTHOR = 'F4BPO'; export const APP_AUTHOR = 'F4BPO';
+2
View File
@@ -367,6 +367,8 @@ export function RenderEQSL(arg1:number,arg2:number):Promise<string>;
export function ReplaceAwardReferences(arg1:string,arg2:Array<awardref.Ref>):Promise<number>; export function ReplaceAwardReferences(arg1:string,arg2:Array<awardref.Ref>):Promise<number>;
export function ReportLiveActivity(arg1:number,arg2:string,arg3:string):Promise<void>;
export function RescanAwards():Promise<void>; export function RescanAwards():Promise<void>;
export function ResetAwardDefs():Promise<Array<award.Def>>; export function ResetAwardDefs():Promise<Array<award.Def>>;
+4
View File
@@ -706,6 +706,10 @@ export function ReplaceAwardReferences(arg1, arg2) {
return window['go']['main']['App']['ReplaceAwardReferences'](arg1, arg2); return window['go']['main']['App']['ReplaceAwardReferences'](arg1, arg2);
} }
export function ReportLiveActivity(arg1, arg2, arg3) {
return window['go']['main']['App']['ReportLiveActivity'](arg1, arg2, arg3);
}
export function RescanAwards() { export function RescanAwards() {
return window['go']['main']['App']['RescanAwards'](); return window['go']['main']['App']['RescanAwards']();
} }
+51 -19
View File
@@ -42,6 +42,7 @@ func (a *App) SetLiveStatusEnabled(on bool) error {
return err return err
} }
if on { if on {
applog.Printf("livestatus: enabled (logbook backend=%q, mysql conn=%v)", a.dbBackend, a.logDb != nil)
go a.publishLiveStatus() // show up right away go a.publishLiveStatus() // show up right away
} else { } else {
a.clearLiveStatus() a.clearLiveStatus()
@@ -52,16 +53,13 @@ func (a *App) SetLiveStatusEnabled(on bool) error {
// liveStatusLoop heartbeats the current activity while enabled. Started once at // liveStatusLoop heartbeats the current activity while enabled. Started once at
// startup; cheap no-op when disabled or not on MySQL. // startup; cheap no-op when disabled or not on MySQL.
func (a *App) liveStatusLoop() { func (a *App) liveStatusLoop() {
defer func() { _ = recover() }() // never crash the app from here
applog.Printf("livestatus: loop started")
a.publishLiveStatus() // attempt immediately, don't wait the first tick
t := time.NewTicker(15 * time.Second) t := time.NewTicker(15 * time.Second)
defer t.Stop() defer t.Stop()
for { for range t.C {
select { a.publishLiveStatus()
case <-a.ctx.Done():
a.clearLiveStatus()
return
case <-t.C:
a.publishLiveStatus()
}
} }
} }
@@ -71,28 +69,47 @@ func (a *App) liveStatusActive() bool {
} }
// liveStatusOperator returns this instance's operator id (the operator callsign, // liveStatusOperator returns this instance's operator id (the operator callsign,
// falling back to the station callsign for a single-op setup). // falling back to the station callsign for a single-op setup). The callsign and
// operator live on the ACTIVE PROFILE (station_profiles table), NOT in the
// settings KV — read them there.
func (a *App) liveStatusOperator() (op, station string) { func (a *App) liveStatusOperator() (op, station string) {
if a.settings == nil { if a.profiles == nil {
return "", "" return "", ""
} }
o, _ := a.settings.Get(a.ctx, keyStationOperator) p, err := a.profiles.Active(a.ctx)
s, _ := a.settings.Get(a.ctx, keyStationCallsign) if err != nil {
op = strings.ToUpper(strings.TrimSpace(o)) return "", ""
station = strings.ToUpper(strings.TrimSpace(s)) }
station = strings.ToUpper(strings.TrimSpace(p.Callsign))
op = strings.ToUpper(strings.TrimSpace(p.Operator))
if op == "" { if op == "" {
op = station op = station
} }
return op, station return op, station
} }
// publishLiveStatus upserts this operator's current activity. Best effort. // ReportLiveActivity is called by the UI with the current entry-strip freq/band/
// mode, used as a fallback for live status when the CAT isn't connected.
func (a *App) ReportLiveActivity(freqHz int64, band, mode string) {
a.liveActMu.Lock()
a.liveFreqHz = freqHz
a.liveBand = strings.ToUpper(strings.TrimSpace(band))
a.liveMode = strings.ToUpper(strings.TrimSpace(mode))
a.liveActMu.Unlock()
}
// publishLiveStatus upserts this operator's current activity. Best effort, with
// explicit logging so a silent no-op is diagnosable.
func (a *App) publishLiveStatus() { func (a *App) publishLiveStatus() {
if !a.liveStatusActive() { if a.logDb == nil || a.dbBackend != "mysql" {
return return // not a MySQL logbook — nothing to do (silent, runs every 15s)
}
if !a.GetLiveStatusEnabled() {
return // disabled (silent)
} }
op, station := a.liveStatusOperator() op, station := a.liveStatusOperator()
if op == "" { if op == "" {
applog.Printf("livestatus: nothing published — no operator/callsign set (Settings → Station)")
return return
} }
var freqHz int64 var freqHz int64
@@ -103,8 +120,21 @@ func (a *App) publishLiveStatus() {
freqHz, band, mode = st.FreqHz, st.Band, st.Mode freqHz, band, mode = st.FreqHz, st.Band, st.Mode
} }
} }
// Fall back to whatever the entry strip last reported (so band/mode/freq are
// published even when the CAT isn't connected).
a.liveActMu.Lock()
if freqHz == 0 {
freqHz = a.liveFreqHz
}
if band == "" {
band = a.liveBand
}
if mode == "" {
mode = a.liveMode
}
a.liveActMu.Unlock()
if err := a.ensureLiveStatusTable(); err != nil { if err := a.ensureLiveStatusTable(); err != nil {
applog.Printf("livestatus: ensure table: %v", err) applog.Printf("livestatus: CREATE TABLE failed: %v", err)
return return
} }
_, err := a.logDb.ExecContext(a.ctx, _, err := a.logDb.ExecContext(a.ctx,
@@ -114,8 +144,10 @@ func (a *App) publishLiveStatus() {
"band=VALUES(band), mode=VALUES(mode), updated_at=UTC_TIMESTAMP()", "band=VALUES(band), mode=VALUES(mode), updated_at=UTC_TIMESTAMP()",
op, station, freqHz, band, mode) op, station, freqHz, band, mode)
if err != nil { if err != nil {
applog.Printf("livestatus: publish: %v", err) applog.Printf("livestatus: INSERT failed: %v", err)
return
} }
applog.Printf("livestatus: published op=%s station=%s %dHz %s %s", op, station, freqHz, band, mode)
} }
func (a *App) ensureLiveStatusTable() error { func (a *App) ensureLiveStatusTable() error {
+1 -1
View File
@@ -21,7 +21,7 @@ import (
const ( const (
// appVersion is stamped on every heartbeat (and could feed the About box). // appVersion is stamped on every heartbeat (and could feed the About box).
appVersion = "0.11.1" appVersion = "0.11.2"
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change // posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
// to https://us.i.posthog.com for a US project. // to https://us.i.posthog.com for a US project.