| @@ -3,7 +3,7 @@ | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <title>fm-rds-tx</title> | |||
| <title>ferrite.fm</title> | |||
| <style> | |||
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); | |||
| :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="header"> | |||
| <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-sub"> | |||
| <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 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="status-text" id="conn-label">connecting</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> | |||
| </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> | |||
| </section> | |||
| @@ -1076,8 +1060,7 @@ function _render(){ | |||
| 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';} | |||
| 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 | |||
| 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');}); | |||
| // TX | |||
| $('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-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))); | |||
| @@ -1255,7 +1238,7 @@ function bindAll(){ | |||
| // Log | |||
| $('btn-clear-log').addEventListener('click',()=>{$('log').innerHTML='<div class="empty-log">No activity recorded yet.</div>';toast('Activity log cleared','info');}); | |||
| // 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 | |||
| 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(){ | |||
| 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})]); | |||
| render();startPollers(); | |||
| log('Polling active: runtime 1s · config 8s','ok'); | |||
| log('Keyboard shortcuts ready','info'); | |||
| log('UI ready','info'); | |||
| } | |||
| init(); | |||
| </script> | |||