ソースを参照

Elevate flow view into on-air master console

main
Jan 1ヶ月前
コミット
884ad7b384
1個のファイルの変更26行の追加8行の削除
  1. +26
    -8
      internal/control/ui.html

+ 26
- 8
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}}
</style>
</head>
<body>
@@ -313,11 +323,18 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1
<!-- FLOW TAB -->
<section class="tab-panel" data-tab-panel="flow">
<div class="card flow-board">
<div class="flow-topbar">
<div class="flow-summary"><div class="flow-summary-label">TX State</div><div class="flow-summary-value" id="flow-top-state">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Applied / Target</div><div class="flow-summary-value" id="flow-top-frequency">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Source</div><div class="flow-summary-value" id="flow-top-source">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Active Alert</div><div class="flow-summary-value" id="flow-top-alert">--</div></div>
<div class="flow-master">
<div class="flow-master-hero">
<div class="flow-master-label">On-Air Master Status</div>
<div class="flow-master-state idle" id="flow-master-state">--</div>
<div class="flow-master-sub" id="flow-master-sub">Waiting for runtime telemetry.</div>
</div>
<div class="flow-topbar">
<div class="flow-summary"><div class="flow-summary-label">Applied Frequency</div><div class="flow-summary-value" id="flow-top-applied">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Target Frequency</div><div class="flow-summary-value" id="flow-top-target">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Program Source</div><div class="flow-summary-value compact" id="flow-top-source">--</div></div>
<div class="flow-summary"><div class="flow-summary-label">Active Alert</div><div class="flow-summary-value compact" id="flow-top-alert">--</div></div>
</div>
</div>
<div class="flow-chain" id="flow-chain"></div>
<div class="flow-bottom">
@@ -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 `<button type="button" class="flow-node ${d.state}${sel}" data-flow-node="${node.key}"><div class="flow-node-head"><div class="flow-node-icon">${node.icon}</div><div class="flow-node-state"></div></div><div class="flow-node-title">${node.label}</div><div class="flow-node-sub">${d.sub||'--'}</div><div class="flow-node-detail">${d.detail||'--'}</div><div class="flow-node-actions">Status • Details • Quick control</div></button>`;}).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=`<div class="flow-tooltip-title">${FLOW_NODES.find(n=>n.key===key)?.label||key}</div><div class="flow-tooltip-status">${String(d.state||'idle').toUpperCase()}</div><div class="flow-tooltip-lines">${(d.lines||[]).map(line=>`<div>${line}</div>`).join('')}</div>`;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');}


読み込み中…
キャンセル
保存