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); }