[feat](trx-frontend-http): align overview strip with spectrum view
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -544,6 +544,20 @@ function trimOverviewWaterfallRows() {
|
||||
}
|
||||
}
|
||||
|
||||
function overviewVisibleBinWindow(data, binCount) {
|
||||
if (!data || !Number.isFinite(data.sample_rate) || binCount <= 1) {
|
||||
return { startIdx: 0, endIdx: Math.max(0, binCount - 1) };
|
||||
}
|
||||
const range = spectrumVisibleRange(data);
|
||||
const fullLoHz = data.center_hz - data.sample_rate / 2;
|
||||
const startFrac = (range.visLoHz - fullLoHz) / data.sample_rate;
|
||||
const endFrac = (range.visHiHz - fullLoHz) / data.sample_rate;
|
||||
const maxIdx = binCount - 1;
|
||||
const startIdx = Math.max(0, Math.min(maxIdx, Math.floor(startFrac * maxIdx)));
|
||||
const endIdx = Math.max(startIdx, Math.min(maxIdx, Math.ceil(endFrac * maxIdx)));
|
||||
return { startIdx, endIdx };
|
||||
}
|
||||
|
||||
function pushOverviewWaterfallFrame(data) {
|
||||
if (!overviewCanvas || !data || !Array.isArray(data.bins) || data.bins.length === 0) return;
|
||||
overviewWaterfallRows.push(data.bins.slice());
|
||||
@@ -585,14 +599,18 @@ function drawOverviewWaterfall(ctx, w, h, isLight) {
|
||||
const rows = overviewWaterfallRows.slice(-Math.max(1, Math.floor(h)));
|
||||
if (rows.length === 0) return;
|
||||
const rowH = h / rows.length;
|
||||
const columnStep = Math.max(1, Math.ceil(w / 320));
|
||||
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||
const bins = rows[rowIdx];
|
||||
if (!Array.isArray(bins) || bins.length === 0) continue;
|
||||
const { startIdx, endIdx } = overviewVisibleBinWindow(lastSpectrumData, bins.length);
|
||||
const spanBins = Math.max(1, endIdx - startIdx);
|
||||
const y = h - (rows.length - rowIdx) * rowH;
|
||||
for (let x = 0; x < w; x++) {
|
||||
const binIdx = Math.floor((x / Math.max(1, w - 1)) * (bins.length - 1));
|
||||
for (let x = 0; x < w; x += columnStep) {
|
||||
const frac = x / Math.max(1, w - 1);
|
||||
const binIdx = Math.min(endIdx, startIdx + Math.floor(frac * spanBins));
|
||||
ctx.fillStyle = waterfallColor(bins[binIdx], isLight);
|
||||
ctx.fillRect(x, y, 1.25, rowH + 1);
|
||||
ctx.fillRect(x, y, columnStep + 0.75, rowH + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2689,7 +2707,10 @@ function scheduleSpectrumDraw() {
|
||||
spectrumDrawPending = true;
|
||||
requestAnimationFrame(() => {
|
||||
spectrumDrawPending = false;
|
||||
if (lastSpectrumData) drawSpectrum(lastSpectrumData);
|
||||
if (lastSpectrumData) {
|
||||
drawSpectrum(lastSpectrumData);
|
||||
if (overviewWaterfallRows.length > 0) scheduleOverviewDraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,9 @@
|
||||
<select id="header-rig-switch-select" aria-label="Select active rig"></select>
|
||||
<button id="header-rig-switch-btn" type="button">Switch Rig</button>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="theme-toggle" class="theme-toggle-btn" type="button" aria-label="Toggle dark or light theme">Light</button>
|
||||
<button id="header-auth-btn" class="theme-toggle-btn" type="button" style="display:none;" aria-label="Login or Logout">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img id="logo" class="header-logo" src="/logo.png?v=1" alt="trx logo" onerror="this.style.display='none'" />
|
||||
@@ -38,24 +35,12 @@
|
||||
<div class="subtitle" id="rig-subtitle" style="font-weight: 700;">Rig: --</div>
|
||||
<div class="subtitle" id="owner-subtitle">Owner: --</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="theme-toggle" class="theme-toggle-btn" type="button" aria-label="Toggle dark or light theme">Light</button>
|
||||
</div>
|
||||
<!-- Auth gate (hidden by default, shown if auth is required) -->
|
||||
<div id="auth-gate" style="display:none; max-width: 30rem; margin: 0 auto 0.9rem; padding: 1.25rem 0 1.5rem; text-align: center;">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;">Access Required</div>
|
||||
<div style="color: var(--text-muted);">Enter passphrase to continue</div>
|
||||
</div>
|
||||
<form id="auth-form" style="margin: 1.5rem 0;">
|
||||
<input type="password" id="auth-passphrase" placeholder="Passphrase" autocomplete="off" style="width: 100%; padding: 0.65rem 0.75rem; margin-bottom: 1rem; border: 1px solid var(--border-light); border-radius: 0.45rem; background: var(--input-bg); color: var(--text); font-size: 1rem; box-sizing: border-box;" />
|
||||
<button type="submit" style="width: 100%; padding: 0.65rem 0.75rem; background: var(--accent-green); color: #fff; border: none; border-radius: 0.45rem; font-weight: 700; cursor: pointer; font-size: 1rem; box-sizing: border-box;">Login</button>
|
||||
</form>
|
||||
<button id="auth-guest-btn" type="button" style="width: 100%; padding: 0.65rem 0.75rem; background: var(--btn-bg); color: var(--text); border: 1px solid var(--border-light); border-radius: 0.45rem; font-weight: 600; cursor: pointer; font-size: 1rem; box-sizing: border-box; margin-top: 0.5rem; display: none;">Continue as Guest</button>
|
||||
<div id="auth-error" style="color: #ff6b6b; font-size: 0.9rem; margin-top: 1rem; display: none;"></div>
|
||||
<div id="auth-role" style="margin-top: 1rem; color: var(--text-muted); font-size: 0.85rem; display: none;"></div>
|
||||
</div>
|
||||
<div class="overview-strip">
|
||||
<div class="overview-toolbar">
|
||||
<span class="overview-label" id="overview-label">Signal History</span>
|
||||
<label class="overview-control">Peak Hold
|
||||
<select id="overview-peak-hold" class="status-input">
|
||||
<option value="500">0.5 s</option>
|
||||
@@ -64,9 +49,24 @@
|
||||
<option value="5000">5 s</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="overview-label" id="overview-label">Signal History</span>
|
||||
</div>
|
||||
<canvas id="overview-canvas" aria-hidden="true"></canvas>
|
||||
</div>
|
||||
<!-- Auth gate (hidden by default, shown if auth is required) -->
|
||||
<div id="auth-gate" style="display:none; max-width: 30rem; margin: 0 auto 0.9rem; padding: 1.25rem 0 1.5rem; text-align: center;">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;">Access Required</div>
|
||||
<div style="color: var(--text-muted);">Enter passphrase to continue</div>
|
||||
</div>
|
||||
<form id="auth-form" style="margin: 1rem 0 0.35rem;">
|
||||
<input type="password" id="auth-passphrase" placeholder="Passphrase" autocomplete="off" style="width: 100%; padding: 0.65rem 0.75rem; margin-bottom: 1rem; border: 1px solid var(--border-light); border-radius: 0.45rem; background: var(--input-bg); color: var(--text); font-size: 1rem; box-sizing: border-box;" />
|
||||
<button type="submit" style="width: 100%; padding: 0.65rem 0.75rem; background: var(--accent-green); color: #fff; border: none; border-radius: 0.45rem; font-weight: 700; cursor: pointer; font-size: 1rem; box-sizing: border-box;">Login</button>
|
||||
</form>
|
||||
<button id="auth-guest-btn" type="button" style="width: 100%; padding: 0.65rem 0.75rem; background: var(--btn-bg); color: var(--text); border: 1px solid var(--border-light); border-radius: 0.45rem; font-weight: 600; cursor: pointer; font-size: 1rem; box-sizing: border-box; margin-top: 0; display: none;">Continue as Guest</button>
|
||||
<div id="auth-error" style="color: #ff6b6b; font-size: 0.9rem; margin-top: 1rem; display: none;"></div>
|
||||
<div id="auth-role" style="margin-top: 1rem; color: var(--text-muted); font-size: 0.85rem; display: none;"></div>
|
||||
</div>
|
||||
<div id="tab-main" class="tab-panel">
|
||||
<div id="loading" style="text-align:center; padding:2rem 0;">
|
||||
<div id="loading-title" style="margin-bottom:0.4rem; font-size:1.1rem; font-weight:600;">Initializing (rig)…</div>
|
||||
|
||||
@@ -351,7 +351,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
small { color: var(--text-muted); }
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -370,11 +370,12 @@ small { color: var(--text-muted); }
|
||||
.overview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.overview-label {
|
||||
margin-left: auto;
|
||||
color: var(--text-heading);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
@@ -409,6 +410,12 @@ small { color: var(--text-muted); }
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -674,6 +681,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.freq-inline { flex-wrap: wrap; }
|
||||
.header-text { width: auto; min-width: 0; flex: 0 1 auto; }
|
||||
.header-left { justify-content: flex-end; }
|
||||
.header-actions { justify-content: flex-end; }
|
||||
.tab-bar { flex-wrap: wrap; }
|
||||
.top-bar-actions { width: 100%; justify-content: space-between; }
|
||||
.header-rig-switch { width: auto; justify-content: flex-end; }
|
||||
|
||||
Reference in New Issue
Block a user