diff --git a/internal/control/control_test.go b/internal/control/control_test.go
index c9c8bf3..375d9e0 100644
--- a/internal/control/control_test.go
+++ b/internal/control/control_test.go
@@ -1128,3 +1128,21 @@ func TestTelemetryUnsubscribeDuringPublishDoesNotPanic(t *testing.T) {
t.Fatal("subscriber done not closed")
}
}
+
+func TestTelemetryHubKeepsNewestMeasurement(t *testing.T) {
+ hub := NewTelemetryHub()
+ sub, unsubscribe := hub.Subscribe()
+ defer unsubscribe()
+ for i := 0; i < 8; i++ {
+ hub.PublishMeasurement(&offpkg.MeasurementSnapshot{Timestamp: time.Now(), Sequence: uint64(i + 1)})
+ }
+ var got TelemetryMessage
+ select {
+ case got = <-sub.ch:
+ case <-time.After(2 * time.Second):
+ t.Fatal("expected telemetry message")
+ }
+ if got.Seq != 8 {
+ t.Fatalf("expected newest seq 8, got %d", got.Seq)
+ }
+}
diff --git a/internal/control/ui.html b/internal/control/ui.html
index 453a09a..8658dec 100644
--- a/internal/control/ui.html
+++ b/internal/control/ui.html
@@ -319,6 +319,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
connecting
+ telemetry: --
@@ -1012,7 +1013,7 @@ function setIngField(path,val){if(!S.ingestDraft)S.ingestDraft=clone(ingFromSrv(
// ── API ────────────────────────────────────────────────────────────────────
async function api(path,opts){const r=await fetch(path,opts);const t=await r.text();if(!r.ok)throw new Error(t.trim()||`HTTP ${r.status}`);if(!t)return{};try{return JSON.parse(t);}catch{return{ok:true};}}
-function setConn(ok,label){const led=$('led-conn'),lbl=$('conn-label');led.className='led '+(ok?S.pending>0?'on-amber':'on-green':'on-red');lbl.textContent=ok?S.pending>0?'busy':label||'connected':label||'offline';}
+function setConn(ok,label){const led=$('led-conn'),lbl=$('conn-label');led.className='led '+(ok?S.pending>0?'on-amber':'on-green':'on-red');lbl.textContent=ok?S.pending>0?'busy':label||'connected':label||'offline';const tl=$('telemetry-label');if(tl){const src=S.telemetry?.source||'snapshot';const why=S.telemetry?.fallbackReason;tl.textContent=src==='ws'?'telemetry: ws':`telemetry: snapshot${why?` (${why})`:''}`;}}
async function loadConfig({silent=false}={}){try{const cfg=await api('/config');S.server.config=cfg;S.server.configOk=true;S.server.lastConfigAt=Date.now();syncIngDraft();syncCfgFromServer();syncFreqPresetIdx(cfg.fm?.frequencyMHz);setConn(true);render();if(!silent)log('Config synchronized','info');return cfg;}catch(e){S.server.configOk=false;if(!S.server.runtimeOk)setConn(false);render();if(!silent)log('Config load failed: '+e.message,'err');throw e;}}
async function loadRuntime({silent=true}={}){try{const rt=await api('/runtime');S.server.runtime=rt;S.server.runtimeOk=true;S.server.lastRuntimeAt=Date.now();const synced=syncTransitions(rt.engine);notifyTransition(rt.engine,!synced);pushHistory(rt);setConn(true);render();return rt;}catch(e){S.server.runtimeOk=false;if(!S.server.configOk)setConn(false);render();if(!silent)log('Runtime load failed: '+e.message,'err');throw e;}}
async function loadMeasurements({silent=true}={}){if(!S.telemetry.snapshotPollingActive)return S.server.measurements;try{const ms=await api('/measurements');S.server.measurements=ms;S.server.lastMeasurementsAt=Date.now();S.telemetry.source='snapshot';render();return ms;}catch(e){if(!silent)log('Measurements load failed: '+e.message,'err');throw e;}}
@@ -1208,6 +1209,7 @@ function _render(){
setText('badge-backend',cfg.backend?.kind||cfg.backend||'--');
setText('badge-mode',eng.state&&eng.state!=='idle'?'TX Active':'Control Plane');
setText('badge-live',S.server.runtimeOk?'Connected':'Waiting');
+ setConn(S.server.configOk||S.server.runtimeOk);
// ── Overview: freq display
const applied=isFinite(Number(eng.appliedFrequencyMHz))?Number(eng.appliedFrequencyMHz):(isFinite(Number(S.server.measurements?.appliedFrequencyMHz))?Number(S.server.measurements.appliedFrequencyMHz):NaN),desired=Number(cfg.fm?.frequencyMHz);