package main import ( "fmt" "strings" "time" "hamlog/internal/applog" ) // Live operator status — for multi-operator events on a SHARED MySQL logbook // (e.g. a special-event call like TM74FR with several ops on different bands). // Each OpsLog instance heartbeats its current activity (operator call + station // call + freq/band/mode from CAT) into a `live_status` table every ~15s. A tiny // web script on the operator's own server reads that table and renders a live // page/image that the QRZ.com bio can embed (``). OpsLog only WRITES // to the DB — it is not a web server. Rows older than a couple of minutes are // "stale" (operator went offline); the web side ignores them. const keyLiveStatusEnabled = "livestatus.enabled" // GetLiveStatusEnabled reports whether this operator publishes live status. func (a *App) GetLiveStatusEnabled() bool { if a.settings == nil { return false } v, _ := a.settings.Get(a.ctx, keyLiveStatusEnabled) return strings.TrimSpace(v) == "1" } // SetLiveStatusEnabled turns live-status publishing on or off (off also removes // this operator's row immediately). func (a *App) SetLiveStatusEnabled(on bool) error { if a.settings == nil { return fmt.Errorf("db not initialized") } val := "0" if on { val = "1" } if err := a.settings.Set(a.ctx, keyLiveStatusEnabled, val); err != nil { return err } if on { go a.publishLiveStatus() // show up right away } else { a.clearLiveStatus() } return nil } // liveStatusLoop heartbeats the current activity while enabled. Started once at // startup; cheap no-op when disabled or not on MySQL. func (a *App) liveStatusLoop() { t := time.NewTicker(15 * time.Second) defer t.Stop() for { select { case <-a.ctx.Done(): a.clearLiveStatus() return case <-t.C: a.publishLiveStatus() } } } // liveStatusActive reports whether publishing should run (MySQL logbook + on). func (a *App) liveStatusActive() bool { return a.logDb != nil && a.dbBackend == "mysql" && a.GetLiveStatusEnabled() } // liveStatusOperator returns this instance's operator id (the operator callsign, // falling back to the station callsign for a single-op setup). func (a *App) liveStatusOperator() (op, station string) { if a.settings == 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)) if op == "" { op = station } return op, station } // publishLiveStatus upserts this operator's current activity. Best effort. func (a *App) publishLiveStatus() { if !a.liveStatusActive() { return } op, station := a.liveStatusOperator() if op == "" { return } var freqHz int64 var band, mode string if a.cat != nil { st := a.cat.State() if st.Connected { freqHz, band, mode = st.FreqHz, st.Band, st.Mode } } if err := a.ensureLiveStatusTable(); err != nil { applog.Printf("livestatus: ensure table: %v", err) return } _, err := a.logDb.ExecContext(a.ctx, "INSERT INTO live_status (operator, station, freq_hz, band, mode, updated_at) "+ "VALUES (?, ?, ?, ?, ?, UTC_TIMESTAMP()) "+ "ON DUPLICATE KEY UPDATE station=VALUES(station), freq_hz=VALUES(freq_hz), "+ "band=VALUES(band), mode=VALUES(mode), updated_at=UTC_TIMESTAMP()", op, station, freqHz, band, mode) if err != nil { applog.Printf("livestatus: publish: %v", err) } } func (a *App) ensureLiveStatusTable() error { _, err := a.logDb.ExecContext(a.ctx, "CREATE TABLE IF NOT EXISTS live_status ("+ "operator VARCHAR(32) PRIMARY KEY, "+ "station VARCHAR(32), "+ "freq_hz BIGINT, "+ "band VARCHAR(16), "+ "mode VARCHAR(16), "+ "updated_at DATETIME)") return err } // clearLiveStatus removes this operator's row (on disable / shutdown). func (a *App) clearLiveStatus() { if a.logDb == nil || a.dbBackend != "mysql" { return } op, _ := a.liveStatusOperator() if op == "" { return } _, _ = a.logDb.ExecContext(a.ctx, "DELETE FROM live_status WHERE operator=?", op) }