diff --git a/internal/control/ui.html b/internal/control/ui.html index 6d710a9..93e004e 100644 --- a/internal/control/ui.html +++ b/internal/control/ui.html @@ -47,10 +47,20 @@ button,input,select{font:inherit}button{user-select:none} .flow-banner-title{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase} .flow-banner-text{font-size:12px;color:var(--text-dim)} .flow-board{padding:16px;display:flex;flex-direction:column;gap:16px} +.flow-master{display:grid;grid-template-columns:minmax(260px,1.2fr) minmax(0,2.2fr);gap:12px;align-items:stretch} +.flow-master-hero{padding:16px;border:1px solid var(--border-strong);border-radius:12px;background:linear-gradient(180deg,#fff 0%,#f5f8fc 100%);box-shadow:0 14px 28px rgba(15,23,42,.09)} +.flow-master-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:8px} +.flow-master-state{font-size:26px;font-weight:800;letter-spacing:-.03em;line-height:1.05} +.flow-master-state.good{color:var(--green)} +.flow-master-state.warn{color:var(--amber)} +.flow-master-state.err{color:var(--red)} +.flow-master-state.idle{color:var(--text-dim)} +.flow-master-sub{margin-top:8px;font-size:12px;color:var(--text-dim)} .flow-topbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px} .flow-summary{padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface2)} .flow-summary-label{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:6px} .flow-summary-value{font-size:16px;font-weight:700} +.flow-summary-value.compact{font-size:14px;line-height:1.3} .flow-chain{display:grid;grid-template-columns:repeat(8,minmax(120px,1fr));gap:16px;align-items:stretch} .flow-node{position:relative;display:flex;flex-direction:column;gap:8px;min-height:136px;padding:14px 14px 16px;border:1px solid var(--border);border-radius:12px;background:linear-gradient(180deg,#ffffff 0%, #f7f9fc 100%);box-shadow:0 10px 26px rgba(15,23,42,.08);cursor:pointer;transition:border-color .16s,transform .16s,box-shadow .16s} .flow-node::after{content:'';position:absolute;top:50%;right:-17px;width:18px;height:2px;background:linear-gradient(90deg,var(--border-strong),rgba(188,197,206,.2));transform:translateY(-50%)} @@ -273,8 +283,8 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 .toast.ok{background:var(--green);color:var(--bg)}.toast.err{background:var(--accent);color:#fff} .toast.warn{background:var(--amber);color:#141414}.toast.info{background:var(--text-dim);color:#fff} /* Responsive */ -@media(max-width:980px){.tab-columns.two{grid-template-columns:1fr}.tx-bar{grid-template-columns:1fr}.tx-state-wrap{align-items:flex-start}.status-hint{text-align:left}.quick-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.signal-grid{grid-template-columns:1fr}.flow-topbar,.flow-bottom{grid-template-columns:repeat(2,minmax(0,1fr))}.flow-chain{grid-template-columns:repeat(4,minmax(140px,1fr))}} -@media(max-width:640px){.app{padding:12px}.header{flex-direction:column;gap:10px}.header h1{font-size:22px}.badge{width:100%;justify-content:space-between}.badge strong{max-width:52%;overflow:hidden;text-overflow:ellipsis}.quick-grid{grid-template-columns:1fr 1fr;gap:8px}.ctrl-row{flex-direction:column;align-items:stretch}.ctrl-label-wrap{min-width:auto}.ctrl-input{flex-wrap:wrap}.ingest-grid{grid-template-columns:1fr}input[type="number"]{width:100%;text-align:left}.actions-row,.tx-actions{flex-direction:column}.tx-btn,.ghost-btn,.apply-btn,.preset-btn,.danger-btn{width:100%}.freq-display{font-size:31px}.flow-topbar,.flow-bottom,.flow-chain{grid-template-columns:1fr}} +@media(max-width:980px){.tab-columns.two{grid-template-columns:1fr}.tx-bar{grid-template-columns:1fr}.tx-state-wrap{align-items:flex-start}.status-hint{text-align:left}.quick-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.signal-grid{grid-template-columns:1fr}.flow-master{grid-template-columns:1fr}.flow-topbar,.flow-bottom{grid-template-columns:repeat(2,minmax(0,1fr))}.flow-chain{grid-template-columns:repeat(4,minmax(140px,1fr))}} +@media(max-width:640px){.app{padding:12px}.header{flex-direction:column;gap:10px}.header h1{font-size:22px}.badge{width:100%;justify-content:space-between}.badge strong{max-width:52%;overflow:hidden;text-overflow:ellipsis}.quick-grid{grid-template-columns:1fr 1fr;gap:8px}.ctrl-row{flex-direction:column;align-items:stretch}.ctrl-label-wrap{min-width:auto}.ctrl-input{flex-wrap:wrap}.ingest-grid{grid-template-columns:1fr}input[type="number"]{width:100%;text-align:left}.actions-row,.tx-actions{flex-direction:column}.tx-btn,.ghost-btn,.apply-btn,.preset-btn,.danger-btn{width:100%}.freq-display{font-size:31px}.flow-master,.flow-topbar,.flow-bottom,.flow-chain{grid-template-columns:1fr}} @@ -313,11 +323,18 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
-
-
TX State
--
-
Applied / Target
--
-
Source
--
-
Active Alert
--
+
+
+
On-Air Master Status
+
--
+
Waiting for runtime telemetry.
+
+
+
Applied Frequency
--
+
Target Frequency
--
+
Program Source
--
+
Active Alert
--
+
@@ -992,7 +1009,8 @@ function flowNodeData(){const cfg=S.server.config||{},rt=S.server.runtime||{},en function renderFlow(){const chain=$('flow-chain');if(!chain)return;const data=flowNodeData();chain.innerHTML=FLOW_NODES.map(node=>{const d=data[node.key]||{state:'idle',sub:'--',detail:'--'};const sel=S.flowSelected===node.key?' selected':'';return ``;}).join(''); chain.querySelectorAll('[data-flow-node]').forEach(el=>{const key=el.dataset.flowNode;el.addEventListener('mouseenter',e=>showFlowTooltip(key,e.currentTarget));el.addEventListener('mouseleave',hideFlowTooltip);el.addEventListener('focus',e=>showFlowTooltip(key,e.currentTarget));el.addEventListener('blur',hideFlowTooltip);el.addEventListener('click',()=>openFlowPopover(key,el));}); const issue=flowSeverityFromRuntime();const banner=$('flow-banner');if(banner){if(issue){banner.className=`flow-banner show ${issue.sev}`;$('flow-banner-title').textContent=issue.title;$('flow-banner-text').textContent=issue.text;}else{banner.className='flow-banner';$('flow-banner-title').textContent='Status';$('flow-banner-text').textContent='No active issues.';}} - setText('flow-top-state',String((S.server.runtime?.engine?.state||'idle')).toUpperCase());setText('flow-top-frequency',joinParts([isFinite(Number(S.server.runtime?.engine?.appliedFrequencyMHz))?`${Number(S.server.runtime.engine.appliedFrequencyMHz).toFixed(1)} MHz applied`:'Applied --',isFinite(Number(S.server.config?.fm?.frequencyMHz))?`${Number(S.server.config.fm.frequencyMHz).toFixed(1)} MHz target`:'Target --'])||'--');setText('flow-top-source',String(S.server.runtime?.ingest?.active?.kind||S.server.config?.ingest?.kind||'none'));setText('flow-top-alert',issue?issue.text:'None');setText('flow-bottom-queue',String(S.server.runtime?.engine?.queue?.health||'--'));setText('flow-bottom-ingest',String(S.server.runtime?.ingest?.runtime?.state||S.server.runtime?.ingest?.source?.state||'--'));setText('flow-bottom-age',fmtTime(Number(S.server.runtime?.engine?.runtimeStateDurationSeconds)));setText('flow-bottom-update',ageStr(Math.max(S.server.lastConfigAt||0,S.server.lastRuntimeAt||0)));renderFlowPopover(); + const masterState=String((S.server.runtime?.engine?.state||'idle')).toUpperCase();setText('flow-master-state',masterState);const fms=$('flow-master-state');if(fms){fms.className=`flow-master-state ${stateClass(S.server.runtime?.engine?.state)}`;}setText('flow-master-sub',issue?issue.text:(masterState==='RUNNING'?'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(S.server.runtime?.engine?.appliedFrequencyMHz))?`${Number(S.server.runtime.engine.appliedFrequencyMHz).toFixed(1)} MHz`:'--');setText('flow-top-target',isFinite(Number(S.server.config?.fm?.frequencyMHz))?`${Number(S.server.config.fm.frequencyMHz).toFixed(1)} MHz`:'--');setText('flow-top-source',joinParts([String(S.server.runtime?.ingest?.active?.kind||S.server.config?.ingest?.kind||'none'),S.server.runtime?.ingest?.active?.origin?.streamName||S.server.runtime?.ingest?.active?.origin?.endpoint||''])||'--');setText('flow-top-alert',issue?issue.text:'None');setText('flow-bottom-queue',String(S.server.runtime?.engine?.queue?.health||'--'));setText('flow-bottom-ingest',String(S.server.runtime?.ingest?.runtime?.state||S.server.runtime?.ingest?.source?.state||'--'));setText('flow-bottom-age',fmtTime(Number(S.server.runtime?.engine?.runtimeStateDurationSeconds)));setText('flow-bottom-update',ageStr(Math.max(S.server.lastConfigAt||0,S.server.lastRuntimeAt||0)));renderFlowPopover(); } function showFlowTooltip(key,anchor){const tip=$('flow-tooltip');if(!tip||S.flowSelected===key)return;const d=flowNodeData()[key];if(!d)return;tip.innerHTML=`
${FLOW_NODES.find(n=>n.key===key)?.label||key}
${String(d.state||'idle').toUpperCase()}
${(d.lines||[]).map(line=>`
${line}
`).join('')}
`;const r=anchor.getBoundingClientRect();tip.style.left=`${Math.min(window.innerWidth-300,Math.max(12,r.left + window.scrollX))}px`;tip.style.top=`${r.bottom + window.scrollY + 8}px`;tip.classList.add('show');} function hideFlowTooltip(){const tip=$('flow-tooltip');if(tip)tip.classList.remove('show');}