Changes are saved to the config file and take effect only after a hard reload of the service.
Changes in this panel take effect only after a hard reload of the service.
Kind
none icecast srt aes67 stdin http-raw
@@ -795,6 +794,7 @@ let toastTimer=null;
const S={
server:{config:null,runtime:null,configOk:false,runtimeOk:false,lastConfigAt:0,lastRuntimeAt:0},
lastRTState:'',draft:{},errors:{},dirty:new Set(),
+ fieldErrors:{},
pending:0,txBusy:false,faultBusy:false,toggleBusy:{},
cfgDraft:{},cfgDirty:{},cfgErrors:{},
ingestDraft:null,ingestDirty:false,ingestSaving:false,ingestError:'',
@@ -863,6 +863,11 @@ function cfgSetDirty(key,val){
S.cfgDirty[f.sec]=Object.keys(CFG).filter(k=>CFG[k].sec===f.sec).some(k=>!cfgEq(k,S.cfgDraft[k]??cfgSrvVal(k),cfgSrvVal(k)));
render();
}
+function setFieldError(key,msg){
+ if(msg) S.fieldErrors[key]=msg;
+ else delete S.fieldErrors[key];
+ render();
+}
function cfgClear(sec){Object.keys(CFG).filter(k=>CFG[k].sec===sec).forEach(k=>delete S.cfgDraft[k]);S.cfgDirty[sec]=false;if(sec==='rds-id'&&S.cfgErrors)delete S.cfgErrors.pi;render();}
function cfgPatch(sec){const p={};Object.keys(CFG).filter(k=>CFG[k].sec===sec).forEach(k=>{const v=S.cfgDraft[k];if(v!==undefined&&!cfgEq(k,v,cfgSrvVal(k)))p[CFG_PATCH_KEY[k]]=v;});return p;}
function cfgHasRestart(sec){return Object.keys(CFG).filter(k=>CFG[k].sec===sec&&!CFG[k].live).some(k=>S.cfgDraft[k]!==undefined);}
@@ -937,6 +942,19 @@ function endReq(){S.pending=Math.max(0,S.pending-1);setConn(S.server.configOk||S
async function sendPatch(patch,{ok='Applied',clearKeys=[]}={}){beginReq();try{const res=await api('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)});clearDirty(clearKeys);render();toast(ok+(res.live?' · live':''),'ok');log('PATCH '+JSON.stringify(patch)+(res.live?' [live]':' [saved]'),'ok');await Promise.allSettled([loadConfig({silent:true}),loadRuntime({silent:true})]);return res;}catch(e){toast(e.message,'err');log('PATCH failed: '+e.message,'err');throw e;}finally{endReq();}}
async function applySection(sec){if(secErrors(sec)){toast('Fix validation errors first','warn');return;}const patch=secPatch(sec),keys=Object.keys(patch);if(!keys.length){toast('No changes','info');return;}const msg=sec==='freq'?'Frequency updated':sec==='rds'?'RDS text updated':'Applied';await sendPatch(patch,{ok:msg,clearKeys:keys});}
function resetSection(sec){clearDirty(Object.keys(FIELDS).filter(k=>FIELDS[k].section===sec));toast('Draft reset','info');}
+function parseAFInput(raw){
+ const text=String(raw||'').trim();
+ if(text==='') return {values:[], error:''};
+ const parts=text.split(',').map(s=>s.trim()).filter(Boolean);
+ const values=[];
+ for(const part of parts){
+ const n=Number(part);
+ if(!isFinite(n)) return {values:null, error:`Invalid AF entry: ${part}`};
+ if(n<87.5||n>108.0) return {values:null, error:`AF out of FM band: ${part}`};
+ values.push(n);
+ }
+ return {values, error:''};
+}
async function applyCfgSection(sec){if(sec==='rds-id'&&S.cfgErrors?.pi){toast('Fix validation errors first','warn');return;}const patch=cfgPatch(sec);if(!Object.keys(patch).length){toast('No changes','info');return;}const hasR=cfgHasRestart(sec);beginReq();try{const res=await api('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)});Object.keys(CFG).filter(k=>CFG[k].sec===sec).forEach(k=>delete S.cfgDraft[k]);S.cfgDirty[sec]=false;if(sec==='rds-id'&&S.cfgErrors)delete S.cfgErrors.pi;toast(hasR?'Saved (restart required)':'Applied live','ok');log('CFG '+sec+' '+JSON.stringify(patch)+(hasR?' [restart]':' [live]'),'ok');await Promise.allSettled([loadConfig({silent:true}),loadRuntime({silent:true})]);return res;}catch(e){toast(e.message,'err');log('CFG failed: '+e.message,'err');throw e;}finally{endReq();}}
async function txAction(action){
if(S.txBusy)return;
@@ -1027,7 +1045,7 @@ function _render(){
syncDirtyInput('freq-slider','frequencyMHz',v=>typeof v==='number'?v.toFixed(1):'100.0');
syncDirtyInput('freq-num','frequencyMHz',v=>typeof v==='number'?v.toFixed(1):'100.0');
$('freq-apply').disabled=!secDirty('freq')||secErrors('freq');$('freq-reset').disabled=!secDirty('freq');
- setText('freq-meta',secErrors('freq')?'Validation error':secDirty('freq')?'Draft pending':'Live + Saved');
+ setText('freq-meta',secErrors('freq')?'Validation error':secDirty('freq')?'Apply immediately · draft pending':'Applies immediately');
const fe=$('freq-error');if(fe){fe.textContent=S.errors.frequencyMHz||'';fe.classList.toggle('show',!!S.errors.frequencyMHz);}
refreshFreqPresets();
// Audio sliders
@@ -1035,7 +1053,7 @@ function _render(){
syncSlider('lim-ceiling-slider','lim-ceiling-val','limiterCeiling',v=>v==null?'--':Number(v).toFixed(2));
syncSlider('gain-slider','gain-val','audioGain',v=>v==null?'--':Number(v).toFixed(2));
const peEl=$('preemph-select');if(peEl&&document.activeElement!==peEl)peEl.value=String(cfgEff('preEmphasisTauUS')??50);
- setText('audio-meta',S.cfgDirty['audio']?'Draft pending':'Mixed Apply Modes');
+ setText('audio-meta',S.cfgDirty['audio']?'Immediate + restart-only · draft pending':'Immediate + restart-only');
$('audio-apply').disabled=!S.cfgDirty['audio'];$('audio-reset').disabled=!S.cfgDirty['audio'];
// Tones
syncSlider('tone-amp-slider','tone-amp-val','toneAmplitude',v=>v==null?'--':Number(v).toFixed(2));
@@ -1048,14 +1066,14 @@ function _render(){
syncCfgToggle('tog-bs412','bs412-label','bs412Enabled');
syncSlider('bs412-threshold-slider','bs412-threshold-val','bs412ThresholdDBr',v=>v==null?'--':Number(v).toFixed(1));
syncSlider('mpxgain-slider','mpxgain-val','mpxGain',v=>v==null?'--':Number(v).toFixed(2));
- setText('compliance-meta',S.cfgDirty['compliance']?'Draft pending':'Saved + Restart Required');
+ setText('compliance-meta',S.cfgDirty['compliance']?'Restart required · draft pending':'Restart required');
$('compliance-apply').disabled=!S.cfgDirty['compliance'];$('compliance-reset').disabled=!S.cfgDirty['compliance'];
// Composite Clipper
syncToggle('tog-compclip','compclip-label','compositeClipperEnabled');
syncSlider('compclip-iter-slider','compclip-iter-val','compositeClipperIterations',v=>v==null?'--':String(Math.round(Number(v))));
syncSlider('compclip-knee-slider','compclip-knee-val','compositeClipperSoftKnee',v=>v==null?'--':Number(v).toFixed(2));
syncSlider('compclip-la-slider','compclip-la-val','compositeClipperLookaheadMs',v=>v==null?'--':Number(v).toFixed(1));
- setText('compclip-meta',S.cfgDirty['compclip']?'Draft pending':'SM.1268');
+ setText('compclip-meta',S.cfgDirty['compclip']?'Immediate + restart-only · draft pending':'Immediate + restart-only');
$('compclip-apply').disabled=!S.cfgDirty['compclip'];$('compclip-reset').disabled=!S.cfgDirty['compclip'];
// Danger
$('danger-stop').disabled=S.txBusy;const rfl=$('danger-reset-fault');if(rfl){rfl.disabled=S.faultBusy||!S.server.runtimeOk;rfl.textContent=S.faultBusy?'Resetting...':'Reset Fault';}
@@ -1069,13 +1087,13 @@ function _render(){
const piErr=$('pi-error');if(piErr){const e=S.cfgErrors?.pi||'';piErr.textContent=e;piErr.classList.toggle('show',!!e);}
// PTY
const ptyEl=$('rds-pty');if(ptyEl&&document.activeElement!==ptyEl)ptyEl.value=String(cfgEff('pty')??cfg.rds?.pty??0);
- const idDirty=!!S.cfgDirty['rds-id'];setText('rds-identity-meta',S.cfgErrors?.pi?'Validation error':idDirty?'Draft pending':'Saved + Restart Required');
+ const idDirty=!!S.cfgDirty['rds-id'];setText('rds-identity-meta',S.cfgErrors?.pi?'Validation error':idDirty?'Mixed runtime impact · draft pending':'Mixed runtime impact');
$('rds-identity-apply').disabled=!idDirty||!!S.cfgErrors?.pi;$('rds-identity-reset').disabled=!idDirty;
// Text
syncDirtyInput('rds-ps','ps',v=>String(v??''));syncDirtyInput('rds-rt','radioText',v=>String(v??''));
const psV=String(effVal('ps')??cfg.rds?.ps??''),rtV=String(effVal('radioText')??cfg.rds?.radioText??'');
setText('ps-count',psV.length);setText('rt-count',rtV.length);
- const rdsD=secDirty('rds');setText('rds-text-meta',secErrors('rds')?'Validation error':rdsD?'Draft pending':'Saved + Runtime');
+ const rdsD=secDirty('rds');setText('rds-text-meta',secErrors('rds')?'Validation error':rdsD?'Applies on-air · draft pending':'Applies on-air');
$('rds-apply').disabled=!rdsD||secErrors('rds');$('rds-reset').disabled=!rdsD;
const psErr=$('ps-error');if(psErr){psErr.textContent=S.errors.ps||'';psErr.classList.toggle('show',!!S.errors.ps);}
const rtErr=$('rt-error');if(rtErr){rtErr.textContent=S.errors.radioText||'';rtErr.classList.toggle('show',!!S.errors.radioText);}
@@ -1093,10 +1111,11 @@ function _render(){
const sepEl=$('rds-rtplus-sep');if(sepEl&&document.activeElement!==sepEl){const sv=cfgEff('rdsRTPlusSep');sepEl.value=sv!=null?sv:(rCfg.rtPlusSeparator||' - ');}
const ptynEl=$('rds-ptyn');if(ptynEl&&document.activeElement!==ptynEl){const pv=cfgEff('rdsPTYN');ptynEl.value=pv!=null?pv:(rCfg.ptyn||'');}
const lpsEl=$('rds-lps');if(lpsEl&&document.activeElement!==lpsEl){const lv=cfgEff('rdsLPS');lpsEl.value=lv!=null?lv:(rCfg.lps||'');}
- const afEl=$('rds-af');if(afEl&&document.activeElement!==afEl){const av=cfgEff('rdsAF');afEl.value=(av!=null?av:(rCfg.af||[])).join(', ');}
+ const afEl=$('rds-af');if(afEl&&document.activeElement!==afEl){const av=cfgEff('rdsAF');afEl.value=(av!=null?av:(rCfg.af||[])).join(', ');afEl.classList.toggle('input-error',!!S.fieldErrors.rdsAF);}
+ const afErr=$('rds-af-error');if(afErr){afErr.textContent=S.fieldErrors.rdsAF||'';afErr.classList.toggle('show',!!S.fieldErrors.rdsAF);}
const ertOnEl=$('rds-ert-on');if(ertOnEl){const ev=cfgEff('rdsERTEnabled');ertOnEl.checked=ev!=null?!!ev:!!rCfg.ertEnabled;}
const ertEl=$('rds-ert');if(ertEl&&document.activeElement!==ertEl){const etv=cfgEff('rdsERT');ertEl.value=etv!=null?etv:(rCfg.ert||'');}
- const featDirty=!!S.cfgDirty['rds-feat'];$('rds-features-apply').disabled=!featDirty;$('rds-features-reset').disabled=!featDirty;
+ const featDirty=!!S.cfgDirty['rds-feat'];$('rds-features-apply').disabled=!featDirty||!!S.fieldErrors.rdsAF;$('rds-features-reset').disabled=!featDirty;
// RDS2
const r2On=$('rds2-on');if(r2On){const r2v=cfgEff('rdsRDS2Enabled');r2On.checked=r2v!=null?!!r2v:!!rCfg.rds2Enabled;}
const r2Logo=$('rds2-logo');if(r2Logo&&document.activeElement!==r2Logo){const lv=cfgEff('rdsLogoPath');r2Logo.value=lv!=null?lv:(rCfg.stationLogoPath||'');}
@@ -1125,7 +1144,7 @@ function _render(){
syncIngInput('ing-aes67-group','aes67.multicastGroup',v=>String(v??''));syncIngInput('ing-aes67-port','aes67.port',v=>isFinite(Number(v))?Number(v):5004);syncIngInput('ing-aes67-sdppath','aes67.sdpPath',v=>String(v??''));syncIngInput('ing-aes67-rate','aes67.sampleRateHz',v=>isFinite(Number(v))?Number(v):48000);syncIngInput('ing-aes67-channels','aes67.channels',v=>isFinite(Number(v))?Number(v):2);syncIngInput('ing-aes67-encoding','aes67.encoding',v=>String(v??'L24'));syncIngInput('ing-aes67-jitter','aes67.jitterDepthPackets',v=>isFinite(Number(v))?Number(v):8);syncIngChk('ing-aes67-discovery-enabled','aes67.discovery.enabled');syncIngInput('ing-aes67-discovery-name','aes67.discovery.streamName',v=>String(v??''));
const kind=String(ingVal('kind')||'none').toLowerCase();
$('ing-group-icecast').style.display=kind==='icecast'?'':'none';$('ing-group-srt').style.display=kind==='srt'?'':'none';$('ing-group-aes67').style.display=kind==='aes67'?'':'none';
- setText('ingest-meta',S.ingestSaving?'Saving…':S.ingestDirty?'Draft pending':'Saved + Hard Reload');
+ setText('ingest-meta',S.ingestSaving?'Saving…':S.ingestDirty?'Hard reload required · draft pending':'Hard reload required');
$('ingest-save-reload').disabled=!S.ingestDirty||S.ingestSaving||!S.server.configOk;$('ingest-save-reload').textContent=S.ingestSaving?'Saving...':'Save + Hard Reload';
$('ingest-reset').disabled=!S.ingestDirty||S.ingestSaving;
const iErr=$('ingest-error');if(iErr){iErr.textContent=S.ingestError||'';iErr.classList.toggle('show',!!S.ingestError);}
@@ -1221,9 +1240,9 @@ function bindAll(){
$('rds-lps')?.addEventListener('input',e=>cfgSetDirty('rdsLPS',e.target.value));
$('rds-ert-on')?.addEventListener('change',e=>cfgSetDirty('rdsERTEnabled',e.target.checked));
$('rds-ert')?.addEventListener('input',e=>cfgSetDirty('rdsERT',e.target.value));
- $('rds-af')?.addEventListener('input',e=>{const v=e.target.value.split(',').map(s=>parseFloat(s.trim())).filter(n=>!isNaN(n)&&n>=87.5&&n<=108.0);cfgSetDirty('rdsAF',v);});
+ $('rds-af')?.addEventListener('input',e=>{const parsed=parseAFInput(e.target.value);if(parsed.error){setFieldError('rdsAF',parsed.error);return;}setFieldError('rdsAF','');cfgSetDirty('rdsAF',parsed.values);});
$('rds-features-apply')?.addEventListener('click',()=>applyCfgSection('rds-feat'));
- $('rds-features-reset')?.addEventListener('click',()=>{cfgClear('rds-feat');toast('Draft reset','info');});
+ $('rds-features-reset')?.addEventListener('click',()=>{cfgClear('rds-feat');setFieldError('rdsAF','');toast('Draft reset','info');});
// RDS2
$('rds2-on')?.addEventListener('change',e=>cfgSetDirty('rdsRDS2Enabled',e.target.checked));
$('rds2-logo')?.addEventListener('input',e=>cfgSetDirty('rdsLogoPath',e.target.value));