chore: release v0.11.2
This commit is contained in:
@@ -379,6 +379,10 @@ type App struct {
|
||||
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
|
||||
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
|
||||
awardSnap []qso.QSO // light-scanned + enriched logbook snapshot reused across award computations
|
||||
awardSnapRev string // logbook revision the snapshot was built at ("" = none)
|
||||
|
||||
@@ -22,22 +22,29 @@ $DB_USER = 'opslog';
|
||||
$DB_PASS = 'CHANGE_ME';
|
||||
$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);
|
||||
if ($mysqli->connect_errno) {
|
||||
http_response_code(500);
|
||||
exit('DB error');
|
||||
}
|
||||
|
||||
// The table is created by OpsLog on first publish; tolerate it not existing yet.
|
||||
$rows = [];
|
||||
$sql = "SELECT operator, station, freq_hz, band, mode, updated_at
|
||||
FROM live_status
|
||||
WHERE updated_at >= UTC_TIMESTAMP() - INTERVAL ? SECOND
|
||||
ORDER BY band, freq_hz";
|
||||
if ($stmt = $mysqli->prepare($sql)) {
|
||||
if ($stmt = @$mysqli->prepare($sql)) {
|
||||
$stmt->bind_param('i', $STALE_SECONDS);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
@$stmt->execute();
|
||||
if ($res = $stmt->get_result()) {
|
||||
while ($r = $res->fetch_assoc()) $rows[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
$station = $rows ? $rows[0]['station'] : 'OpsLog';
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart,
|
||||
GetAwardDefs,
|
||||
GetUIPref,
|
||||
ReportLiveActivity,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs } from '@/lib/awardRefs';
|
||||
@@ -678,6 +679,12 @@ export default function App() {
|
||||
setMainPaneRight(valid(r) ? r : 'map2');
|
||||
}, []);
|
||||
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
|
||||
// Main-view cluster pane (portable UI pref). Hiding it keeps the filters
|
||||
// active, it just reclaims the width.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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).
|
||||
export const APP_VERSION = '0.11.1';
|
||||
export const APP_VERSION = '0.11.2';
|
||||
|
||||
// Author / credits, shown in Help -> About.
|
||||
export const APP_AUTHOR = 'F4BPO';
|
||||
|
||||
Vendored
+2
@@ -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 ReportLiveActivity(arg1:number,arg2:string,arg3:string):Promise<void>;
|
||||
|
||||
export function RescanAwards():Promise<void>;
|
||||
|
||||
export function ResetAwardDefs():Promise<Array<award.Def>>;
|
||||
|
||||
@@ -706,6 +706,10 @@ export function 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() {
|
||||
return window['go']['main']['App']['RescanAwards']();
|
||||
}
|
||||
|
||||
+50
-18
@@ -42,6 +42,7 @@ func (a *App) SetLiveStatusEnabled(on bool) error {
|
||||
return err
|
||||
}
|
||||
if on {
|
||||
applog.Printf("livestatus: enabled (logbook backend=%q, mysql conn=%v)", a.dbBackend, a.logDb != nil)
|
||||
go a.publishLiveStatus() // show up right away
|
||||
} else {
|
||||
a.clearLiveStatus()
|
||||
@@ -52,17 +53,14 @@ func (a *App) SetLiveStatusEnabled(on bool) error {
|
||||
// liveStatusLoop heartbeats the current activity while enabled. Started once at
|
||||
// startup; cheap no-op when disabled or not on MySQL.
|
||||
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)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
a.clearLiveStatus()
|
||||
return
|
||||
case <-t.C:
|
||||
for range t.C {
|
||||
a.publishLiveStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// liveStatusActive reports whether publishing should run (MySQL logbook + on).
|
||||
@@ -71,28 +69,47 @@ func (a *App) liveStatusActive() bool {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if a.settings == nil {
|
||||
if a.profiles == nil {
|
||||
return "", ""
|
||||
}
|
||||
o, _ := a.settings.Get(a.ctx, keyStationOperator)
|
||||
s, _ := a.settings.Get(a.ctx, keyStationCallsign)
|
||||
op = strings.ToUpper(strings.TrimSpace(o))
|
||||
station = strings.ToUpper(strings.TrimSpace(s))
|
||||
p, err := a.profiles.Active(a.ctx)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
station = strings.ToUpper(strings.TrimSpace(p.Callsign))
|
||||
op = strings.ToUpper(strings.TrimSpace(p.Operator))
|
||||
if op == "" {
|
||||
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() {
|
||||
if !a.liveStatusActive() {
|
||||
return
|
||||
if a.logDb == nil || a.dbBackend != "mysql" {
|
||||
return // not a MySQL logbook — nothing to do (silent, runs every 15s)
|
||||
}
|
||||
if !a.GetLiveStatusEnabled() {
|
||||
return // disabled (silent)
|
||||
}
|
||||
op, station := a.liveStatusOperator()
|
||||
if op == "" {
|
||||
applog.Printf("livestatus: nothing published — no operator/callsign set (Settings → Station)")
|
||||
return
|
||||
}
|
||||
var freqHz int64
|
||||
@@ -103,8 +120,21 @@ func (a *App) publishLiveStatus() {
|
||||
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 {
|
||||
applog.Printf("livestatus: ensure table: %v", err)
|
||||
applog.Printf("livestatus: CREATE TABLE failed: %v", err)
|
||||
return
|
||||
}
|
||||
_, err := a.logDb.ExecContext(a.ctx,
|
||||
@@ -114,8 +144,10 @@ func (a *App) publishLiveStatus() {
|
||||
"band=VALUES(band), mode=VALUES(mode), updated_at=UTC_TIMESTAMP()",
|
||||
op, station, freqHz, band, mode)
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
// 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
|
||||
// to https://us.i.posthog.com for a US project.
|
||||
|
||||
Reference in New Issue
Block a user