| @@ -3,7 +3,7 @@ | |||||
| <head> | <head> | ||||
| <meta charset="utf-8"> | <meta charset="utf-8"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <title>fm-rds-tx</title> | |||||
| <title>ferrite.fm</title> | |||||
| <style> | <style> | ||||
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); | ||||
| :root { | :root { | ||||
| @@ -228,7 +228,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 | |||||
| <div class="app"> | <div class="app"> | ||||
| <div class="header"> | <div class="header"> | ||||
| <div class="header-main"> | <div class="header-main"> | ||||
| <h1>FM-RDS-TX Control Plane</h1> | |||||
| <h1>ferrite.fm Control Plane</h1> | |||||
| <div class="header-note">Operate confidently: tune fast, inspect state instantly, diagnose only when needed.</div> | <div class="header-note">Operate confidently: tune fast, inspect state instantly, diagnose only when needed.</div> | ||||
| <div class="header-sub"> | <div class="header-sub"> | ||||
| <div class="badge"><span>Backend</span><strong id="badge-backend">--</strong></div> | <div class="badge"><span>Backend</span><strong id="badge-backend">--</strong></div> | ||||
| @@ -237,6 +237,8 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="header-status"> | <div class="header-status"> | ||||
| <button class="danger-btn" id="header-danger-stop" type="button">Emergency Stop TX</button> | |||||
| <button class="danger-btn" id="header-danger-reset-fault" type="button">Reset Fault</button> | |||||
| <div class="led" id="led-conn"></div> | <div class="led" id="led-conn"></div> | ||||
| <div class="status-text" id="conn-label">connecting</div> | <div class="status-text" id="conn-label">connecting</div> | ||||
| </div> | </div> | ||||
| @@ -454,24 +456,6 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 | |||||
| <div class="section-note reset-hint">Structural clipper changes (iterations, knee, look-ahead) are persisted to config and require TX restart.</div> | <div class="section-note reset-hint">Structural clipper changes (iterations, knee, look-ahead) are persisted to config and require TX restart.</div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <!-- Danger --> | |||||
| <div class="card panel" data-panel-key="danger"> | |||||
| <div class="panel-head" data-panel><h2>Danger Zone</h2><div class="meta">emergency</div><span class="chevron">▼</span></div> | |||||
| <div class="panel-body"> | |||||
| <div class="actions-row" style="margin-top:0"><button class="danger-btn" id="danger-stop" type="button">Emergency Stop TX</button><button class="danger-btn" id="danger-reset-fault" type="button">Reset Fault</button></div> | |||||
| <div class="section-note reset-hint" id="reset-hint">Reset Fault moves the runtime back to DEGRADED while the queue settles.</div> | |||||
| </div> | |||||
| </div> | |||||
| <!-- Shortcuts --> | |||||
| <div class="card panel" data-panel-key="shortcuts"> | |||||
| <div class="panel-head" data-panel><h2>Shortcuts</h2><div class="meta">keyboard</div><span class="chevron">▼</span></div> | |||||
| <div class="panel-body"> | |||||
| <div class="shortcuts-grid"> | |||||
| <div><div class="shortcut-line"><span class="name">Start TX</span><span class="keys"><span class="kbd">t</span></span></div><div class="shortcut-line"><span class="name">Stop TX</span><span class="keys"><span class="kbd">T</span></span></div><div class="shortcut-line"><span class="name">Refresh</span><span class="keys"><span class="kbd">r</span></span></div></div> | |||||
| <div><div class="shortcut-line"><span class="name">Next Preset</span><span class="keys"><span class="kbd">]</span></span></div><div class="shortcut-line"><span class="name">Prev Preset</span><span class="keys"><span class="kbd">[</span></span></div><div class="shortcut-line"><span class="name">Apply Draft</span><span class="keys"><span class="kbd">Enter</span></span></div></div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| @@ -1076,8 +1060,7 @@ function _render(){ | |||||
| setText('compclip-meta',S.cfgDirty['compclip']?'Immediate + restart-only · draft pending':'Immediate + restart-only'); | 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']; | $('compclip-apply').disabled=!S.cfgDirty['compclip'];$('compclip-reset').disabled=!S.cfgDirty['compclip']; | ||||
| // Danger | // 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';} | |||||
| const rh=$('reset-hint');if(rh){const sn=normState(eng.state);rh.textContent=sn==='faulted'?'Faulted: reset moves runtime back to DEGRADED.':sn==='muted'||sn==='degraded'?'Reset Fault holds at DEGRADED until queue recovers.':'Manual fault reset drops to DEGRADED while queue recovers.';} | |||||
| const stopButtons=[$('header-danger-stop')].filter(Boolean);stopButtons.forEach(btn=>{btn.disabled=S.txBusy;btn.textContent='Emergency Stop TX';});const resetButtons=[$('header-danger-reset-fault')].filter(Boolean);resetButtons.forEach(btn=>{btn.disabled=S.faultBusy||!S.server.runtimeOk;btn.textContent=S.faultBusy?'Resetting...':'Reset Fault';}); | |||||
| // ── RDS tab | // ── RDS tab | ||||
| syncToggle('tog-rds','rds-label','rdsEnabled'); | syncToggle('tog-rds','rds-label','rdsEnabled'); | ||||
| @@ -1217,8 +1200,8 @@ function bindAll(){ | |||||
| $('compclip-apply').addEventListener('click',()=>applyCfgSection('compclip'));$('compclip-reset').addEventListener('click',()=>{cfgClear('compclip');toast('Draft reset','info');}); | $('compclip-apply').addEventListener('click',()=>applyCfgSection('compclip'));$('compclip-reset').addEventListener('click',()=>{cfgClear('compclip');toast('Draft reset','info');}); | ||||
| // TX | // TX | ||||
| $('btn-start').addEventListener('click',()=>txAction('start'));$('btn-stop').addEventListener('click',()=>txAction('stop')); | $('btn-start').addEventListener('click',()=>txAction('start'));$('btn-stop').addEventListener('click',()=>txAction('stop')); | ||||
| $('danger-stop').addEventListener('click',()=>txAction('stop'));$('btn-refresh').addEventListener('click',manualRefresh); | |||||
| $('danger-reset-fault').addEventListener('click',()=>resetFault()); | |||||
| $('header-danger-stop').addEventListener('click',()=>txAction('stop'));$('btn-refresh').addEventListener('click',manualRefresh); | |||||
| $('header-danger-reset-fault').addEventListener('click',()=>resetFault()); | |||||
| // RDS | // RDS | ||||
| $('rds-pi').addEventListener('input',e=>{const v=e.target.value.toUpperCase().replace(/[^0-9A-F]/g,'').slice(0,4);e.target.value=v;S.cfgErrors=S.cfgErrors||{};S.cfgErrors.pi=validate('pi',v);cfgSetDirty('pi',v);}); | $('rds-pi').addEventListener('input',e=>{const v=e.target.value.toUpperCase().replace(/[^0-9A-F]/g,'').slice(0,4);e.target.value=v;S.cfgErrors=S.cfgErrors||{};S.cfgErrors.pi=validate('pi',v);cfgSetDirty('pi',v);}); | ||||
| $('rds-pty').addEventListener('change',e=>cfgSetDirty('pty',Number(e.target.value))); | $('rds-pty').addEventListener('change',e=>cfgSetDirty('pty',Number(e.target.value))); | ||||
| @@ -1255,7 +1238,7 @@ function bindAll(){ | |||||
| // Log | // Log | ||||
| $('btn-clear-log').addEventListener('click',()=>{$('log').innerHTML='<div class="empty-log">No activity recorded yet.</div>';toast('Activity log cleared','info');}); | $('btn-clear-log').addEventListener('click',()=>{$('log').innerHTML='<div class="empty-log">No activity recorded yet.</div>';toast('Activity log cleared','info');}); | ||||
| // Keyboard | // Keyboard | ||||
| window.addEventListener('keydown',e=>{const typing=(()=>{const t=e.target;if(!t)return false;const tag=(t.tagName||'').toLowerCase();return tag==='input'||tag==='textarea'||t.isContentEditable;})();if(typing){if(e.key==='Enter'){e.preventDefault();if(S.dirty.has('frequencyMHz'))applySection('freq');else if(secDirty('rds'))applySection('rds');}return;}if(e.key==='t'&&!e.shiftKey){e.preventDefault();txAction('start');}else if(e.key==='T'||(e.key==='t'&&e.shiftKey)){e.preventDefault();txAction('stop');}else if(e.key.toLowerCase()==='r'){e.preventDefault();manualRefresh();}else if(e.key==='['){e.preventDefault();cyclePreset(-1);}else if(e.key===']'){e.preventDefault();cyclePreset(1);}else if(e.key==='Enter'){e.preventDefault();if(S.dirty.has('frequencyMHz'))applySection('freq');else if(secDirty('rds'))applySection('rds');}}); | |||||
| window.addEventListener('keydown',e=>{const typing=(()=>{const t=e.target;if(!t)return false;const tag=(t.tagName||'').toLowerCase();return tag==='input'||tag==='textarea'||t.isContentEditable;})();if(typing&&e.key==='Enter'){e.preventDefault();if(S.dirty.has('frequencyMHz'))applySection('freq');else if(secDirty('rds'))applySection('rds');}}); | |||||
| // Responsive | // Responsive | ||||
| const respHandler=()=>{if(!mobileMq.matches)S.mobilePanelsApplied=false;else applyMobilePanels();};if(mobileMq.addEventListener)mobileMq.addEventListener('change',respHandler);else mobileMq.addListener(respHandler); | const respHandler=()=>{if(!mobileMq.matches)S.mobilePanelsApplied=false;else applyMobilePanels();};if(mobileMq.addEventListener)mobileMq.addEventListener('change',respHandler);else mobileMq.addListener(respHandler); | ||||
| } | } | ||||
| @@ -1265,11 +1248,11 @@ function startPollers(){if(S.pollersStarted)return;S.pollersStarted=true;setInte | |||||
| async function init(){ | async function init(){ | ||||
| bindAll();render(); | bindAll();render(); | ||||
| log('fm-rds-tx control UI booting','info'); | |||||
| log('ferrite.fm control UI booting','info'); | |||||
| await Promise.allSettled([loadConfig({silent:false}),loadRuntime({silent:true})]); | await Promise.allSettled([loadConfig({silent:false}),loadRuntime({silent:true})]); | ||||
| render();startPollers(); | render();startPollers(); | ||||
| log('Polling active: runtime 1s · config 8s','ok'); | log('Polling active: runtime 1s · config 8s','ok'); | ||||
| log('Keyboard shortcuts ready','info'); | |||||
| log('UI ready','info'); | |||||
| } | } | ||||
| init(); | init(); | ||||
| </script> | </script> | ||||