瀏覽代碼

refactor: narrow live UI updates for meters and flow badges

main
Jan 1 月之前
父節點
當前提交
1220c702e9
共有 1 個檔案被更改,包括 43 行新增19 行删除
  1. +43
    -19
      internal/control/ui.html

+ 43
- 19
internal/control/ui.html 查看文件

@@ -914,6 +914,7 @@ let toastTimer=null;
const S={
server:{config:null,runtime:null,measurements:null,configOk:false,runtimeOk:false,lastConfigAt:0,lastRuntimeAt:0,lastMeasurementsAt:0},
telemetry:{ws:null,wsConnected:false,wsRetryTimer:null,snapshotPollingActive:true,source:'snapshot',lastWsMessageAt:null,fallbackReason:null,reconnectDelayMs:1500},
liveRenderScheduled:false,
lastRTState:'',draft:{},errors:{},dirty:new Set(),
fieldErrors:{},
flowSelected:null,flowHover:null,flowAnchor:null,
@@ -1015,7 +1016,7 @@ function setConn(ok,label){const led=$('led-conn'),lbl=$('conn-label');led.class
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;}}
function scheduleLiveRender(){if(S.liveRenderScheduled)return;S.liveRenderScheduled=true;requestAnimationFrame(()=>{S.liveRenderScheduled=false;renderLiveOnly();});}
function scheduleLiveRender(){if(S.liveRenderScheduled)return;S.liveRenderScheduled=true;requestAnimationFrame(()=>{S.liveRenderScheduled=false;renderMetersOnly();});}
function mergeLiveMeasurement(msg){const prev=S.server.measurements||{};S.server.measurements={...prev,measurement:msg.data};S.server.lastMeasurementsAt=Date.now();S.telemetry.lastWsMessageAt=Date.now();S.telemetry.source='ws';}
function connectTelemetryWS(){try{if(S.telemetry.ws){try{S.telemetry.ws.close();}catch{}}const proto=location.protocol==='https:'?'wss':'ws';const ws=new WebSocket(`${proto}://${location.host}/ws/telemetry`);S.telemetry.ws=ws;ws.onopen=()=>{S.telemetry.wsConnected=true;S.telemetry.snapshotPollingActive=false;S.telemetry.fallbackReason=null;S.telemetry.source='ws';S.telemetry.reconnectDelayMs=1500;render();log('Telemetry WS connected','ok');};ws.onmessage=(ev)=>{try{const msg=JSON.parse(ev.data);if(msg?.type==='measurement'&&msg.data){mergeLiveMeasurement(msg);scheduleLiveRender();}}catch(e){console.warn('telemetry ws parse',e);}};ws.onclose=()=>{S.telemetry.wsConnected=false;S.telemetry.snapshotPollingActive=true;S.telemetry.fallbackReason='ws-disconnected';S.telemetry.source='snapshot';render();if(S.telemetry.ws===ws)S.telemetry.ws=null;if(S.telemetry.wsRetryTimer)clearTimeout(S.telemetry.wsRetryTimer);const delay=S.telemetry.reconnectDelayMs||1500;S.telemetry.wsRetryTimer=setTimeout(()=>connectTelemetryWS(),delay);S.telemetry.reconnectDelayMs=Math.min(10000,Math.round(delay*1.7));};ws.onerror=()=>{try{ws.close();}catch{}};}catch(e){S.telemetry.wsConnected=false;S.telemetry.snapshotPollingActive=true;S.telemetry.fallbackReason='ws-error';S.telemetry.source='snapshot';}}
function syncCfgFromServer(){Object.keys(CFG).forEach(k=>{if(S.cfgDraft[k]===undefined)S.cfgDraft[k]=cfgSrvVal(k);});Object.keys(S.cfgDirty).forEach(s=>{S.cfgDirty[s]=Object.keys(CFG).filter(k=>CFG[k].sec===s).some(k=>S.cfgDraft[k]!==undefined&&!cfgEq(k,S.cfgDraft[k],cfgSrvVal(k)));});}
@@ -1158,7 +1159,46 @@ async function saveIngest(){if(S.ingestSaving)return;if(!S.ingestDirty){toast('N

// ── Render ─────────────────────────────────────────────────────────────────
function render(){try{_render();}catch(e){console.error('[render]',e);}}
function renderLiveOnly(){try{_render();}catch(e){console.error('[renderLiveOnly]',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 txSt=normState(eng.state);
const urN=Number(eng.underruns??drv.underruns??0);
const txR=txSt==='running'?1:S.txBusy?.55:.08;
setMeter('meter-stream-fill','meter-stream-text',urN<=0?1:Math.max(0,1-Math.min(urN,10)/10),urN===0?'Clean':`${urN} underrun${urN===1?'':'s'}`,urN===0?'good':urN<3?'warn':'err');
setMeter('meter-tx-fill','meter-tx-text',txR,txSt==='running'?'Live':S.txBusy?'Working':'Idle',txSt==='running'?'good':S.txBusy?'warn':'err');
renderFlowLiveBits();
const audioL=updateHoldMeter(S.meterState.audioL,Number(meas.lrPreEncodePostWatermark?.lRms||0),Number(meas.lrPreEncodePostWatermark?.lPeakAbs||0),{attackPerSec:7.0,releasePerSec:0.6,rmsHoldMs:1700,rmsHoldReleasePerSec:0.10,peakReleasePerSec:0.45,holdMs:1700,holdReleasePerSec:0.14,textRmsAttackPerSec:1.0,textRmsReleasePerSec:0.06,textPeakAttackPerSec:1.6,textPeakReleasePerSec:0.07,textRmsLatchMs:260,textPeakLatchMs:260,textRmsStep:0.01,textPeakStep:0.01,textRmsHysteresis:0.01,textPeakHysteresis:0.01});
const audioR=updateHoldMeter(S.meterState.audioR,Number(meas.lrPreEncodePostWatermark?.rRms||0),Number(meas.lrPreEncodePostWatermark?.rPeakAbs||0),{attackPerSec:7.0,releasePerSec:0.6,rmsHoldMs:1700,rmsHoldReleasePerSec:0.10,peakReleasePerSec:0.45,holdMs:1700,holdReleasePerSec:0.14,textRmsAttackPerSec:1.0,textRmsReleasePerSec:0.06,textPeakAttackPerSec:1.6,textPeakReleasePerSec:0.07,textRmsLatchMs:260,textPeakLatchMs:260,textRmsStep:0.01,textPeakStep:0.01,textRmsHysteresis:0.01,textPeakHysteresis:0.01});
const mpx=updateHoldMeter(S.meterState.mpx,0,Number(meas.compositeFinalPreIq?.peakAbs||0),{attackPerSec:8.0,releasePerSec:0.4,peakReleasePerSec:0.375,holdMs:2200,holdReleasePerSec:0.09,textPeakAttackPerSec:1.4,textPeakReleasePerSec:0.06,textPeakLatchMs:320,textPeakStep:0.01,textPeakHysteresis:0.01});
const audioLRmsDb=ampToDb(audioL.rms),audioRRmsDb=ampToDb(audioR.rms),audioLTextRmsDb=ampToDb(audioL.textRmsDisplay),audioRTextRmsDb=ampToDb(audioR.textRmsDisplay),audioLPeakDb=ampToDb(audioL.peak),audioRPeakDb=ampToDb(audioR.peak),audioLHoldDb=ampToDb(audioL.hold),audioRHoldDb=ampToDb(audioR.hold),audioLTextPeakDb=ampToDb(audioL.textPeakDisplay),audioRTextPeakDb=ampToDb(audioR.textPeakDisplay);
const audioMode=(db)=>(db>=-6?'err':db>=-12?'warn':'good');
const mpxMode=(v)=>(v>1.10?'err':v>1.00?'warn':'good');
renderHifiMeter('audio-l-rms-fill','audio-l-rms-marker','audio-l-rms-text',dbToRatio(audioLRmsDb),dbToRatio(ampToDb(audioL.rmsHold)),typeof meas.lrPreEncodePostWatermark?.lRms==='number'?`${audioLTextRmsDb.toFixed(1)} dB`:'--',1,audioMode(audioLRmsDb));
renderHifiMeter('audio-r-rms-fill','audio-r-rms-marker','audio-r-rms-text',dbToRatio(audioRRmsDb),dbToRatio(ampToDb(audioR.rmsHold)),typeof meas.lrPreEncodePostWatermark?.rRms==='number'?`${audioRTextRmsDb.toFixed(1)} dB`:'--',1,audioMode(audioRRmsDb));
renderHifiMeter('audio-l-peak-fill','audio-l-peak-marker','audio-l-peak-text',dbToRatio(audioLPeakDb),dbToRatio(audioLHoldDb),typeof meas.lrPreEncodePostWatermark?.lPeakAbs==='number'?`${audioLTextPeakDb.toFixed(1)} dB`:'--',1,audioMode(audioLPeakDb));
renderHifiMeter('audio-r-peak-fill','audio-r-peak-marker','audio-r-peak-text',dbToRatio(audioRPeakDb),dbToRatio(audioRHoldDb),typeof meas.lrPreEncodePostWatermark?.rPeakAbs==='number'?`${audioRTextPeakDb.toFixed(1)} dB`:'--',1,audioMode(audioRPeakDb));
renderHifiMeter('mpx-peak-fill','mpx-peak-marker','mpx-peak-text',mpx.peak,mpx.hold,typeof meas.compositeFinalPreIq?.peakAbs==='number'?fmtModPct(mpx.textPeakDisplay):'--',1.5,mpxMode(mpx.peak));
setHifiScale('Audio RMS',['−60 dB','−24 dB','−12 dB','−6 dB','0 dB']);
setHifiScale('Audio Peak',['−60 dB','−24 dB','−12 dB','−6 dB','0 dB']);
setHifiScale('MPX Peak',['0%','50%','100%','150%']);
}

function renderFlowLiveBits(){
const rt=S.server.runtime||{},eng=rt.engine||{},ing=rt.ingest||{},measWrap=S.server.measurements||{},meas=measWrap.measurement||eng.measurement||{};
const masterState=String((eng.state||'idle')).toUpperCase();
const issue=flowSeverityFromRuntime();
const runtimeAge=Number(eng.runtimeStateDurationSeconds);
const ingestState=(ing.runtime?.state||ing.source?.state||'--');
setText('flow-master-sub',issue?issue.text:(masterState==='RUNNING'?(typeof meas.compositeFinalPreIq?.peakAbs==='number'?`Transmission active · composite peak ${Number(meas.compositeFinalPreIq.peakAbs).toFixed(2)} · pilot ${typeof meas.compositeFinalPreIq?.pilotInjectionEquivalentPercent==='number'?Number(meas.compositeFinalPreIq.pilotInjectionEquivalentPercent).toFixed(1):'--'}%`:'Transmission active and signal path healthy.'):masterState==='IDLE'?'Transmitter idle. Configure and start when ready.':'Runtime state present; inspect flow modules for details.'));
setText('flow-top-applied',isFinite(Number(eng.appliedFrequencyMHz))?`${Number(eng.appliedFrequencyMHz).toFixed(1)} MHz`:(isFinite(Number(measWrap.appliedFrequencyMHz))?`${Number(measWrap.appliedFrequencyMHz).toFixed(1)} MHz`:'--'));
setText('flow-top-alert',issue?issue.text:(measWrap.runtimeAlert||'None'));
setText('flow-bottom-queue',String(eng.queue?.health||measWrap.queue?.health||'--').toUpperCase());
setText('flow-bottom-ingest',String(ingestState||'--').toUpperCase());
setText('flow-bottom-age',isFinite(runtimeAge)?fmtTime(runtimeAge):'--');
setText('flow-bottom-update',ageStr(Math.max(S.server.lastConfigAt||0,S.server.lastRuntimeAt||0,S.server.lastMeasurementsAt||0)));
}

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||{};
@@ -1195,23 +1235,7 @@ function _render(){
// ── Overview: meters + sparklines
if(typeof ingRt.buffered==='number'||typeof ingRt.bufferedSeconds==='number'){const pct=Math.max(0,Math.min(1,Number(ingRt.buffered)||0));const secs=Math.max(0,Number(ingRt.bufferedSeconds)||0);const ratio=Math.max(0,Math.min(1,secs/1.5));const text=`${secs.toFixed(2)} s · ${fmtPct(pct)}`;const mode=secs<0.25?'err':secs<0.75?'warn':'good';setMeter('meter-audio-fill','meter-audio-text',ratio,text,mode);}else if(aud&&typeof aud.buffered==='number'){const r=Math.max(0,Math.min(1,aud.buffered));setMeter('meter-audio-fill','meter-audio-text',r,fmtPct(r),r<.05?'err':r<.2?'warn':'good');}else setMeter('meter-audio-fill','meter-audio-text',0,'N/A','warn');
const urN=Number(eng.underruns??drv.underruns??0);
setMeter('meter-stream-fill','meter-stream-text',urN<=0?1:Math.max(0,1-Math.min(urN,10)/10),urN===0?'Clean':`${urN} underrun${urN===1?'':'s'}`,urN===0?'good':urN<3?'warn':'err');
const txR=txSt==='running'?1:S.txBusy?.55:.08;
setMeter('meter-tx-fill','meter-tx-text',txR,txSt==='running'?'Live':S.txBusy?'Working':'Idle',txSt==='running'?'good':S.txBusy?'warn':'err');
const audioL=updateHoldMeter(S.meterState.audioL,Number(meas.lrPreEncodePostWatermark?.lRms||0),Number(meas.lrPreEncodePostWatermark?.lPeakAbs||0),{attackPerSec:7.0,releasePerSec:0.6,rmsHoldMs:1700,rmsHoldReleasePerSec:0.10,peakReleasePerSec:0.45,holdMs:1700,holdReleasePerSec:0.14,textRmsAttackPerSec:1.0,textRmsReleasePerSec:0.06,textPeakAttackPerSec:1.6,textPeakReleasePerSec:0.07,textRmsLatchMs:260,textPeakLatchMs:260,textRmsStep:0.01,textPeakStep:0.01,textRmsHysteresis:0.01,textPeakHysteresis:0.01});
const audioR=updateHoldMeter(S.meterState.audioR,Number(meas.lrPreEncodePostWatermark?.rRms||0),Number(meas.lrPreEncodePostWatermark?.rPeakAbs||0),{attackPerSec:7.0,releasePerSec:0.6,rmsHoldMs:1700,rmsHoldReleasePerSec:0.10,peakReleasePerSec:0.45,holdMs:1700,holdReleasePerSec:0.14,textRmsAttackPerSec:1.0,textRmsReleasePerSec:0.06,textPeakAttackPerSec:1.6,textPeakReleasePerSec:0.07,textRmsLatchMs:260,textPeakLatchMs:260,textRmsStep:0.01,textPeakStep:0.01,textRmsHysteresis:0.01,textPeakHysteresis:0.01});
const mpx=updateHoldMeter(S.meterState.mpx,0,Number(meas.compositeFinalPreIq?.peakAbs||0),{attackPerSec:8.0,releasePerSec:0.4,peakReleasePerSec:0.375,holdMs:2200,holdReleasePerSec:0.09,textPeakAttackPerSec:1.4,textPeakReleasePerSec:0.06,textPeakLatchMs:320,textPeakStep:0.01,textPeakHysteresis:0.01});
const audioLRmsDb=ampToDb(audioL.rms),audioRRmsDb=ampToDb(audioR.rms),audioLTextRmsDb=ampToDb(audioL.textRmsDisplay),audioRTextRmsDb=ampToDb(audioR.textRmsDisplay),audioLPeakDb=ampToDb(audioL.peak),audioRPeakDb=ampToDb(audioR.peak),audioLHoldDb=ampToDb(audioL.hold),audioRHoldDb=ampToDb(audioR.hold),audioLTextPeakDb=ampToDb(audioL.textPeakDisplay),audioRTextPeakDb=ampToDb(audioR.textPeakDisplay);
const audioMode=(db)=>(db>=-6?'err':db>=-12?'warn':'good');
const mpxMode=(v)=>(v>1.10?'err':v>1.00?'warn':'good');
renderHifiMeter('audio-l-rms-fill','audio-l-rms-marker','audio-l-rms-text',dbToRatio(audioLRmsDb),dbToRatio(ampToDb(audioL.rmsHold)),typeof meas.lrPreEncodePostWatermark?.lRms==='number'?`${audioLTextRmsDb.toFixed(1)} dB`:'--',1,audioMode(audioLRmsDb));
renderHifiMeter('audio-r-rms-fill','audio-r-rms-marker','audio-r-rms-text',dbToRatio(audioRRmsDb),dbToRatio(ampToDb(audioR.rmsHold)),typeof meas.lrPreEncodePostWatermark?.rRms==='number'?`${audioRTextRmsDb.toFixed(1)} dB`:'--',1,audioMode(audioRRmsDb));
renderHifiMeter('audio-l-peak-fill','audio-l-peak-marker','audio-l-peak-text',dbToRatio(audioLPeakDb),dbToRatio(audioLHoldDb),typeof meas.lrPreEncodePostWatermark?.lPeakAbs==='number'?`${audioLTextPeakDb.toFixed(1)} dB`:'--',1,audioMode(audioLPeakDb));
renderHifiMeter('audio-r-peak-fill','audio-r-peak-marker','audio-r-peak-text',dbToRatio(audioRPeakDb),dbToRatio(audioRHoldDb),typeof meas.lrPreEncodePostWatermark?.rPeakAbs==='number'?`${audioRTextPeakDb.toFixed(1)} dB`:'--',1,audioMode(audioRPeakDb));
renderHifiMeter('mpx-peak-fill','mpx-peak-marker','mpx-peak-text',mpx.peak,mpx.hold,typeof meas.compositeFinalPreIq?.peakAbs==='number'?fmtModPct(mpx.textPeakDisplay):'--',1.5,mpxMode(mpx.peak));
setHifiScale('Audio RMS',['−60 dB','−24 dB','−12 dB','−6 dB','0 dB']);
setHifiScale('Audio Peak',['−60 dB','−24 dB','−12 dB','−6 dB','0 dB']);
setHifiScale('MPX Peak',['0%','50%','100%','150%']);
renderMetersSection();
drawSpark('spark-audio',S.charts.audio,'good',1);
drawSpark('spark-underruns',S.charts.underruns,urN>0?'err':'warn');
drawSpark('spark-tx',S.charts.tx,txSt==='running'?'good':'warn',1);


Loading…
取消
儲存