Przeglądaj źródła

feat(ui): rework control plane layout

main
Jan 1 miesiąc temu
rodzic
commit
1d8eb1e856
1 zmienionych plików z 142 dodań i 125 usunięć
  1. +142
    -125
      internal/control/ui.html

+ 142
- 125
internal/control/ui.html Wyświetl plik

@@ -5,63 +5,52 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>fm-rds-tx</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Archivo+Black&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap');

:root {
--bg: #0a0a0c;
--bg-2: #0f1015;
--surface: #111116;
--surface2: #18181e;
--surface3: #1f2028;
--border: #2a2a35;
--border-strong: #3a3a49;
--text: #d4d4dc;
--text-dim: #8b8b99;
--text-muted: #666674;
--accent: #ff3b30;
--accent-glow: #ff3b3044;
--green: #30d158;
--green-glow: #30d15844;
--amber: #ff9f0a;
--amber-glow: #ff9f0a44;
--blue: #0a84ff;
--blue-glow: #0a84ff33;
--bg: #f7f8fb;
--bg-2: #eef0f3;
--surface: #ffffff;
--surface2: #f4f6f8;
--surface3: #e8ecf1;
--border: #d7dcdf;
--border-strong: #bcc5ce;
--text: #111724;
--text-dim: #4c5a6a;
--text-muted: #6b7683;
--accent: #1f4d9d;
--accent-soft: rgba(31, 77, 157, 0.08);
--green: #0d944a;
--green-soft: rgba(13, 148, 74, 0.1);
--amber: #b7791f;
--amber-soft: rgba(183, 121, 31, 0.12);
--red: #b03030;
--red-soft: rgba(176, 48, 48, 0.1);
--mono: 'JetBrains Mono', monospace;
--display: 'Archivo Black', sans-serif;
--display: 'Inter', sans-serif;
--radius: 8px;
--shadow: 0 10px 30px rgba(0,0,0,.25);
--shadow: 0 8px 24px rgba(15,23,42,0.08);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { color-scheme: dark; }
html { color-scheme: light; }
body {
background:
radial-gradient(circle at top right, rgba(10,132,255,.06), transparent 28%),
radial-gradient(circle at top left, rgba(255,59,48,.06), transparent 30%),
var(--bg);
background: linear-gradient(180deg, #fbfcfe 0%, var(--bg) 100%);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
font-family: var(--display);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
overflow-x: hidden;
}

body::before {
content: '';
position: fixed; inset: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px);
pointer-events: none;
z-index: 1000;
}

button, input { font: inherit; }
button { user-select: none; }

.app {
max-width: 1120px;
max-width: 1200px;
margin: 0 auto;
padding: 18px;
padding: 24px;
}

.header {
@@ -69,9 +58,9 @@ button { user-select: none; }
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: 8px 0 22px;
padding: 4px 0 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 18px;
margin-bottom: 20px;
}
.header-main {
display: flex;
@@ -80,11 +69,15 @@ button { user-select: none; }
}
.header h1 {
font-family: var(--display);
font-size: 24px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--accent);
text-shadow: 0 0 20px var(--accent-glow), 0 0 40px var(--accent-glow);
font-size: 28px;
font-weight: 800;
letter-spacing: -0.03em;
text-transform: none;
color: var(--text);
}
.header-note {
font-size: 13px;
color: var(--text-muted);
}
.header-sub {
display: flex;
@@ -99,11 +92,11 @@ button { user-select: none; }
padding: 0 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(255,255,255,0.02);
background: var(--surface2);
color: var(--text-dim);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .8px;
letter-spacing: .08em;
}
.badge strong {
color: var(--text);
@@ -127,50 +120,56 @@ button { user-select: none; }
}
.led.on-green {
background: var(--green);
box-shadow: 0 0 10px rgba(46, 125, 50, 0.45);
box-shadow: 0 0 0 3px rgba(13,148,74,0.16);
}
.led.on-red {
background: var(--accent);
box-shadow: 0 0 10px rgba(180, 35, 24, 0.45);
background: var(--red);
box-shadow: 0 0 0 3px rgba(176,48,48,0.14);
}
.led.on-amber {
background: var(--amber);
box-shadow: 0 0 10px rgba(178, 106, 0, 0.45);
box-shadow: 0 0 0 3px rgba(183,121,31,0.14);
}
.led.on-blue {
background: var(--blue);
box-shadow: 0 0 10px rgba(41, 98, 179, 0.45);
background: var(--accent);
box-shadow: 0 0 0 3px rgba(31,77,157,0.14);
}

.status-text {
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1.2px;
letter-spacing: .08em;
}

.tab-bar {
display: flex;
gap: 10px;
gap: 6px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
margin: 0 -24px 18px;
padding-bottom: 0;
margin: 0 0 20px;
flex-wrap: wrap;
position: sticky;
top: 0;
background: rgba(247,248,251,.92);
backdrop-filter: blur(8px);
z-index: 10;
}
.tab-btn {
border: 1px solid var(--border);
border-radius: var(--radius);
border: none;
border-bottom: 3px solid transparent;
border-radius: 0;
background: transparent;
color: var(--text-dim);
font-size: 12px;
font-weight: 600;
padding: 8px 18px;
font-size: 13px;
font-weight: 700;
padding: 12px 16px 11px;
cursor: pointer;
transition: all .16s ease;
transition: color .16s ease, border-color .16s ease, background-color .16s ease;
}
.tab-btn.active {
border-color: var(--accent);
background: var(--surface);
border-bottom-color: var(--accent);
background: transparent;
color: var(--accent);
}
.tab-btn:focus-visible {
@@ -212,10 +211,6 @@ button { user-select: none; }
.hero {
padding: 16px;
}
@keyframes blinkSoft {
0%, 100% { opacity: 1; }
50% { opacity: .55; }
}

.tx-bar {
position: relative;
@@ -234,16 +229,16 @@ button { user-select: none; }
.freq-display-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.4px;
letter-spacing: .08em;
color: var(--text-dim);
}
.freq-display {
font-family: var(--display);
font-size: 38px;
font-size: 40px;
color: var(--text);
letter-spacing: 0.6px;
letter-spacing: -0.04em;
line-height: 1;
font-weight: 600;
font-weight: 800;
}
.freq-display .unit {
font-family: var(--mono);
@@ -259,7 +254,7 @@ button { user-select: none; }
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: .08em;
}
.freq-note-item {
display: inline-flex;
@@ -287,7 +282,7 @@ button { user-select: none; }
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: .08em;
transition: all .16s ease;
}
.tx-btn:hover, .ghost-btn:hover, .apply-btn:hover, .preset-btn:hover, .danger-btn:hover {
@@ -299,18 +294,18 @@ button { user-select: none; }
cursor: not-allowed;
transform: none;
}
.tx-btn.start { border-color: var(--green); color: var(--green); }
.tx-btn.start:hover:not(:disabled) { background: rgba(48,209,88,.1); }
.tx-btn.stop { border-color: var(--accent); color: var(--accent); }
.tx-btn.stop:hover:not(:disabled) { background: rgba(255,59,48,.1); }
.tx-btn.start { background: var(--green); color: #fff; border-color: transparent; }
.tx-btn.start:hover:not(:disabled) { background: #0b7f40; }
.tx-btn.stop { background: #fff; color: var(--red); border-color: rgba(176,48,48,.35); }
.tx-btn.stop:hover:not(:disabled) { background: var(--red-soft); }
.ghost-btn { color: var(--text-dim); }
.danger-btn {
border-color: rgba(255,59,48,.45);
color: var(--accent);
background: rgba(255,59,48,.04);
border-color: rgba(176,48,48,.35);
color: var(--red);
background: var(--red-soft);
}
.danger-btn:hover:not(:disabled) {
background: rgba(255,59,48,.12);
background: rgba(176,48,48,.16);
}

.tx-state-wrap {
@@ -325,10 +320,10 @@ button { user-select: none; }
letter-spacing: 2px;
color: var(--text-dim);
}
.tx-state.running { color: var(--green); animation: blinkSoft 2s ease-in-out infinite; }
.tx-state.running { color: var(--green); }
.tx-state.idle, .tx-state.stopped { color: var(--text-dim); }
.tx-state.starting, .tx-state.stopping, .tx-state.working { color: var(--amber); animation: blinkSoft 1.1s ease-in-out infinite; }
.tx-state.error { color: var(--accent); }
.tx-state.starting, .tx-state.stopping, .tx-state.working { color: var(--amber); }
.tx-state.error { color: var(--red); }
.status-hint {
font-size: 10px;
color: var(--text-muted);
@@ -347,12 +342,12 @@ button { user-select: none; }
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-2);
background: var(--surface2);
}
.quick-item .label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1.4px;
letter-spacing: .08em;
color: var(--text-dim);
margin-bottom: 6px;
}
@@ -377,7 +372,7 @@ button { user-select: none; }
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-2);
background: var(--surface2);
}
.signal-head {
display: flex;
@@ -390,7 +385,7 @@ button { user-select: none; }
font-size: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1.2px;
letter-spacing: .08em;
}
.signal-value {
font-size: 11px;
@@ -401,7 +396,7 @@ button { user-select: none; }
width: 100%;
height: 10px;
border-radius: 999px;
background: #171821;
background: #e7ebf0;
border: 1px solid var(--border);
overflow: hidden;
}
@@ -409,21 +404,21 @@ button { user-select: none; }
height: 100%;
width: 0%;
transition: width .25s ease, background-color .25s ease;
background: linear-gradient(90deg, var(--green), #5cff90);
background: linear-gradient(90deg, var(--green), #3dbd75);
}
.meter-fill.warn {
background: linear-gradient(90deg, var(--amber), #ffc45b);
background: linear-gradient(90deg, var(--amber), #d9a14a);
}
.meter-fill.err {
background: linear-gradient(90deg, var(--accent), #ff6b63);
background: linear-gradient(90deg, var(--red), #d85c5c);
}
.spark {
width: 100%;
height: 34px;
margin-top: 10px;
border-radius: 6px;
background: rgba(255,255,255,0.01);
border: 1px solid rgba(255,255,255,0.03);
background: #f7f9fb;
border: 1px solid #e3e8ee;
}
.spark path.line {
fill: none;
@@ -494,7 +489,7 @@ button { user-select: none; }
justify-content: space-between;
gap: 12px;
padding: 7px 0;
border-bottom: 1px solid #1a1a22;
border-bottom: 1px solid #e6eaef;
}
.shortcut-line:last-child { border-bottom: none; }
.shortcut-line .name { font-size: 11px; color: var(--text-dim); }
@@ -509,7 +504,7 @@ button { user-select: none; }
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: 6px;
background: var(--bg-2);
background: var(--surface2);
font-size: 10px;
color: var(--text);
text-align: center;
@@ -525,13 +520,13 @@ button { user-select: none; }
min-height: 34px;
padding: 0 12px;
font-size: 11px;
letter-spacing: .8px;
letter-spacing: .08em;
color: var(--text-dim);
}
.preset-btn.active {
border-color: var(--blue);
color: var(--blue);
background: rgba(10,132,255,.08);
border-color: var(--accent);
color: var(--accent);
background: var(--accent-soft);
}
.preset-btn.rds {
text-transform: none;
@@ -543,7 +538,7 @@ button { user-select: none; }
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #1a1a22;
border-bottom: 1px solid #e6eaef;
}
.ctrl-row:last-child { border-bottom: none; }
.ctrl-label-wrap {
@@ -593,7 +588,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.06);
}
input[type="number"], input[type="text"] {
background: var(--bg);
background: #fff;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
@@ -608,16 +603,16 @@ input[type="number"] {
input[type="text"] { width: 100%; }
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(255,59,48,.12);
box-shadow: 0 0 0 3px rgba(31,77,157,.12);
}
input.input-dirty {
border-color: var(--amber);
box-shadow: 0 0 0 3px rgba(255,159,10,.08);
box-shadow: 0 0 0 3px rgba(183,121,31,.08);
}
input.input-error {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(255,59,48,.14);
background: rgba(255,59,48,.04);
border-color: var(--red);
box-shadow: 0 0 0 3px rgba(176,48,48,.14);
background: rgba(176,48,48,.04);
}
.val-display {
min-width: 64px;
@@ -711,10 +706,10 @@ input.input-error {
}
.rds-input {
width: 100%;
background: var(--bg);
background: #fff;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--green);
color: var(--accent);
font-family: var(--mono);
font-size: 15px;
font-weight: 700;
@@ -765,7 +760,7 @@ input.input-error {
.sidebar-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.4px;
letter-spacing: .08em;
color: var(--text-dim);
margin-bottom: 10px;
}
@@ -779,7 +774,7 @@ input.input-error {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: .08em;
}
.kv .v {
font-size: 12px;
@@ -813,7 +808,7 @@ input.input-error {
.health-trend-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: .08em;
color: var(--text-muted);
margin-bottom: 6px;
}
@@ -823,7 +818,7 @@ input.input-error {
padding: 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface1);
background: #fbfcfd;
font-size: 11px;
max-height: 180px;
overflow-y: auto;
@@ -902,11 +897,11 @@ input.input-error {
}

.log {
background: var(--bg);
background: #fbfcfd;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
font-size: 10px;
font-size: 11px;
color: var(--text-dim);
max-height: 220px;
overflow-y: auto;
@@ -995,7 +990,8 @@ input.input-error {
<div class="app">
<div class="header">
<div class="header-main">
<h1>FM-RDS-TX</h1>
<h1>FM-RDS-TX Control Plane</h1>
<div class="header-note">Overview first, controls second, diagnostics when needed.</div>
<div class="header-sub">
<div class="badge"><span>Backend</span><strong id="badge-backend">--</strong></div>
<div class="badge"><span>Mode</span><strong id="badge-mode">Control Plane</strong></div>
@@ -1011,9 +1007,9 @@ input.input-error {

<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>
<button class="tab-btn" data-tab="control" type="button">Transmission Control</button>
<button class="tab-btn" data-tab="diagnostics" type="button">Diagnostics &amp; Health</button>
<button class="tab-btn" data-tab="activity" type="button">Activity &amp; Logs</button>
</div>

<div class="tab-panels">
@@ -1099,16 +1095,24 @@ input.input-error {
<div class="stack">
<div class="card sidebar-card">
<div class="sidebar-section">
<div class="sidebar-title">System Snapshot</div>
<div class="sidebar-title">Runtime 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">Runtime Age</div><div class="v" id="info-runtime-age">--</div>
<div class="k">Last Alert</div><div class="v" id="info-last-alert">--</div>
<div class="k">Live Config</div><div class="v" id="info-live">--</div>
</div>
</div>

<div class="sidebar-section">
<div class="sidebar-title">Signal Notes</div>
<div class="section-note">Overview stays compact: primary state here, deep diagnostics in the dedicated tab.</div>
<div class="kv">
<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>
</div>

</div>
</div>
@@ -1249,7 +1253,7 @@ input.input-error {
<span class="chevron">▼</span>
</div>
<div class="panel-body">
<div class="section-note">Fast control, as long as you're not typing in an input field.</div>
<div class="section-note">Fast control reference. Shortcuts stay out of the main operator path.</div>
<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>
@@ -2097,6 +2101,8 @@ function render() {
updateText('info-freq', fmtFreq(cfg.fm?.frequencyMHz));
updateText('info-preemph', cfg.fm?.preEmphasisTauUS ? `${cfg.fm.preEmphasisTauUS} µs` : 'Off');
updateText('info-fmmod', fmtBool(cfg.fm?.fmModulationEnabled));
updateText('info-runtime-age', ageString(state.server.lastRuntimeAt));
updateText('info-last-alert', engine.runtimeAlert ? engine.runtimeAlert : (engine.lastError ? engine.lastError : 'None'));
updateText('info-live', engine.state ? `${String(engine.state).toUpperCase()} / ${state.server.runtimeOk ? 'runtime ok' : 'runtime pending'}` : (state.server.configOk ? 'config only' : '--'));

updateHealth(engine, driver, audioStream);
@@ -2486,6 +2492,16 @@ function log(message, type = '') {
logEl.scrollTop = logEl.scrollHeight;
}

function bindTabs() {
const buttons = Array.from(document.querySelectorAll('.tab-btn[data-tab]'));
const panels = Array.from(document.querySelectorAll('.tab-panel[data-tab-panel]'));
const activate = (tab) => {
buttons.forEach((btn) => btn.classList.toggle('active', btn.dataset.tab === tab));
panels.forEach((panel) => panel.classList.toggle('active', panel.dataset.tabPanel === tab));
};
buttons.forEach((btn) => btn.addEventListener('click', () => activate(btn.dataset.tab)));
}

function bindPanels() {
document.querySelectorAll('[data-panel]').forEach((head) => {
head.addEventListener('click', () => {
@@ -2645,6 +2661,7 @@ function bindKeyboardShortcuts() {
}

async function init() {
bindTabs();
bindPanels();
bindInputs();
bindResponsiveBehavior();


Ładowanie…
Anuluj
Zapisz