Bläddra i källkod

docs: describe WS-1 telemetry and tighten UI measurement source

main
Jan 4 veckor sedan
förälder
incheckning
d2d80bd0d5
2 ändrade filer med 56 tillägg och 4 borttagningar
  1. +51
    -0
      docs/API.md
  2. +5
    -4
      internal/control/ui.html

+ 51
- 0
docs/API.md Visa fil

@@ -234,6 +234,57 @@ Notes:
- `clipperLookaheadGain` and `clipperEnvelope` are preferred raw diagnostics; `clipperOrProtectionActive` is only a derived convenience indicator.
- `licenseInjectionActive` means chunk-local actual activity, not merely feature presence.
- `overNominalEvents` / `overHeadroomEvents` are internal normalized composite envelope counters, not legal overmodulation verdicts.
- `GET /measurements` remains the canonical snapshot/fallback API for live metering.
- `noData` / `stale` are wrapper-level snapshot semantics and must not be inferred solely from the existence of a measurement object.

### `GET /ws/telemetry`

WebSocket endpoint for **live metering telemetry**.

WS-1 scope is intentionally small:
- server → client only
- only `measurement` messages
- no topics
- no bundles
- no runtime multiplexing

`GET /measurements` stays the canonical snapshot/fallback API. The WebSocket path carries the same latest measurement truth, but in a live transport envelope.

**Message shape:**
```json
{
"type": "measurement",
"ts": "2026-04-13T07:00:53.842Z",
"seq": 128,
"data": {
"timestamp": "2026-04-13T07:00:53.842Z",
"sampleRateHz": 228000,
"chunkSamples": 11400,
"chunkDurationMs": 50,
"sequence": 128,
"flags": {
"stereoEnabled": true,
"stereoMode": "DSB"
},
"lrPreEncodePostWatermark": {
"lRms": 0.27,
"rRms": 0.27,
"lPeakAbs": 0.51,
"rPeakAbs": 0.51
},
"compositeFinalPreIq": {
"peakAbs": 0.63,
"pilotInjectionEquivalentPercent": 9.0
}
}
}
```

Notes:
- `data` is intended to be semantically identical to the `measurement` object returned by `GET /measurements`.
- Transport metadata such as `type`, `ts`, and `seq` exists only in the WS envelope.
- On connect, the server may immediately send the latest known measurement snapshot so the client does not have to wait for the next natural update.
- Slow clients are best-effort only; the server may drop stale telemetry frames and keep the newest state.

---



+ 5
- 4
internal/control/ui.html Visa fil

@@ -1032,7 +1032,7 @@ function normState(s){return(typeof s==='string'?s.trim().toLowerCase():'')||'id
function stateSev(s){switch(normState(s)){case 'running':return'ok';case 'degraded':case 'muted':return'warn';case 'faulted':return'err';default:return'info';}}
function stateClass(s){switch(normState(s)){case 'faulted':return'err';case 'muted':case 'degraded':case 'prebuffering':case 'arming':case 'stopping':case 'idle':return'warn';default:return'good';}}
function flowSeverityFromRuntime(){const rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},src=ing.source||{},ir=ing.runtime||{};if(normState(eng.state)==='faulted')return{sev:'err',title:'Fault',text:eng.lastError||eng.runtimeAlert||'TX faulted'};if(eng.runtimeAlert)return{sev:'warn',title:'Runtime warning',text:String(eng.runtimeAlert)};if(ir.writeBlocked)return{sev:'err',title:'Ingest blocked',text:'Ingest runtime is write-blocked'};if(src.state&&String(src.state).toLowerCase()==='reconnecting')return{sev:'warn',title:'Source reconnecting',text:'Input source is reconnecting'};return null;}
function flowNodeData(){const cfg=S.server.config||{},rt=S.server.runtime||{},eng=rt.engine||{},measWrap=S.server.measurements||{},meas=measWrap.measurement||eng.measurement||{},ing=rt.ingest||{},src=ing.source||{},ir=ing.runtime||{},active=ing.active||{},rds=cfg.rds||{},fm=cfg.fm||{},audio=cfg.audio||{};const txState=normState(eng.state||measWrap.state);const queueHealth=String(eng.queue?.health||measWrap.queue?.health||'').toLowerCase();const sourceKind=String(active.kind||cfg.ingest?.kind||'none');const sourceState=String(src.state||'').toLowerCase();const sourceHost=(()=>{const ep=String(active.origin?.endpoint||'');try{return ep?new URL(ep).host:active.origin?.streamName||'--';}catch{return active.origin?.streamName||ep||ep||'--';}})();
function flowNodeData(){const cfg=S.server.config||{},rt=S.server.runtime||{},eng=rt.engine||{},measWrap=S.server.measurements||{},meas=currentMeasurement(),ing=rt.ingest||{},src=ing.source||{},ir=ing.runtime||{},active=ing.active||{},rds=cfg.rds||{},fm=cfg.fm||{},audio=cfg.audio||{};const txState=normState(eng.state||measWrap.state);const queueHealth=String(eng.queue?.health||measWrap.queue?.health||'').toLowerCase();const sourceKind=String(active.kind||cfg.ingest?.kind||'none');const sourceState=String(src.state||'').toLowerCase();const sourceHost=(()=>{const ep=String(active.origin?.endpoint||'');try{return ep?new URL(ep).host:active.origin?.streamName||'--';}catch{return active.origin?.streamName||ep||ep||'--';}})();
return {
source:{state:sourceKind==='none'?'idle':(sourceState==='reconnecting'?'warn':(src.connected===false?'err':'good')),sub:sourceKind.toUpperCase(),detail:(sourceKind==='none'?'No source':(src.connected===false?'Disconnected':'Connected')),lines:[`Kind: ${sourceKind}`,`Origin: ${active.origin?.endpoint||active.origin?.streamName||'--'}`,`State: ${src.state||'idle'}`,`Reconnects: ${src.reconnects??0}`],applyMode:'reload'},
ingest:{state:ir.writeBlocked?'err':(String(ir.state||'').toLowerCase()==='degraded'||sourceState==='reconnecting'?'warn':(sourceKind==='none'?'idle':'good')),sub:(ir.state||src.state||'idle').toUpperCase(),detail:joinParts([isFinite(Number(src.bufferedSeconds))?`${Number(src.bufferedSeconds).toFixed(1)} s buffer`:'',ir.prebuffering?'prebuffer':'']).trim()||'Stable',lines:[`Runtime: ${ir.state||'--'}`,`Buffered: ${isFinite(Number(src.bufferedSeconds))?Number(src.bufferedSeconds).toFixed(2)+'s':'--'}`,`Last chunk: ${ageFrom(ir.lastChunkAt||src.lastChunkAt)}`,`Write blocked: ${ir.writeBlocked?'yes':'no'}`],applyMode:'reload'},
@@ -1087,6 +1087,7 @@ function fmtPTY(v){const n=Number(v);return(n>=0&&n<=31)?`${n} — ${PTY_NAMES[n
function ageStr(ts){if(!ts)return'--';const d=Math.max(0,Math.floor((Date.now()-ts)/1000));if(d<2)return'just now';if(d<60)return d+'s ago';const m=Math.floor(d/60);if(m<60)return m+'m ago';return Math.floor(m/60)+'h ago';}
function ageFrom(v){if(!v)return'--';if(typeof v==='number')return ageStr(v);const ts=Date.parse(String(v));return isNaN(ts)?'--':ageStr(ts);}
function joinParts(ps){return ps.filter(p=>String(p||'').trim()!=='').join(' · ');}
function currentMeasurement(){const wrap=S.server.measurements||{};if(wrap.measurement)return wrap.measurement;const legacy=S.server.runtime?.engine?.measurement;return legacy||{};}

// ── DOM helpers ────────────────────────────────────────────────────────────
function setText(id,text){const el=$(id);if(el){const s=text==null?'--':String(text);if(el.textContent!==s)el.textContent=s;}}
@@ -1162,7 +1163,7 @@ function render(){try{_render();}catch(e){console.error('[render]',e);}}
function renderMetersOnly(){try{renderMetersSection();}catch(e){console.error('[renderMetersOnly]',e);}}

function renderMetersSection(){
const rt=S.server.runtime||{},eng=rt.engine||{},drv=rt.driver||{},measWrap=S.server.measurements||{},meas=measWrap.measurement||eng.measurement||{};
const rt=S.server.runtime||{},eng=rt.engine||{},drv=rt.driver||{},measWrap=S.server.measurements||{},meas=currentMeasurement();
const txSt=normState(eng.state);
const urN=Number(eng.underruns??drv.underruns??0);
const txR=txSt==='running'?1:S.txBusy?.55:.08;
@@ -1186,7 +1187,7 @@ function renderMetersSection(){
}

function renderFlowLiveBits(){
const rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},measWrap=S.server.measurements||{},meas=measWrap.measurement||eng.measurement||{};
const rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},measWrap=S.server.measurements||{},meas=currentMeasurement();
const masterState=String((eng.state||'idle')).toUpperCase();
const issue=flowSeverityFromRuntime();
const runtimeAge=Number(eng.runtimeStateDurationSeconds);
@@ -1201,7 +1202,7 @@ function renderFlowLiveBits(){
}

function _render(){
const cfg=S.server.config||{},rt=S.server.runtime||{},eng=rt.engine||{},drv=rt.driver||{},aud=rt.audioStream||null,ing=rt.ingest||{},ingRt=ing.runtime||{},measWrap=S.server.measurements||{},meas=measWrap.measurement||eng.measurement||{};
const cfg=S.server.config||{},rt=S.server.runtime||{},eng=rt.engine||{},drv=rt.driver||{},aud=rt.audioStream||null,ing=rt.ingest||{},ingRt=ing.runtime||{},measWrap=S.server.measurements||{},meas=currentMeasurement();

// ── Header
setText('badge-backend',cfg.backend?.kind||cfg.backend||'--');


Laddar…
Avbryt
Spara