Files
DigiRadio/Software/components/net/www/index.html
T
micheleandCursor 0fc714e431 Add preset polish and broadcast metadata (fw 0.8.0).
Ship station reorder, DAB playing ids in presets, RDS PS/RT and DAB DLS in tuner status, Si4684 data-service read, host tests, and UI now-playing lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 00:01:49 +02:00

748 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DigiRadio</title>
<style>
:root {
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--space-4: 2.5rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.75rem;
--accent: #c47a2c;
--bg: #0f1114;
--surface: #1a1d22;
--text: #e8eaed;
--muted: #9aa0a6;
--radius: 0.5rem;
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font);
font-size: var(--text-base);
line-height: 1.5;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: var(--space-3);
}
main {
background: var(--surface);
border-radius: var(--radius);
padding: var(--space-4);
max-width: 36rem;
width: 100%;
border-top: 3px solid var(--accent);
}
h1 { font-size: var(--text-xl); font-weight: 600; margin-bottom: var(--space-2); }
h2 { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-2); }
p { color: var(--muted); margin-bottom: var(--space-2); }
label {
display: block;
font-size: 0.875rem;
color: var(--muted);
margin-bottom: var(--space-1);
}
input, select {
width: 100%;
padding: var(--space-1) var(--space-2);
margin-bottom: var(--space-2);
border: 1px solid #333;
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
}
input[type="range"] { padding: 0; }
button {
width: 100%;
padding: var(--space-2);
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
font: inherit;
font-weight: 600;
cursor: pointer;
}
button.secondary {
background: transparent;
color: var(--accent);
border: 1px solid var(--accent);
}
button:disabled { opacity: 0.6; cursor: wait; }
.status {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-lg);
margin-bottom: var(--space-3);
}
.dot {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: var(--accent);
}
.msg { font-size: 0.875rem; margin-top: var(--space-2); min-height: 1.25rem; }
.msg.ok { color: #7cb342; }
.msg.err { color: #e57373; }
code {
font-size: 0.9em;
background: var(--bg);
padding: 0.1em 0.35em;
border-radius: 0.25rem;
}
section + section {
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid #333;
}
.row { display: flex; gap: var(--space-2); margin-bottom: var(--space-2); }
.row button { flex: 1; }
.tuner-status {
background: var(--bg);
border-radius: var(--radius);
padding: var(--space-2);
font-size: 0.875rem;
margin-bottom: var(--space-2);
white-space: pre-wrap;
word-break: break-word;
}
.panel { display: none; }
.panel.active { display: block; }
ul.services {
list-style: none;
margin-bottom: var(--space-2);
max-height: 12rem;
overflow-y: auto;
}
ul.services li {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) 0;
border-bottom: 1px solid #2a2d33;
font-size: 0.875rem;
}
ul.services button {
width: auto;
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
}
</style>
</head>
<body>
<main>
<h1>DigiRadio</h1>
<div class="status" id="health">
<span class="dot" aria-hidden="true"></span>
<span>Checking health…</span>
</div>
<section id="wifi-section">
<h2>WiFi</h2>
<p>Connect this radio to your network (setup mode).</p>
<form id="wifi-form">
<label for="ssid">Network name (SSID)</label>
<input id="ssid" name="ssid" autocomplete="off" required maxlength="32">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="off" maxlength="63">
<button type="submit" id="save-btn">Save &amp; connect</button>
</form>
<p class="msg" id="wifi-msg" aria-live="polite"></p>
</section>
<section id="tuner-section">
<h2>Radio</h2>
<p>Control the DAB/FM tuner (available after companion chips boot).</p>
<div class="tuner-status" id="tuner-status">No status yet.</div>
<div class="row">
<button type="button" class="secondary" id="refresh-status">Refresh status</button>
</div>
<label for="band">Band</label>
<select id="band">
<option value="dab">DAB</option>
<option value="fm">FM</option>
</select>
<div id="panel-dab" class="panel active">
<label for="dab-index">Ensemble index (037)</label>
<input id="dab-index" type="number" min="0" max="37" value="12">
<button type="button" id="tune-dab">Tune DAB</button>
<div class="row" style="margin-top: var(--space-2);">
<button type="button" class="secondary" id="load-services">Load services</button>
</div>
<ul class="services" id="service-list"></ul>
</div>
<div id="panel-fm" class="panel">
<label for="fm-khz">Frequency (kHz)</label>
<input id="fm-khz" type="number" min="64000" max="108000" step="100" value="101500">
<button type="button" id="tune-fm">Tune FM</button>
<div class="row" style="margin-top: var(--space-2);">
<button type="button" class="secondary" id="seek-fm">Seek up</button>
</div>
</div>
<p class="msg" id="tuner-msg" aria-live="polite"></p>
</section>
<section id="presets-section">
<h2>Presets</h2>
<p>Saved DAB/FM stations (persisted in NVS).</p>
<ul class="services" id="preset-list"></ul>
<label for="preset-name">Name</label>
<input id="preset-name" maxlength="32" placeholder="My station">
<label for="preset-slot">Preset slot (120, optional)</label>
<input id="preset-slot" type="number" min="1" max="20">
<button type="button" id="save-preset">Save current tune target</button>
<p class="msg" id="preset-msg" aria-live="polite"></p>
</section>
<section id="bluetooth-section">
<h2>Bluetooth</h2>
<p>FSC-BT1035 aptX transmitter — pair headphones or speakers.</p>
<div class="tuner-status" id="bt-status">No status yet.</div>
<div class="row">
<button type="button" id="bt-refresh">Refresh</button>
<button type="button" class="secondary" id="bt-pair">Start pairing</button>
</div>
<div class="row">
<button type="button" class="secondary" id="bt-stop-pair">Stop pairing</button>
<button type="button" class="secondary" id="bt-disconnect">Disconnect</button>
</div>
<p class="msg" id="bt-msg" aria-live="polite"></p>
</section>
<section id="audio-section">
<h2>Audio</h2>
<p>ADAU1701 mixer and master volume (safeload at runtime).</p>
<label for="master-db">Master (dB)</label>
<input id="master-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="si4684-db">Radio path Si4684 (dB)</label>
<input id="si4684-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="esp32-db">ESP32 path (dB)</label>
<input id="esp32-db" type="range" min="-60" max="12" step="0.5" value="0">
<label for="stereo-level">Stereo depth (0100)</label>
<input id="stereo-level" type="range" min="0" max="100" step="1" value="0">
<label for="bass-level">Bass enhance (0100)</label>
<input id="bass-level" type="range" min="0" max="100" step="1" value="0">
<div class="row">
<button type="button" id="save-audio">Save profile</button>
<button type="button" class="secondary" id="reset-audio">Reset flat</button>
</div>
<p class="msg" id="audio-msg" aria-live="polite"></p>
</section>
</main>
<script>
function showMsg(el, text, ok) {
el.textContent = text;
el.className = "msg " + (ok ? "ok" : "err");
}
function formatTunerStatus(s) {
var lines = [
"Booted: " + s.booted,
"Band: " + s.band,
"Locked: " + s.locked,
"Volume: " + s.volume
];
if (s.dab) {
lines.push("DAB index: " + (s.dab.freq_index != null ? s.dab.freq_index : "—"));
if (s.dab.fic_quality != null) lines.push("FIC: " + s.dab.fic_quality);
if (s.dab.dynamic_label) lines.push("Now: " + s.dab.dynamic_label);
}
if (s.fm) {
if (s.fm.frequency_khz != null) lines.push("FM: " + s.fm.frequency_khz + " kHz");
if (s.fm.rssi_dbuv != null) lines.push("RSSI: " + s.fm.rssi_dbuv + " dBµV");
if (s.fm.stereo != null) lines.push("Stereo: " + s.fm.stereo);
if (s.fm.station_name) lines.push("Station: " + s.fm.station_name);
if (s.fm.radiotext) lines.push("Text: " + s.fm.radiotext);
}
return lines.join("\n");
}
fetch("/api/health")
.then(function (r) { return r.json(); })
.then(function (d) {
document.getElementById("health").innerHTML =
'<span class="dot"></span><span>Status: <code>' +
d.status + "</code> · FW <code>" + d.fw + "</code></span>";
})
.catch(function () {
document.getElementById("health").textContent = "Health check failed.";
});
document.getElementById("wifi-form").addEventListener("submit", function (e) {
e.preventDefault();
var btn = document.getElementById("save-btn");
var msg = document.getElementById("wifi-msg");
btn.disabled = true;
showMsg(msg, "Saving…", true);
fetch("/api/wifi", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ssid: document.getElementById("ssid").value,
password: document.getElementById("password").value
})
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Saved. Rebooting in " + res.d.reboot_in_sec + "s…", true);
} else {
showMsg(msg, "Error: " + (res.d.reason || "unknown"), false);
btn.disabled = false;
}
})
.catch(function () {
showMsg(msg, "Request failed.", false);
btn.disabled = false;
});
});
document.getElementById("band").addEventListener("change", function () {
var dab = document.getElementById("panel-dab");
var fm = document.getElementById("panel-fm");
if (this.value === "dab") {
dab.classList.add("active");
fm.classList.remove("active");
} else {
fm.classList.add("active");
dab.classList.remove("active");
}
});
function refreshTunerStatus() {
var msg = document.getElementById("tuner-msg");
return fetch("/api/tuner/status")
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
if (res.d.band === "fm") {
document.getElementById("band").value = "fm";
document.getElementById("band").dispatchEvent(new Event("change"));
}
showMsg(msg, "", true);
} else {
showMsg(msg, "Status error: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Status request failed.", false); });
}
document.getElementById("refresh-status").addEventListener("click", refreshTunerStatus);
document.getElementById("tune-dab").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var idx = parseInt(document.getElementById("dab-index").value, 10);
fetch("/api/tuner/tune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ band: "dab", freq_index: idx })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
showMsg(msg, "DAB tuned.", true);
} else {
showMsg(msg, "Tune failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Tune request failed.", false); });
});
document.getElementById("tune-fm").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var khz = parseInt(document.getElementById("fm-khz").value, 10);
fetch("/api/tuner/tune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ band: "fm", frequency_khz: khz })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("tuner-status").textContent = formatTunerStatus(res.d);
showMsg(msg, "FM tuned.", true);
} else {
showMsg(msg, "Tune failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Tune request failed.", false); });
});
document.getElementById("seek-fm").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
fetch("/api/tuner/seek", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.frequency_khz != null) {
document.getElementById("fm-khz").value = res.d.frequency_khz;
showMsg(msg, "Seeked to " + res.d.frequency_khz + " kHz.", true);
refreshTunerStatus();
} else {
showMsg(msg, "Seek failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Seek request failed.", false); });
});
document.getElementById("load-services").addEventListener("click", function () {
var msg = document.getElementById("tuner-msg");
var list = document.getElementById("service-list");
list.innerHTML = "";
fetch("/api/tuner/services")
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (!res.ok || !res.d.services) {
showMsg(msg, "Services error: " + (res.d.reason || "unknown"), false);
return;
}
res.d.services.forEach(function (svc) {
var li = document.createElement("li");
var label = document.createElement("span");
label.textContent = svc.label || ("SID " + svc.service_id);
var btn = document.createElement("button");
btn.textContent = "Play";
btn.addEventListener("click", function () {
fetch("/api/tuner/play", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service_id: svc.service_id,
component_id: svc.component_id
})
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d.status === "playing") {
showMsg(msg, "Playing " + (svc.label || svc.service_id), true);
} else {
showMsg(msg, "Play failed: " + (d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Play request failed.", false); });
});
li.appendChild(label);
li.appendChild(btn);
list.appendChild(li);
});
showMsg(msg, res.d.services.length + " service(s).", true);
})
.catch(function () { showMsg(msg, "Services request failed.", false); });
});
refreshTunerStatus();
var audioProfile = null;
function loadAudioProfile() {
var msg = document.getElementById("audio-msg");
return fetch("/api/audio/profile")
.then(function (r) { return r.json(); })
.then(function (d) {
audioProfile = d;
document.getElementById("master-db").value = d.master.left_db;
document.getElementById("si4684-db").value = d.mixer.si4684_left_db;
document.getElementById("esp32-db").value = d.mixer.esp32_left_db;
if (d.enhancements) {
document.getElementById("stereo-level").value = d.enhancements.stereo_level;
document.getElementById("bass-level").value = d.enhancements.bass_level;
}
showMsg(msg, "", true);
})
.catch(function () { showMsg(msg, "Audio profile load failed.", false); });
}
document.getElementById("save-audio").addEventListener("click", function () {
var msg = document.getElementById("audio-msg");
if (!audioProfile) {
showMsg(msg, "Profile not loaded.", false);
return;
}
var master = parseFloat(document.getElementById("master-db").value, 10);
var si4684 = parseFloat(document.getElementById("si4684-db").value, 10);
var esp32 = parseFloat(document.getElementById("esp32-db").value, 10);
audioProfile.master.left_db = master;
audioProfile.master.right_db = master;
audioProfile.mixer.si4684_left_db = si4684;
audioProfile.mixer.si4684_right_db = si4684;
audioProfile.mixer.esp32_left_db = esp32;
audioProfile.mixer.esp32_right_db = esp32;
if (!audioProfile.enhancements) {
audioProfile.enhancements = { stereo_level: 0, bass_level: 0 };
}
audioProfile.enhancements.stereo_level =
parseInt(document.getElementById("stereo-level").value, 10);
audioProfile.enhancements.bass_level =
parseInt(document.getElementById("bass-level").value, 10);
fetch("/api/audio/profile", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(audioProfile)
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Audio profile saved.", true);
} else {
showMsg(msg, "Save failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Save request failed.", false); });
});
function postEnhance(url, level, msg) {
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ level: level })
})
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Enhancement applied.", true);
} else {
showMsg(msg, "Enhance failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Enhance request failed.", false); });
}
document.getElementById("stereo-level").addEventListener("change", function () {
var msg = document.getElementById("audio-msg");
var level = parseInt(document.getElementById("stereo-level").value, 10);
postEnhance("/api/audio/stereo-enhance", level, msg);
});
document.getElementById("bass-level").addEventListener("change", function () {
var msg = document.getElementById("audio-msg");
var level = parseInt(document.getElementById("bass-level").value, 10);
postEnhance("/api/audio/bass-enhance", level, msg);
});
document.getElementById("reset-audio").addEventListener("click", function () {
var msg = document.getElementById("audio-msg");
fetch("/api/audio/reset", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Factory-flat profile applied.", true);
loadAudioProfile();
} else {
showMsg(msg, "Reset failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Reset request failed.", false); });
});
function formatBtStatus(s) {
return [
"Booted: " + s.booted,
"Pairing: " + s.pairing,
"A2DP: " + s.a2dp
].join("\n");
}
function refreshBluetooth() {
var msg = document.getElementById("bt-msg");
return fetch("/api/bluetooth/status")
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok && res.d.booted != null) {
document.getElementById("bt-status").textContent = formatBtStatus(res.d);
showMsg(msg, "", true);
} else {
showMsg(msg, "BT error: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "BT request failed.", false); });
}
document.getElementById("bt-refresh").addEventListener("click", refreshBluetooth);
document.getElementById("bt-pair").addEventListener("click", function () {
var msg = document.getElementById("bt-msg");
fetch("/api/bluetooth/pair", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok) {
showMsg(msg, "Discoverable — pair from your phone.", true);
refreshBluetooth();
} else {
showMsg(msg, "Pair failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Pair request failed.", false); });
});
document.getElementById("bt-stop-pair").addEventListener("click", function () {
var msg = document.getElementById("bt-msg");
fetch("/api/bluetooth/pair/stop", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok) {
showMsg(msg, "Pairing stopped.", true);
refreshBluetooth();
} else {
showMsg(msg, "Stop failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Stop request failed.", false); });
});
document.getElementById("bt-disconnect").addEventListener("click", function () {
var msg = document.getElementById("bt-msg");
fetch("/api/bluetooth/disconnect", { method: "POST" })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (res) {
if (res.ok) {
showMsg(msg, "A2DP disconnected.", true);
refreshBluetooth();
} else {
showMsg(msg, "Disconnect failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Disconnect request failed.", false); });
});
function renderPresets(data) {
var list = document.getElementById("preset-list");
list.innerHTML = "";
(data.stations || []).forEach(function (s, idx) {
var li = document.createElement("li");
var label = s.name + " (" + s.band + ")";
if (s.fm_frequency_khz) label += " " + s.fm_frequency_khz + " kHz";
if (s.dab_freq_index != null) label += " idx " + s.dab_freq_index;
if (s.dab_service_id != null) label += " svc " + s.dab_service_id;
li.textContent = label;
var tuneBtn = document.createElement("button");
tuneBtn.textContent = "Tune";
tuneBtn.addEventListener("click", function () {
fetch("/api/stations/tune", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ index: idx })
}).then(function () { refreshTunerStatus(); });
});
var delBtn = document.createElement("button");
delBtn.textContent = "Del";
delBtn.className = "secondary";
delBtn.addEventListener("click", function () {
fetch("/api/stations/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ index: idx })
}).then(function () { loadPresets(); });
});
var upBtn = document.createElement("button");
upBtn.textContent = "Up";
upBtn.className = "secondary";
upBtn.disabled = idx === 0;
upBtn.addEventListener("click", function () {
fetch("/api/stations/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: idx, to: idx - 1 })
}).then(function () { loadPresets(); });
});
var downBtn = document.createElement("button");
downBtn.textContent = "Dn";
downBtn.className = "secondary";
downBtn.disabled = idx >= (data.stations.length - 1);
downBtn.addEventListener("click", function () {
fetch("/api/stations/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: idx, to: idx + 1 })
}).then(function () { loadPresets(); });
});
li.appendChild(tuneBtn);
li.appendChild(upBtn);
li.appendChild(downBtn);
li.appendChild(delBtn);
list.appendChild(li);
});
}
function buildPresetBody(name, band, status) {
var body = { name: name, band: band };
if (band === "fm") {
var khz = status && status.fm && status.fm.frequency_khz != null
? status.fm.frequency_khz
: parseInt(document.getElementById("fm-khz").value, 10);
body.fm_frequency_khz = khz;
} else {
var idx = status && status.dab && status.dab.freq_index != null
? status.dab.freq_index
: parseInt(document.getElementById("dab-index").value, 10);
body.dab_freq_index = idx;
if (status && status.dab && status.dab.playing_service_id != null) {
body.dab_service_id = status.dab.playing_service_id;
}
if (status && status.dab && status.dab.playing_component_id != null) {
body.dab_component_id = status.dab.playing_component_id;
}
}
var slotVal = document.getElementById("preset-slot").value;
if (slotVal) body.preset_slot = parseInt(slotVal, 10);
return body;
}
function loadPresets() {
return fetch("/api/stations")
.then(function (r) { return r.json(); })
.then(function (d) { renderPresets(d); })
.catch(function () {});
}
document.getElementById("save-preset").addEventListener("click", function () {
var msg = document.getElementById("preset-msg");
var name = document.getElementById("preset-name").value.trim();
if (!name) {
showMsg(msg, "Enter a name.", false);
return;
}
var band = document.getElementById("band").value;
fetch("/api/tuner/status")
.then(function (r) { return r.json(); })
.then(function (status) {
var body = buildPresetBody(name, band, status);
return fetch("/api/stations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
}).then(function (r) {
return r.json().then(function (d) { return { ok: r.ok, d: d }; });
});
})
.then(function (res) {
if (res.ok && res.d.status === "saved") {
showMsg(msg, "Preset saved.", true);
loadPresets();
} else {
showMsg(msg, "Save failed: " + (res.d.reason || "unknown"), false);
}
})
.catch(function () { showMsg(msg, "Save request failed.", false); });
});
refreshBluetooth();
loadPresets();
loadAudioProfile();
</script>
</body>
</html>