Sfoglia il codice sorgente

Streamline control UI semantics and validation

main
Jan 1 mese fa
parent
commit
adf72a169f
1 ha cambiato i file con 64 aggiunte e 45 eliminazioni
  1. +64
    -45
      internal/control/ui.html

+ 64
- 45
internal/control/ui.html Vedi File

@@ -110,7 +110,6 @@ button,input,select{font:inherit}button{user-select:none}
/* Tags */
.tag{display:inline-flex;align-items:center;height:16px;padding:0 6px;border-radius:4px;font-size:9px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;flex-shrink:0}
.tag-live{background:rgba(13,148,74,.12);color:var(--green)}
.tag-saved{background:rgba(31,77,157,.12);color:var(--accent)}
.tag-restart{background:rgba(183,121,31,.12);color:var(--amber)}
.tag-reload{background:rgba(17,23,36,.08);color:var(--text-dim)}
/* Buttons */
@@ -326,9 +325,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
<div class="stack">
<!-- Frequency -->
<div class="card panel" data-panel-key="frequency">
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Frequency</h2><div class="meta" id="freq-meta">Live + Saved</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Frequency</h2><div class="meta" id="freq-meta">Applies immediately</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Tune without restarting — when TX is running, the change takes effect at the next chunk boundary (~50ms). The desired frequency is also written into config.</div>
<div class="section-note">Takes effect without restarting. When TX is running, the change applies at the next chunk boundary (~50 ms) and is written into config.</div>
<div class="preset-row" id="freq-presets">
<button class="preset-btn" type="button" data-freq-preset="87.6">87.6</button>
<button class="preset-btn" type="button" data-freq-preset="94.5">94.5</button>
@@ -338,7 +337,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">TX Freq</span><span class="ctrl-sub">65–110 MHz</span></div>
<div class="ctrl-input"><input type="range" min="65" max="110" step="0.1" id="freq-slider"><input type="number" min="65" max="110" step="0.1" id="freq-num"><span class="unit-label">MHz</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="65" max="110" step="0.1" id="freq-slider"><input type="number" min="65" max="110" step="0.1" id="freq-num"><span class="unit-label">MHz</span></div>
</div>
<div class="field-error" id="freq-error"></div>
<div class="actions-row"><button class="apply-btn" id="freq-apply" type="button">Apply + Save Frequency</button><button class="apply-btn secondary" id="freq-reset" type="button">Reset Draft</button></div>
@@ -346,9 +345,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- Audio & Drive -->
<div class="card panel" data-panel-key="audio">
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Audio &amp; Drive</h2><div class="meta" id="audio-meta">Mixed Apply Modes</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Audio &amp; Drive</h2><div class="meta" id="audio-meta">Immediate + restart-only</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Output Drive and Limiter Ceiling apply live. Pre-emphasis and Input Gain are saved to config and require TX restart to affect the DSP path.</div>
<div class="section-note">Output Drive and Limiter Ceiling take effect immediately. Pre-emphasis and Input Gain require TX restart before they affect the DSP path.</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Output Drive</span><span class="ctrl-sub">0 – 10</span></div>
<div class="ctrl-input"><input type="range" min="0" max="10" step="0.05" id="drive-slider"><span class="val-display" id="drive-val">--</span><span class="tag tag-live">live</span></div>
@@ -371,20 +370,20 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- Test Tones -->
<div class="card panel" data-panel-key="tones">
<div class="panel-head" data-panel><h2>Test Tones</h2><div class="meta">Diagnostic</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><h2>Test Tones</h2><div class="meta">Applies immediately</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Tone settings apply live to the running generator path and are also written into config. No TX restart is required for the new tone values to take effect. Set amplitude to 0 to disable.</div>
<div class="section-note">Tone settings take effect immediately in the running generator path. No TX restart is required. Set amplitude to 0 to disable.</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Left (Hz)</span></div>
<div class="ctrl-input"><input type="range" min="0" max="20000" step="10" id="tone-l-slider"><input type="number" min="0" max="20000" step="10" id="tone-l-num"><span class="unit-label">Hz</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="0" max="20000" step="10" id="tone-l-slider"><input type="number" min="0" max="20000" step="10" id="tone-l-num"><span class="unit-label">Hz</span></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Right (Hz)</span></div>
<div class="ctrl-input"><input type="range" min="0" max="20000" step="10" id="tone-r-slider"><input type="number" min="0" max="20000" step="10" id="tone-r-num"><span class="unit-label">Hz</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="0" max="20000" step="10" id="tone-r-slider"><input type="number" min="0" max="20000" step="10" id="tone-r-num"><span class="unit-label">Hz</span></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Amplitude</span><span class="ctrl-sub">0 – 1.0</span></div>
<div class="ctrl-input"><input type="range" min="0" max="1" step="0.01" id="tone-amp-slider"><span class="val-display" id="tone-amp-val">--</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="0" max="1" step="0.01" id="tone-amp-slider"><span class="val-display" id="tone-amp-val">--</span></div>
</div>
<div class="actions-row"><button class="apply-btn" id="tones-apply" type="button">Save Tone Config</button><button class="apply-btn secondary" id="tones-off" type="button">Disable Tones</button></div>
</div>
@@ -393,7 +392,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
<div class="stack">
<!-- Switches -->
<div class="card panel" data-panel-key="switches">
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Switches</h2><div class="meta">Live</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Switches</h2><div class="meta">Apply immediately</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="toggle-row">
<div class="toggle-copy"><div class="title">Stereo</div><div class="sub">19 kHz pilot + 38 kHz DSB-SC</div></div>
@@ -411,9 +410,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- MPX Compliance -->
<div class="card panel" data-panel-key="compliance">
<div class="panel-head" data-panel><h2>MPX Compliance</h2><div class="meta" id="compliance-meta">BS.412</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><h2>MPX Compliance</h2><div class="meta" id="compliance-meta">Restart required</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">ITU-R BS.412 limits total MPX power. Mandatory for licensed FM in EU/CH. Changes require TX restart.</div>
<div class="section-note">ITU-R BS.412 limits total MPX power. Mandatory for licensed FM in EU/CH. Changes in this panel require TX restart.</div>
<div class="toggle-row">
<div class="toggle-copy"><div class="title">BS.412 Limiter</div><div class="sub">60 s rolling power window &nbsp;<span class="tag tag-restart">restart</span></div></div>
<div class="toggle-ctl"><div class="toggle" id="tog-bs412" data-toggle-cfg="bs412Enabled" role="switch" aria-checked="false" tabindex="0"></div><div class="toggle-state" id="bs412-label">--</div></div>
@@ -432,9 +431,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- Composite Clipper -->
<div class="card panel" data-panel-key="compclip">
<div class="panel-head" data-panel><h2>Composite Clipper</h2><div class="meta" id="compclip-meta">SM.1268</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><h2>Composite Clipper</h2><div class="meta" id="compclip-meta">Immediate + restart-only</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">ITU-R SM.1268 iterative composite clipper. Enable/disable is live. Iterations, knee, and look-ahead require TX restart.</div>
<div class="section-note">ITU-R SM.1268 iterative composite clipper. Enable/disable takes effect immediately; iterations, knee and look-ahead require TX restart.</div>
<div class="toggle-row">
<div class="toggle-copy"><div class="title">Composite Clipper</div><div class="sub">Iterative clip-filter-clip + look-ahead &nbsp;<span class="tag tag-live">live</span></div></div>
<div class="toggle-ctl"><div class="toggle" id="tog-compclip" data-toggle="compositeClipperEnabled" role="switch" aria-checked="false" tabindex="0"></div><div class="toggle-state" id="compclip-label">--</div></div>
@@ -482,12 +481,12 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
<div class="stack">
<!-- Station Identity -->
<div class="card panel" data-panel-key="rds-identity">
<div class="panel-head" data-panel><div class="led on-amber" style="width:6px;height:6px"></div><h2>Station Identity</h2><div class="meta" id="rds-identity-meta">Restart required</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-amber" style="width:6px;height:6px"></div><h2>Station Identity</h2><div class="meta" id="rds-identity-meta">Mixed runtime impact</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">RDS enable applies live. PI and PTY are saved to config and take effect after the next TX restart.</div>
<div class="section-note">RDS enable takes effect immediately. PI and PTY require TX restart before they change on-air.</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Enable RDS</span><span class="ctrl-sub">57 kHz subcarrier</span></div>
<div class="ctrl-input"><div class="toggle" id="tog-rds" data-toggle="rdsEnabled" role="switch" aria-checked="false" tabindex="0"></div><div class="toggle-state" id="rds-label">--</div><span class="tag tag-live" style="margin-left:4px">live</span></div>
<div class="ctrl-input"><div class="toggle" id="tog-rds" data-toggle="rdsEnabled" role="switch" aria-checked="false" tabindex="0"></div><div class="toggle-state" id="rds-label">--</div></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">PI Code</span><span class="ctrl-sub">Programme Identifier (hex)</span></div>
@@ -515,9 +514,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- On-Air Text -->
<div class="card panel" data-panel-key="rds-text">
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>On-Air Text</h2><div class="meta" id="rds-text-meta">Saved + Runtime</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>On-Air Text</h2><div class="meta" id="rds-text-meta">Applies on-air</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">PS and RadioText apply at the next RDS group boundary (~88ms). Edits stay local until you apply, then update the live encoder and config snapshot together. When StreamTitle relay is enabled, the active on-air RadioText can temporarily differ from the saved config value shown in the editor.</div>
<div class="section-note">PS and RadioText apply at the next RDS group boundary (~88 ms). Edits stay local until you apply. With StreamTitle relay enabled, the active on-air RadioText can temporarily differ from the editor value.</div>
<div class="preset-row">
<button class="preset-btn rds" type="button" data-rds-ps="FMRTX" data-rds-rt="fm-rds-tx live">Station ID</button>
<button class="preset-btn rds" type="button" data-rds-ps="ONAIR" data-rds-rt="Now broadcasting">On Air</button>
@@ -526,13 +525,13 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<div class="rds-grid">
<div class="rds-field">
<div style="display:flex;align-items:center;justify-content:space-between"><span class="ctrl-label">Program Service (PS)</span><span class="tag tag-live">live</span></div>
<div style="display:flex;align-items:center;justify-content:space-between"><span class="ctrl-label">Program Service (PS)</span></div>
<input type="text" class="rds-input" id="rds-ps" maxlength="8" placeholder="STATION" spellcheck="false">
<div class="rds-charcount"><span id="ps-count">0</span>/8</div>
<div class="field-error" id="ps-error"></div>
</div>
<div class="rds-field">
<div style="display:flex;align-items:center;justify-content:space-between"><span class="ctrl-label">RadioText (RT)</span><span class="tag tag-live">live</span></div>
<div style="display:flex;align-items:center;justify-content:space-between"><span class="ctrl-label">RadioText (RT)</span></div>
<input type="text" class="rds-input rt" id="rds-rt" maxlength="64" placeholder="Now playing..." spellcheck="false">
<div class="rds-charcount"><span id="rt-count">0</span>/64</div>
<div class="field-error" id="rt-error"></div>
@@ -544,16 +543,16 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<!-- RDS Features -->
<div class="card panel" data-panel-key="rds-features">
<div class="panel-head" data-panel><div class="led on-amber" style="width:6px;height:6px"></div><h2>RDS Features</h2><div class="meta">Restart required</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-amber" style="width:6px;height:6px"></div><h2>RDS Features</h2><div class="meta">Immediate + restart-only</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Traffic, clock, RT+ and other RDS features. TP/TA apply live and are also saved to config; the remaining fields in this panel are saved to config and take effect after TX restart.</div>
<div class="section-note">Traffic, clock, RT+ and other RDS features. TP/TA take effect immediately; the remaining fields in this panel require TX restart.</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Traffic Program (TP)</span><span class="ctrl-sub">Station carries traffic info</span></div>
<div class="ctrl-input"><input type="checkbox" id="rds-tp"><span class="tag tag-live">live</span><span class="tag tag-saved">saved</span></div>
<div class="ctrl-input"><input type="checkbox" id="rds-tp"><span class="tag tag-live">live</span></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Traffic Announcement (TA)</span><span class="ctrl-sub">Currently on air</span></div>
<div class="ctrl-input"><input type="checkbox" id="rds-ta"><span class="tag tag-live">live</span><span class="tag tag-saved">saved</span></div>
<div class="ctrl-input"><input type="checkbox" id="rds-ta"><span class="tag tag-live">live</span></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Music / Speech</span><span class="ctrl-sub">MS flag for receivers</span></div>
@@ -581,7 +580,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Alt. Frequencies</span><span class="ctrl-sub">Comma-separated MHz</span></div>
<div class="ctrl-input"><input type="text" id="rds-af" placeholder="93.3, 95.7" style="width:180px;font-family:var(--mono);font-size:12px"><span class="tag tag-restart">restart</span></div>
<div class="ctrl-input" style="flex-direction:column;align-items:flex-start;gap:6px;width:100%"><div style="display:flex;align-items:center;gap:10px;width:100%"><input type="text" id="rds-af" placeholder="93.3, 95.7" style="width:180px;font-family:var(--mono);font-size:12px"><span class="tag tag-restart">restart</span></div><div class="field-error" id="rds-af-error"></div></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">eRT (Enhanced RT)</span><span class="ctrl-sub">UTF-8, 128 bytes, ODA</span></div>
@@ -614,16 +613,16 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
<div class="stack">
<!-- Injection Levels -->
<div class="card panel" data-panel-key="rds-levels">
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Injection Levels</h2><div class="meta">Live + Saved</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-green" style="width:6px;height:6px"></div><h2>Injection Levels</h2><div class="meta">Apply immediately</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Fixed percentages of ±75 kHz deviation. ITU standard: pilot 9%, RDS 4%. When TX is running, changes hot-apply; they are also written back into config.</div>
<div class="section-note">Fixed percentages of ±75 kHz deviation. ITU standard: pilot 9%, RDS 4%. When TX is running, changes apply immediately.</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">Pilot Level</span><span class="ctrl-sub">19 kHz, 0–20%</span></div>
<div class="ctrl-input"><input type="range" min="0" max="0.2" step="0.001" id="pilot-slider"><span class="val-display" id="pilot-val">--</span><span class="unit-label">%dev</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="0" max="0.2" step="0.001" id="pilot-slider"><span class="val-display" id="pilot-val">--</span><span class="unit-label">%dev</span></div>
</div>
<div class="ctrl-row">
<div class="ctrl-label-wrap"><span class="ctrl-label">RDS Injection</span><span class="ctrl-sub">57 kHz, 0–15%</span></div>
<div class="ctrl-input"><input type="range" min="0" max="0.15" step="0.001" id="rdsinj-slider"><span class="val-display" id="rdsinj-val">--</span><span class="unit-label">%dev</span><span class="tag tag-live">live</span></div>
<div class="ctrl-input"><input type="range" min="0" max="0.15" step="0.001" id="rdsinj-slider"><span class="val-display" id="rdsinj-val">--</span><span class="unit-label">%dev</span></div>
</div>
<div class="actions-row"><button class="apply-btn" id="rds-levels-apply" type="button">Apply + Save Levels</button><button class="apply-btn secondary" id="rds-levels-reset" type="button">Reset Draft</button></div>
</div>
@@ -666,9 +665,9 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
</div>
</div>
<div class="card panel" data-panel-key="ingest">
<div class="panel-head" data-panel><div class="led on-blue" style="width:6px;height:6px"></div><h2>Ingest Config</h2><div class="meta" id="ingest-meta">Saved + Hard Reload</div><span class="chevron">▼</span></div>
<div class="panel-head" data-panel><div class="led on-blue" style="width:6px;height:6px"></div><h2>Ingest Config</h2><div class="meta" id="ingest-meta">Hard reload required</div><span class="chevron">▼</span></div>
<div class="panel-body">
<div class="section-note">Changes are saved to the config file and take effect only after a hard reload of the service.</div>
<div class="section-note">Changes in this panel take effect only after a hard reload of the service.</div>
<div class="ingest-grid">
<div class="ctrl-row"><div class="ctrl-label-wrap"><span class="ctrl-label">Kind</span></div><div class="ctrl-input"><select id="ing-kind" data-ingest-path="kind"><option value="none">none</option><option value="icecast">icecast</option><option value="srt">srt</option><option value="aes67">aes67</option><option value="stdin">stdin</option><option value="http-raw">http-raw</option></select></div></div>
<div class="ctrl-row"><div class="ctrl-label-wrap"><span class="ctrl-label">Prebuffer</span><span class="ctrl-sub">ms</span></div><div class="ctrl-input"><input type="number" id="ing-prebuffer" data-ingest-path="prebufferMs"></div></div>
@@ -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));


Loading…
Annulla
Salva