From 9e4f43f6483f35b569a761a76ef5a5755c1a5476 Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 20 Jul 2026 09:55:14 +0200 Subject: [PATCH] fix: offline operators removed from live_status + award-column visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live status: when an operator goes offline (no QSO in the window) OpsLog now DELETEs its live_status row instead of just flipping online=0, so a status page that lists present/recent rows shows them as gone without having to read the online column — the whole point of the feature. The row reappears on the next QSO. Recent QSOs columns: toggling an award column (e.g. DDFM) rebuilt columnDefs and made AG Grid re-apply every colDef hide default, so hidden non-award columns (QTH/Grid) came back and restoringRef stayed stuck true (saving off). The restore-saved-state effect now also runs on awardShown changes, so the other columns keep the user's visibility. --- frontend/src/components/RecentQSOsGrid.tsx | 14 +++++----- livestatus.go | 30 ++++++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index 1ca19fe..fd7c3d8 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -377,11 +377,13 @@ export function RecentQSOsGrid({ rows, selectAllSignal, storageKey, onRowDoubleC if (state) saveState(colStateKey, stripAwardCols(state)); }, []); - // The award columns load asynchronously; when they arrive (or change) the - // columnDefs memo is rebuilt and AG Grid re-applies each colDef's `hide` - // default — wiping the user's saved visibility (award columns reappear, - // manually-shown ones like LoTW sent vanish). Re-apply the saved state after - // every rebuild so the user's choices win. No-op before the grid is ready. + // columnDefs is rebuilt whenever the award columns load OR the user toggles an + // award column (both change the memo → restoringRef flips true at line 316). Each + // rebuild makes AG Grid re-apply every colDef's `hide` default, wiping the user's + // saved visibility of the NON-award columns (QTH/Grid reappear, a manually-shown + // LoTW-sent vanishes). Re-apply the saved (award-stripped) state after EVERY such + // rebuild — hence awardShown in the deps, not just awardCols; without it, toggling + // an award reset the other columns AND left restoringRef stuck true (saving off). useEffect(() => { const api = gridRef.current?.api; const local = loadLocal(colStateKey); @@ -389,7 +391,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, storageKey, onRowDoubleC // Re-enable saving once AG Grid has settled the column events from the rebuild. const t = window.setTimeout(() => { restoringRef.current = false; }, 0); return () => window.clearTimeout(t); - }, [awardCols]); + }, [awardCols, awardShown]); function handleRowDoubleClicked(e: RowDoubleClickedEvent) { if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data); diff --git a/livestatus.go b/livestatus.go index 1958df6..ffe04df 100644 --- a/livestatus.go +++ b/livestatus.go @@ -202,33 +202,37 @@ func (a *App) publishLiveStatus() { } a.liveActMu.Unlock() lastQSO := a.liveLastQSOTime() // authoritative (in-memory OR shared DB) - // Online = a new contact was logged within the window. An operator who leaves - // the log open but stops working shows offline after `liveOnlineWindow`; the - // next QSO flips them back on. never-logged (zero time) → offline. - online := 0 - var lastQSOArg any - if !lastQSO.IsZero() { - lastQSOArg = lastQSO.UTC() - if time.Since(lastQSO) < liveOnlineWindow { - online = 1 - } - } + // On air = a new contact was logged within the window. An operator who leaves + // the log open but stops working goes offline after `liveOnlineWindow`; the next + // QSO puts them back on. never-logged (zero time) → offline. + online := !lastQSO.IsZero() && time.Since(lastQSO) < liveOnlineWindow if err := a.ensureLiveStatusTable(); err != nil { applog.Printf("livestatus: CREATE TABLE failed: %v", err) return } + // Offline → REMOVE the row entirely, not just flip a flag: a status page that + // lists the present rows (the common case, keyed on updated_at) then shows the + // operator as gone without having to read the online column. The row reappears + // on the next QSO. This is the whole point — no one shows on air when they're not. + if !online { + if _, err := a.logDb.ExecContext(a.ctx, "DELETE FROM live_status WHERE operator=?", op); err != nil { + applog.Printf("livestatus: offline DELETE failed: %v", err) + } + return + } + lastQSOArg := lastQSO.UTC() _, err := a.logDb.ExecContext(a.ctx, "INSERT INTO live_status (operator, station, freq_hz, band, mode, online, version, last_qso_at, updated_at) "+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP()) "+ "ON DUPLICATE KEY UPDATE station=VALUES(station), freq_hz=VALUES(freq_hz), "+ "band=VALUES(band), mode=VALUES(mode), online=VALUES(online), version=VALUES(version), "+ "last_qso_at=VALUES(last_qso_at), updated_at=UTC_TIMESTAMP()", - op, station, freqHz, band, mode, online, appVersion, lastQSOArg) + op, station, freqHz, band, mode, 1, appVersion, lastQSOArg) if err != nil { applog.Printf("livestatus: INSERT failed: %v", err) return } - applog.Printf("livestatus: published op=%s station=%s %dHz %s %s online=%d", op, station, freqHz, band, mode, online) + applog.Printf("livestatus: published op=%s station=%s %dHz %s %s ON AIR", op, station, freqHz, band, mode) } // LiveStation is one operator's live status for the multi-op "who's on air" widget.