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
|
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)
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
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 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>>;
|
||||||
|
|||||||
@@ -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']();
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-18
@@ -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,17 +53,14 @@ 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 {
|
|
||||||
case <-a.ctx.Done():
|
|
||||||
a.clearLiveStatus()
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
a.publishLiveStatus()
|
a.publishLiveStatus()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// liveStatusActive reports whether publishing should run (MySQL logbook + on).
|
// 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,
|
// 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
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user