Initial commit: scan_web integrated recording UI

FastAPI backend + vanilla JS frontend for LiDAR/camera startup, camera
settings, and rosbag recording, unifying the previous scan_gui/scan_gui_dual/
scan_gui_triple desktop tools into one web app.
This commit is contained in:
gardentech
2026-08-07 14:26:00 +09:00
commit f34817d5b6
38 changed files with 5430 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
import { formatBytes, formatDuration } from "/js/format.js";
async function getJSON(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(`${url}: ${res.status} ${await res.text()}`);
return res.json();
}
export function initRecordingPanel(defaultSaveDir) {
const saveDirInput = document.getElementById("rec-save-dir");
saveDirInput.value = defaultSaveDir;
const browseBtn = document.getElementById("btn-browse");
const browsePanel = document.getElementById("browse-panel");
const filenameInput = document.getElementById("rec-filename");
const confirmBtn = document.getElementById("btn-rec-confirm");
const stopBtn = document.getElementById("btn-rec-stop");
const bagList = document.getElementById("bag-list");
browseBtn.onclick = async () => {
if (!browsePanel.classList.contains("hidden")) {
browsePanel.classList.add("hidden");
return;
}
await renderBrowse(saveDirInput.value || defaultSaveDir);
browsePanel.classList.remove("hidden");
};
async function renderBrowse(path) {
const data = await getJSON(`/api/fs/browse?path=${encodeURIComponent(path)}`);
browsePanel.innerHTML = "";
const cur = document.createElement("div");
cur.className = "dir-entry";
cur.style.fontWeight = "bold";
cur.textContent = `✓ 선택: ${data.path}`;
cur.onclick = () => {
saveDirInput.value = data.path;
browsePanel.classList.add("hidden");
refreshBags();
};
browsePanel.appendChild(cur);
if (data.parent) {
const up = document.createElement("div");
up.className = "dir-entry";
up.textContent = "..";
up.onclick = () => renderBrowse(data.parent);
browsePanel.appendChild(up);
}
for (const d of data.dirs) {
const el = document.createElement("div");
el.className = "dir-entry";
el.textContent = d.name;
el.onclick = () => renderBrowse(d.path);
browsePanel.appendChild(el);
}
}
saveDirInput.addEventListener("change", refreshBags);
confirmBtn.onclick = async () => {
try {
await getJSON("/api/recording/start", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: filenameInput.value, save_dir: saveDirInput.value }),
});
refreshBags();
document.getElementById("overlay-recording").classList.add("hidden");
} catch (e) {
alert(String(e));
}
};
stopBtn.onclick = async () => {
await getJSON("/api/recording/stop", { method: "POST" });
// rosbag2 flushes metadata.yaml on SIGINT — give it a moment before
// reading the bag back, otherwise it still shows as "incomplete".
setTimeout(refreshBags, 1200);
};
function renderBagItem(bag) {
const li = document.createElement("li");
li.className = "bag-item";
const nameRow = document.createElement("div");
nameRow.className = "bag-item-name";
nameRow.textContent = bag.name;
if (!bag.complete) {
const badge = document.createElement("span");
badge.className = "bag-item-incomplete";
badge.textContent = "(녹화 중 / 미완료)";
nameRow.appendChild(badge);
}
li.appendChild(nameRow);
const meta = document.createElement("div");
meta.className = "bag-item-meta";
const parts = [formatDuration(bag.duration_s), formatBytes(bag.size_bytes)];
if (bag.message_count != null) parts.push(`msg ${bag.message_count.toLocaleString()}`);
if (bag.topics && bag.topics.length) parts.push(`topic ${bag.topics.length}`);
meta.textContent = parts.filter(Boolean).join(" · ");
li.appendChild(meta);
return li;
}
// Disk-free is owned entirely by status-panel.js now (HUD strip, driven by
// /ws/status) — this panel only needs the bag list.
async function refreshBags() {
const dir = saveDirInput.value || defaultSaveDir;
try {
const bagsData = await getJSON(`/api/bags?save_dir=${encodeURIComponent(dir)}`);
bagList.innerHTML = "";
for (const bag of bagsData.bags) {
bagList.appendChild(renderBagItem(bag));
}
} catch (e) {
// Directory may not exist yet (new save path) — not an error worth alarming over.
bagList.innerHTML = "";
}
}
refreshBags();
setInterval(refreshBags, 15000);
}