| @@ -127,19 +127,19 @@ button { user-select: none; } | |||||
| } | } | ||||
| .led.on-green { | .led.on-green { | ||||
| background: var(--green); | background: var(--green); | ||||
| box-shadow: 0 0 8px var(--green), 0 0 20px var(--green-glow); | |||||
| box-shadow: 0 0 10px rgba(46, 125, 50, 0.45); | |||||
| } | } | ||||
| .led.on-red { | .led.on-red { | ||||
| background: var(--accent); | background: var(--accent); | ||||
| box-shadow: 0 0 8px var(--accent), 0 0 20px var(--accent-glow); | |||||
| box-shadow: 0 0 10px rgba(180, 35, 24, 0.45); | |||||
| } | } | ||||
| .led.on-amber { | .led.on-amber { | ||||
| background: var(--amber); | background: var(--amber); | ||||
| box-shadow: 0 0 8px var(--amber), 0 0 20px var(--amber-glow); | |||||
| box-shadow: 0 0 10px rgba(178, 106, 0, 0.45); | |||||
| } | } | ||||
| .led.on-blue { | .led.on-blue { | ||||
| background: var(--blue); | background: var(--blue); | ||||
| box-shadow: 0 0 8px var(--blue), 0 0 20px var(--blue-glow); | |||||
| box-shadow: 0 0 10px rgba(41, 98, 179, 0.45); | |||||
| } | } | ||||
| .status-text { | .status-text { | ||||
| @@ -149,16 +149,61 @@ button { user-select: none; } | |||||
| letter-spacing: 1.2px; | letter-spacing: 1.2px; | ||||
| } | } | ||||
| .layout { | |||||
| display: grid; | |||||
| grid-template-columns: minmax(0, 1.35fr) minmax(310px, .75fr); | |||||
| .tab-bar { | |||||
| display: flex; | |||||
| gap: 10px; | |||||
| border-bottom: 1px solid var(--border); | |||||
| padding-bottom: 10px; | |||||
| margin: 0 -24px 18px; | |||||
| flex-wrap: wrap; | |||||
| } | |||||
| .tab-btn { | |||||
| border: 1px solid var(--border); | |||||
| border-radius: var(--radius); | |||||
| background: transparent; | |||||
| color: var(--text-dim); | |||||
| font-size: 12px; | |||||
| font-weight: 600; | |||||
| padding: 8px 18px; | |||||
| cursor: pointer; | |||||
| transition: all .16s ease; | |||||
| } | |||||
| .tab-btn.active { | |||||
| border-color: var(--accent); | |||||
| background: var(--surface); | |||||
| color: var(--accent); | |||||
| } | |||||
| .tab-btn:focus-visible { | |||||
| outline: 2px solid var(--accent); | |||||
| outline-offset: 2px; | |||||
| } | |||||
| .tab-panels { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| gap: 14px; | gap: 14px; | ||||
| align-items: start; | |||||
| } | } | ||||
| .tab-panel { | |||||
| display: none; | |||||
| } | |||||
| .tab-panel.active { | |||||
| display: block; | |||||
| } | |||||
| .tab-columns { | |||||
| display: grid; | |||||
| gap: 16px; | |||||
| } | |||||
| .tab-columns.two { | |||||
| grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.85fr); | |||||
| } | |||||
| .tab-columns.one { | |||||
| grid-template-columns: 1fr; | |||||
| } | |||||
| .stack { display: flex; flex-direction: column; gap: 12px; } | .stack { display: flex; flex-direction: column; gap: 12px; } | ||||
| .card { | .card { | ||||
| background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); | |||||
| background: var(--surface); | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: var(--radius); | border-radius: var(--radius); | ||||
| box-shadow: var(--shadow); | box-shadow: var(--shadow); | ||||
| @@ -166,32 +211,6 @@ button { user-select: none; } | |||||
| .hero { | .hero { | ||||
| padding: 16px; | padding: 16px; | ||||
| position: relative; | |||||
| overflow: hidden; | |||||
| } | |||||
| .hero.tx-live::after { | |||||
| content: ''; | |||||
| position: absolute; | |||||
| inset: -40%; | |||||
| background: radial-gradient(circle, rgba(48,209,88,.12), transparent 55%); | |||||
| animation: pulseGlow 2.8s ease-in-out infinite; | |||||
| pointer-events: none; | |||||
| } | |||||
| .hero.tx-busy::after { | |||||
| content: ''; | |||||
| position: absolute; | |||||
| inset: -50%; | |||||
| background: conic-gradient(from 0deg, transparent, rgba(255,159,10,.12), transparent 45%); | |||||
| animation: spinWash 2s linear infinite; | |||||
| pointer-events: none; | |||||
| } | |||||
| @keyframes pulseGlow { | |||||
| 0%, 100% { transform: scale(.95); opacity: .45; } | |||||
| 50% { transform: scale(1.05); opacity: .8; } | |||||
| } | |||||
| @keyframes spinWash { | |||||
| from { transform: rotate(0deg); } | |||||
| to { transform: rotate(360deg); } | |||||
| } | } | ||||
| @keyframes blinkSoft { | @keyframes blinkSoft { | ||||
| 0%, 100% { opacity: 1; } | 0%, 100% { opacity: 1; } | ||||
| @@ -221,10 +240,10 @@ button { user-select: none; } | |||||
| .freq-display { | .freq-display { | ||||
| font-family: var(--display); | font-family: var(--display); | ||||
| font-size: 38px; | font-size: 38px; | ||||
| color: var(--green); | |||||
| text-shadow: 0 0 15px var(--green-glow); | |||||
| letter-spacing: 1px; | |||||
| color: var(--text); | |||||
| letter-spacing: 0.6px; | |||||
| line-height: 1; | line-height: 1; | ||||
| font-weight: 600; | |||||
| } | } | ||||
| .freq-display .unit { | .freq-display .unit { | ||||
| font-family: var(--mono); | font-family: var(--mono); | ||||
| @@ -925,7 +944,12 @@ input.input-error { | |||||
| .toast.warn { background: var(--amber); color: #141414; } | .toast.warn { background: var(--amber); color: #141414; } | ||||
| @media (max-width: 980px) { | @media (max-width: 980px) { | ||||
| .layout { grid-template-columns: 1fr; } | |||||
| .tab-columns.two { | |||||
| grid-template-columns: 1fr; | |||||
| } | |||||
| .tab-bar { | |||||
| margin: 0 -12px 18px; | |||||
| } | |||||
| .tx-bar { | .tx-bar { | ||||
| grid-template-columns: 1fr; | grid-template-columns: 1fr; | ||||
| align-items: stretch; | align-items: stretch; | ||||
| @@ -939,6 +963,12 @@ input.input-error { | |||||
| } | } | ||||
| @media (max-width: 640px) { | @media (max-width: 640px) { | ||||
| .tab-columns.two { | |||||
| grid-template-columns: 1fr; | |||||
| } | |||||
| .tab-bar { | |||||
| margin: 0 -12px 14px; | |||||
| } | |||||
| .app { padding: 12px; } | .app { padding: 12px; } | ||||
| .header { flex-direction: column; align-items: stretch; gap: 10px; } | .header { flex-direction: column; align-items: stretch; gap: 10px; } | ||||
| .header h1 { font-size: 22px; } | .header h1 { font-size: 22px; } | ||||
| @@ -978,8 +1008,18 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="layout"> | |||||
| <div class="stack"> | |||||
| <div class="tab-bar"> | |||||
| <button class="tab-btn active" data-tab="overview" type="button">Overview</button> | |||||
| <button class="tab-btn" data-tab="control" type="button">Control</button> | |||||
| <button class="tab-btn" data-tab="diagnostics" type="button">Diagnostics</button> | |||||
| <button class="tab-btn" data-tab="activity" type="button">Activity</button> | |||||
| </div> | |||||
| <div class="tab-panels"> | |||||
| <section class="tab-panel active" data-tab-panel="overview"> | |||||
| <div class="tab-columns two"> | |||||
| <div class="stack"> | |||||
| <div class="card hero" id="hero-card"> | <div class="card hero" id="hero-card"> | ||||
| <div class="tx-bar"> | <div class="tx-bar"> | ||||
| <div class="freq-display-wrap"> | <div class="freq-display-wrap"> | ||||
| @@ -1054,6 +1094,30 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="stack"> | |||||
| <div class="card sidebar-card"> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">System Snapshot</div> | |||||
| <div class="kv"> | |||||
| <div class="k">Backend</div><div class="v" id="info-backend">--</div> | |||||
| <div class="k">Frequency</div><div class="v" id="info-freq">--</div> | |||||
| <div class="k">Pre-emphasis</div><div class="v" id="info-preemph">--</div> | |||||
| <div class="k">FM Mod</div><div class="v" id="info-fmmod">--</div> | |||||
| <div class="k">Live Config</div><div class="v" id="info-live">--</div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| <section class="tab-panel" data-tab-panel="control"> | |||||
| <div class="tab-columns two"> | |||||
| <div class="stack"> | |||||
| <div class="card panel" data-panel-key="frequency"> | <div class="card panel" data-panel-key="frequency"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <div class="led on-green" style="width:6px;height:6px"></div> | <div class="led on-green" style="width:6px;height:6px"></div> | ||||
| @@ -1091,6 +1155,7 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="card panel" data-panel-key="switches"> | <div class="card panel" data-panel-key="switches"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <div class="led on-green" style="width:6px;height:6px"></div> | <div class="led on-green" style="width:6px;height:6px"></div> | ||||
| @@ -1136,6 +1201,7 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="card panel" data-panel-key="rds"> | <div class="card panel" data-panel-key="rds"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <div class="led on-amber" style="width:6px;height:6px"></div> | <div class="led on-amber" style="width:6px;height:6px"></div> | ||||
| @@ -1173,61 +1239,9 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="stack"> | |||||
| <div class="card sidebar-card"> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">System Snapshot</div> | |||||
| <div class="kv"> | |||||
| <div class="k">Backend</div><div class="v" id="info-backend">--</div> | |||||
| <div class="k">Frequency</div><div class="v" id="info-freq">--</div> | |||||
| <div class="k">Pre-emphasis</div><div class="v" id="info-preemph">--</div> | |||||
| <div class="k">FM Mod</div><div class="v" id="info-fmmod">--</div> | |||||
| <div class="k">Live Config</div><div class="v" id="info-live">--</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">Health</div> | |||||
| <div class="health-line"><div class="name">HTTP</div><div class="val" id="health-http">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime</div><div class="val" id="health-runtime">--</div></div> | |||||
| <div class="health-line"><div class="name">State Age</div><div class="val" id="health-state-age">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime Signal</div><div class="val" id="health-indicator">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime Alert</div><div class="val" id="health-alert">--</div></div> | |||||
| <div class="health-line"><div class="name">Transitions (D/M/F)</div><div class="val" id="health-transitions">--</div></div> | |||||
| <div class="health-line"><div class="name">Fault Count</div><div class="val" id="health-fault-count">--</div></div> | |||||
| <div class="health-line"><div class="name">Last Fault</div><div class="val" id="health-last-fault">--</div></div> | |||||
| <div class="health-line"><div class="name">Audio Buffer</div><div class="val" id="health-audio">--</div></div> | |||||
| <div class="health-line"><div class="name">Buffer Duration</div><div class="val" id="health-buffer-duration">--</div></div> | |||||
| <div class="health-line"><div class="name">High Watermark</div><div class="val" id="health-buffer-highwater">--</div></div> | |||||
| <div class="health-line"><div class="name">Queue Fill</div><div class="val" id="health-queue-fill">--</div></div> | |||||
| <div class="health-line"><div class="name">Underrun Streak</div><div class="val" id="health-underrun-streak">--</div></div> | |||||
| <div class="health-line"><div class="name">Last Update</div><div class="val" id="health-last">--</div></div> | |||||
| <div class="health-trend"> | |||||
| <div class="health-trend-label">High Watermark Trend</div> | |||||
| <svg class="spark warn" id="spark-high-watermark" viewBox="0 0 160 34" preserveAspectRatio="none"></svg> | |||||
| </div> | |||||
| <div class="health-trend"> | |||||
| <div class="health-trend-label">Queue Fill Trend</div> | |||||
| <svg class="spark good" id="spark-queue-fill" viewBox="0 0 160 34" preserveAspectRatio="none"></svg> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">Control Audit</div> | |||||
| <div class="section-note">Counts of 4xx rejects recorded by the control plane APIs.</div> | |||||
| <div class="kv"> | |||||
| <div class="k">Rejects total</div><div class="v" id="audit-total">--</div> | |||||
| <div class="k">405 Method Not Allowed</div><div class="v" id="audit-methodNotAllowed">--</div> | |||||
| <div class="k">415 Unsupported Media Type</div><div class="v" id="audit-unsupportedMediaType">--</div> | |||||
| <div class="k">413 Request Too Large</div><div class="v" id="audit-bodyTooLarge">--</div> | |||||
| <div class="k">400 Unexpected Body</div><div class="v" id="audit-unexpectedBody">--</div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div class="stack"> | |||||
| <div class="card panel" data-panel-key="shortcuts"> | <div class="card panel" data-panel-key="shortcuts"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <h2>Shortcuts</h2> | <h2>Shortcuts</h2> | ||||
| @@ -1251,6 +1265,7 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="card panel" data-panel-key="danger"> | <div class="card panel" data-panel-key="danger"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <h2>Danger Zone</h2> | <h2>Danger Zone</h2> | ||||
| @@ -1271,6 +1286,58 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| <section class="tab-panel" data-tab-panel="diagnostics"> | |||||
| <div class="tab-columns two"> | |||||
| <div class="stack"> | |||||
| <div class="card sidebar-card"> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">Health</div> | |||||
| <div class="health-line"><div class="name">HTTP</div><div class="val" id="health-http">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime</div><div class="val" id="health-runtime">--</div></div> | |||||
| <div class="health-line"><div class="name">State Age</div><div class="val" id="health-state-age">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime Signal</div><div class="val" id="health-indicator">--</div></div> | |||||
| <div class="health-line"><div class="name">Runtime Alert</div><div class="val" id="health-alert">--</div></div> | |||||
| <div class="health-line"><div class="name">Transitions (D/M/F)</div><div class="val" id="health-transitions">--</div></div> | |||||
| <div class="health-line"><div class="name">Fault Count</div><div class="val" id="health-fault-count">--</div></div> | |||||
| <div class="health-line"><div class="name">Last Fault</div><div class="val" id="health-last-fault">--</div></div> | |||||
| <div class="health-line"><div class="name">Audio Buffer</div><div class="val" id="health-audio">--</div></div> | |||||
| <div class="health-line"><div class="name">Buffer Duration</div><div class="val" id="health-buffer-duration">--</div></div> | |||||
| <div class="health-line"><div class="name">High Watermark</div><div class="val" id="health-buffer-highwater">--</div></div> | |||||
| <div class="health-line"><div class="name">Queue Fill</div><div class="val" id="health-queue-fill">--</div></div> | |||||
| <div class="health-line"><div class="name">Underrun Streak</div><div class="val" id="health-underrun-streak">--</div></div> | |||||
| <div class="health-line"><div class="name">Last Update</div><div class="val" id="health-last">--</div></div> | |||||
| <div class="health-trend"> | |||||
| <div class="health-trend-label">High Watermark Trend</div> | |||||
| <svg class="spark warn" id="spark-high-watermark" viewBox="0 0 160 34" preserveAspectRatio="none"></svg> | |||||
| </div> | |||||
| <div class="health-trend"> | |||||
| <div class="health-trend-label">Queue Fill Trend</div> | |||||
| <svg class="spark good" id="spark-queue-fill" viewBox="0 0 160 34" preserveAspectRatio="none"></svg> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="card sidebar-card"> | |||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">Control Audit</div> | |||||
| <div class="section-note">Counts of 4xx rejects recorded by the control plane APIs.</div> | |||||
| <div class="kv"> | |||||
| <div class="k">Rejects total</div><div class="v" id="audit-total">--</div> | |||||
| <div class="k">405 Method Not Allowed</div><div class="v" id="audit-methodNotAllowed">--</div> | |||||
| <div class="k">415 Unsupported Media Type</div><div class="v" id="audit-unsupportedMediaType">--</div> | |||||
| <div class="k">413 Request Too Large</div><div class="v" id="audit-bodyTooLarge">--</div> | |||||
| <div class="k">400 Unexpected Body</div><div class="v" id="audit-unexpectedBody">--</div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="stack"> | |||||
| <div class="card panel" data-panel-key="transition-history"> | <div class="card panel" data-panel-key="transition-history"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <h2>Transition History</h2> | <h2>Transition History</h2> | ||||
| @@ -1285,6 +1352,7 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="card panel" data-panel-key="fault-history"> | <div class="card panel" data-panel-key="fault-history"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <h2>Fault History</h2> | <h2>Fault History</h2> | ||||
| @@ -1299,6 +1367,14 @@ input.input-error { | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| <section class="tab-panel" data-tab-panel="activity"> | |||||
| <div class="tab-columns one"> | |||||
| <div class="stack"> | |||||
| <div class="card panel" data-panel-key="log"> | <div class="card panel" data-panel-key="log"> | ||||
| <div class="panel-head" data-panel> | <div class="panel-head" data-panel> | ||||
| <h2>Activity Log</h2> | <h2>Activity Log</h2> | ||||
| @@ -1312,7 +1388,9 @@ input.input-error { | |||||
| <div class="log" id="log"><div class="empty-log">No events yet.</div></div> | <div class="log" id="log"><div class="empty-log">No events yet.</div></div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||