[feat](trx-frontend-http): streamline main control layout

Refine main control interactions and presentation in the HTTP frontend.\n\n- remove frequency and mode Set buttons\n- apply mode changes immediately on picker change\n- place Mode/Tune/Transmit-Power controls in one horizontal row\n- align control labels vertically across that row\n- move and enlarge MHz/kHz/Hz selector beside frequency input\n- keep Enter-to-set frequency behavior\n- switch signal measurement to elapsed-time averaging\n- enlarge header logo 2x\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-13 01:38:51 +01:00
parent 55c70f0fb7
commit 088a683c62
3 changed files with 87 additions and 40 deletions
@@ -19,7 +19,6 @@ const vfoPicker = document.getElementById("vfo-picker");
const signalBar = document.getElementById("signal-bar");
const signalValue = document.getElementById("signal-value");
const pttBtn = document.getElementById("ptt-btn");
const modeBtn = document.getElementById("mode-apply");
const txLimitInput = document.getElementById("tx-limit");
const txLimitBtn = document.getElementById("tx-limit-btn");
const txLimitRow = document.getElementById("tx-limit-row");
@@ -41,7 +40,12 @@ let lastRendered = null;
let rigName = "Rig";
let hintTimer = null;
let sigMeasuring = false;
let sigSamples = [];
let sigLastSUnits = null;
let sigMeasureTimer = null;
let sigMeasureLastTickMs = 0;
let sigMeasureAccumMs = 0;
let sigMeasureWeighted = 0;
let sigMeasurePeak = null;
let lastFreqHz = null;
let jogStep = loadSetting("jogStep", 1000);
let minFreqStepHz = 1;
@@ -69,7 +73,6 @@ function showHint(msg, duration) {
let supportedModes = [];
let supportedBands = [];
let freqDirty = false;
let modeDirty = false;
let initialized = false;
let lastEventAt = Date.now();
let es;
@@ -228,7 +231,7 @@ function formatSignal(sUnits) {
}
function setDisabled(disabled) {
[freqEl, modeEl, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
[freqEl, modeEl, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
if (el) el.disabled = disabled;
});
}
@@ -317,7 +320,7 @@ function render(update) {
window.updateFt8RfDisplay();
}
}
if (!modeDirty && update.status && update.status.mode) {
if (update.status && update.status.mode) {
const mode = normalizeMode(update.status.mode);
modeEl.value = mode ? mode.toUpperCase() : "";
}
@@ -424,14 +427,12 @@ function render(update) {
}
if (update.status && update.status.rx && typeof update.status.rx.sig === "number") {
const sUnits = dbmToSUnits(update.status.rx.sig);
sigLastSUnits = sUnits;
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
signalBar.style.width = `${pct}%`;
signalValue.textContent = formatSignal(sUnits);
if (sigMeasuring) {
sigSamples.push(sUnits);
sigMeasureBtn.textContent = `Stop (${sigSamples.length})`;
}
} else {
sigLastSUnits = null;
signalBar.style.width = "0%";
signalValue.textContent = "--";
}
@@ -756,14 +757,13 @@ jogStepEl.addEventListener("click", (e) => {
}
}
modeBtn.addEventListener("click", async () => {
async function applyModeFromPicker() {
const mode = modeEl.value || "";
if (!mode) {
showHint("Mode missing", 1500);
return;
}
modeDirty = false;
modeBtn.disabled = true;
modeEl.disabled = true;
showHint("Setting mode…");
try {
await postPath(`/set_mode?mode=${encodeURIComponent(mode)}`);
@@ -772,13 +772,11 @@ modeBtn.addEventListener("click", async () => {
showHint("Set mode failed", 2000);
console.error(err);
} finally {
modeBtn.disabled = false;
modeEl.disabled = false;
}
});
}
modeEl.addEventListener("input", () => {
modeDirty = true;
});
modeEl.addEventListener("change", applyModeFromPicker);
txLimitInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
@@ -997,27 +995,67 @@ const sigMeasureBtn = document.getElementById("sig-measure-btn");
const sigClearBtn = document.getElementById("sig-clear-btn");
const sigResult = document.getElementById("sig-result");
function resetSignalMeasurementState() {
sigMeasureLastTickMs = 0;
sigMeasureAccumMs = 0;
sigMeasureWeighted = 0;
sigMeasurePeak = null;
}
function updateSignalMeasurement(nowMs) {
if (!sigMeasuring) return;
if (sigMeasureLastTickMs === 0) {
sigMeasureLastTickMs = nowMs;
return;
}
const dt = Math.max(0, nowMs - sigMeasureLastTickMs);
sigMeasureLastTickMs = nowMs;
if (!Number.isFinite(sigLastSUnits)) return;
sigMeasureAccumMs += dt;
sigMeasureWeighted += sigLastSUnits * dt;
if (sigMeasurePeak === null || sigLastSUnits > sigMeasurePeak) {
sigMeasurePeak = sigLastSUnits;
}
}
function stopSignalMeasurement() {
if (sigMeasureTimer) {
clearInterval(sigMeasureTimer);
sigMeasureTimer = null;
}
sigMeasuring = false;
sigMeasureBtn.textContent = "Measure";
sigMeasureBtn.style.borderColor = "";
sigMeasureBtn.style.color = "";
}
sigMeasureBtn.addEventListener("click", () => {
if (!sigMeasuring) {
sigSamples = [];
resetSignalMeasurementState();
sigMeasuring = true;
sigMeasureBtn.textContent = "Stop (0)";
sigMeasureBtn.textContent = "Stop (0.0s)";
sigMeasureBtn.style.borderColor = "#00d17f";
sigMeasureBtn.style.color = "#00d17f";
sigMeasureTimer = setInterval(() => {
const now = Date.now();
updateSignalMeasurement(now);
sigMeasureBtn.textContent = `Stop (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
}, 200);
} else {
sigMeasuring = false;
sigMeasureBtn.textContent = "Measure";
sigMeasureBtn.style.borderColor = "";
sigMeasureBtn.style.color = "";
if (sigSamples.length > 0) {
const avg = sigSamples.reduce((a, b) => a + b, 0) / sigSamples.length;
const peak = Math.max(...sigSamples);
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${sigSamples.length} samples)`;
updateSignalMeasurement(Date.now());
stopSignalMeasurement();
if (sigMeasureAccumMs > 0) {
const avg = sigMeasureWeighted / sigMeasureAccumMs;
const peak = sigMeasurePeak;
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
}
}
});
sigClearBtn.addEventListener("click", () => {
stopSignalMeasurement();
resetSignalMeasurementState();
sigResult.textContent = "";
});
@@ -41,25 +41,27 @@
<button type="button" data-step="1">Hz</button>
</div>
</div>
<div class="jog-container">
<button id="jog-down" type="button" class="jog-btn">&minus;</button>
<div class="jog-wheel" id="jog-wheel">
<div class="jog-indicator" id="jog-indicator"></div>
</div>
<button id="jog-up" type="button" class="jog-btn">+</button>
</div>
</div>
<div class="controls-row full-row">
<div>
<div class="controls-col">
<div class="label">Mode</div>
<div class="inline">
<select class="status-input" id="mode">
<option value="">--</option>
</select>
<button id="mode-apply" type="button">Set</button>
</div>
</div>
<div>
<div class="controls-col controls-col-center">
<div class="label">Tune</div>
<div class="jog-container">
<button id="jog-down" type="button" class="jog-btn">&minus;</button>
<div class="jog-wheel" id="jog-wheel">
<div class="jog-indicator" id="jog-indicator"></div>
</div>
<button id="jog-up" type="button" class="jog-btn">+</button>
</div>
</div>
<div class="controls-col">
<div class="label">Transmit / Power</div>
<div class="btn-grid">
<button id="ptt-btn" type="button">Toggle PTT</button>
@@ -22,10 +22,18 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
.controls-row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: start;
}
.controls-col { min-width: 0; }
.controls-col-center { justify-self: center; width: auto; }
.controls-row .label {
margin-bottom: 6px;
min-height: 1.2rem;
display: flex;
align-items: center;
}
.btn-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -37,7 +45,6 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 0.6rem;
}
.jog-wheel {
width: 52px;
@@ -137,7 +144,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
small { color: var(--text-muted); }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; }
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
.header-logo { height: 5em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
.header-logo { height: 10em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
.subtitle { color: var(--text-muted); font-size: 0.95rem; }
.subtitle a { color: var(--accent-green); text-decoration: none; }
.subtitle a:hover { text-decoration: underline; }