| @@ -127,19 +127,19 @@ button { user-select: none; } | |||
| } | |||
| .led.on-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 { | |||
| 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 { | |||
| 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 { | |||
| 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 { | |||
| @@ -149,16 +149,61 @@ button { user-select: none; } | |||
| 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; | |||
| 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; } | |||
| .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-radius: var(--radius); | |||
| box-shadow: var(--shadow); | |||
| @@ -166,32 +211,6 @@ button { user-select: none; } | |||
| .hero { | |||
| 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 { | |||
| 0%, 100% { opacity: 1; } | |||
| @@ -221,10 +240,10 @@ button { user-select: none; } | |||
| .freq-display { | |||
| font-family: var(--display); | |||
| 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; | |||
| font-weight: 600; | |||
| } | |||
| .freq-display .unit { | |||
| font-family: var(--mono); | |||
| @@ -925,7 +944,12 @@ input.input-error { | |||
| .toast.warn { background: var(--amber); color: #141414; } | |||
| @media (max-width: 980px) { | |||
| .layout { grid-template-columns: 1fr; } | |||
| .tab-columns.two { | |||
| grid-template-columns: 1fr; | |||
| } | |||
| .tab-bar { | |||
| margin: 0 -12px 18px; | |||
| } | |||
| .tx-bar { | |||
| grid-template-columns: 1fr; | |||
| align-items: stretch; | |||
| @@ -939,6 +963,12 @@ input.input-error { | |||
| } | |||
| @media (max-width: 640px) { | |||
| .tab-columns.two { | |||
| grid-template-columns: 1fr; | |||
| } | |||
| .tab-bar { | |||
| margin: 0 -12px 14px; | |||
| } | |||
| .app { padding: 12px; } | |||
| .header { flex-direction: column; align-items: stretch; gap: 10px; } | |||
| .header h1 { font-size: 22px; } | |||
| @@ -978,8 +1008,18 @@ input.input-error { | |||
| </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="tx-bar"> | |||
| <div class="freq-display-wrap"> | |||
| @@ -1054,6 +1094,30 @@ input.input-error { | |||
| </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="panel-head" data-panel> | |||
| <div class="led on-green" style="width:6px;height:6px"></div> | |||
| @@ -1091,6 +1155,7 @@ input.input-error { | |||
| </div> | |||
| </div> | |||
| <div class="card panel" data-panel-key="switches"> | |||
| <div class="panel-head" data-panel> | |||
| <div class="led on-green" style="width:6px;height:6px"></div> | |||
| @@ -1136,6 +1201,7 @@ input.input-error { | |||
| </div> | |||
| </div> | |||
| <div class="card panel" data-panel-key="rds"> | |||
| <div class="panel-head" data-panel> | |||
| <div class="led on-amber" style="width:6px;height:6px"></div> | |||
| @@ -1173,61 +1239,9 @@ input.input-error { | |||
| </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 class="stack"> | |||
| <div class="card panel" data-panel-key="shortcuts"> | |||
| <div class="panel-head" data-panel> | |||
| <h2>Shortcuts</h2> | |||
| @@ -1251,6 +1265,7 @@ input.input-error { | |||
| </div> | |||
| </div> | |||
| <div class="card panel" data-panel-key="danger"> | |||
| <div class="panel-head" data-panel> | |||
| <h2>Danger Zone</h2> | |||
| @@ -1271,6 +1286,58 @@ input.input-error { | |||
| </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="panel-head" data-panel> | |||
| <h2>Transition History</h2> | |||
| @@ -1285,6 +1352,7 @@ input.input-error { | |||
| </div> | |||
| </div> | |||
| <div class="card panel" data-panel-key="fault-history"> | |||
| <div class="panel-head" data-panel> | |||
| <h2>Fault History</h2> | |||
| @@ -1299,6 +1367,14 @@ input.input-error { | |||
| </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="panel-head" data-panel> | |||
| <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> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </section> | |||
| </div> | |||
| </div> | |||