(() => { const el = (id) => document.getElementById(id); const canvasEl = el("dashboardCanvas"); const saveBtn = el("dashboardSaveBtn"); const modeSelectEl = el("dashboardModeSelect"); const store = { widgets: [], pollTimer: null, }; async function api(path, opts = {}) { if (window.MissionsApp?.missionsApi) return window.MissionsApp.missionsApi(path, opts); const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...opts }); if (res.status === 204) return null; const data = res.ok ? await res.json() : null; if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); return data; } function newWidgetId() { return `w_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; } async function loadDashboard() { const data = await api("/api/dashboard"); store.widgets = Array.isArray(data.widgets) ? data.widgets : []; if (!store.widgets.length) { store.widgets = [ { id: newWidgetId(), type: "mission_queue" }, { id: newWidgetId(), type: "pause_continue" }, { id: newWidgetId(), type: "action_log" }, ]; } renderCanvas(); } async function saveDashboard() { await api("/api/dashboard", { method: "PUT", body: JSON.stringify({ widgets: store.widgets }), }); } function missions() { return window.MissionsApp?.getMissions?.() || []; } function groups() { const g = new Set(); missions().forEach((m) => g.add(m.group || "Missions")); return [...g].sort(); } function renderCanvas() { if (!canvasEl) return; canvasEl.innerHTML = ""; if (!store.widgets.length) { const empty = document.createElement("p"); empty.className = "mutedNote"; empty.textContent = "Thêm widget từ panel trái."; canvasEl.appendChild(empty); return; } store.widgets.forEach((widget) => { canvasEl.appendChild(buildWidget(widget)); }); } function buildWidget(widget) { const box = document.createElement("div"); box.className = "dashboardWidget"; box.dataset.widgetId = widget.id; const head = document.createElement("div"); head.className = "dashboardWidgetHead"; head.innerHTML = `${widget.type.replace(/_/g, " ")}`; const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "dashboardWidgetRemove"; removeBtn.textContent = "×"; removeBtn.addEventListener("click", () => { store.widgets = store.widgets.filter((w) => w.id !== widget.id); renderCanvas(); }); head.appendChild(removeBtn); box.appendChild(head); const body = document.createElement("div"); body.className = "dashboardWidgetBody"; switch (widget.type) { case "mission_button": renderMissionButton(body, widget); break; case "mission_group": renderMissionGroup(body, widget); break; case "mission_queue": renderMissionQueue(body); break; case "pause_continue": renderPauseContinue(body); break; case "action_log": renderActionLog(body); break; default: body.textContent = "Unknown widget"; } box.appendChild(body); return box; } function renderMissionButton(container, widget) { const select = document.createElement("select"); missions().forEach((m) => { const opt = document.createElement("option"); opt.value = m.id; opt.textContent = m.name; if (m.id === widget.mission_id) opt.selected = true; select.appendChild(opt); }); select.addEventListener("change", () => { widget.mission_id = select.value; }); container.appendChild(select); const btn = document.createElement("button"); btn.type = "button"; btn.className = "dashboardMissionBtn"; btn.textContent = "Start mission"; btn.addEventListener("click", async () => { try { if (window.MissionsApp?.queueMission) { await window.MissionsApp.queueMission(select.value, {}); } else { await api("/api/mission_queue", { method: "POST", body: JSON.stringify({ mission_id: select.value, parameters: {} }), }); } await refreshWidgetsDynamic(); } catch (e) { alert(e.message); } }); container.appendChild(btn); } function renderMissionGroup(container, widget) { const select = document.createElement("select"); groups().forEach((g) => { const opt = document.createElement("option"); opt.value = g; opt.textContent = g; if (g === (widget.group || "Missions")) opt.selected = true; select.appendChild(opt); }); select.addEventListener("change", () => { widget.group = select.value; renderCanvas(); }); container.appendChild(select); const list = document.createElement("div"); list.className = "dashboardMissionGroup"; missions() .filter((m) => (m.group || "Missions") === (widget.group || select.value)) .forEach((m) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "btn subtle btnBlock"; btn.textContent = m.name; btn.addEventListener("click", async () => { try { await window.MissionsApp.queueMission(m.id, {}); await refreshWidgetsDynamic(); } catch (e) { alert(e.message); } }); list.appendChild(btn); }); container.appendChild(list); } function renderMissionQueue(container) { const wrap = document.createElement("div"); wrap.className = "dashboardQueueMini"; wrap.dataset.dynamic = "queue"; container.appendChild(wrap); updateQueueMini(wrap); } function renderPauseContinue(container) { const play = document.createElement("button"); play.type = "button"; play.className = "btn primary dashboardPauseBtn"; play.textContent = "▶ Continue"; play.addEventListener("click", async () => { await api("/api/mission_runner/state", { method: "PUT", body: JSON.stringify({ state_id: 3 }) }); await refreshWidgetsDynamic(); }); const pause = document.createElement("button"); pause.type = "button"; pause.className = "btn subtle dashboardPauseBtn"; pause.textContent = "⏸ Pause"; pause.addEventListener("click", async () => { await api("/api/mission_runner/state", { method: "PUT", body: JSON.stringify({ state_id: 4 }) }); await refreshWidgetsDynamic(); }); container.appendChild(play); container.appendChild(pause); } function renderActionLog(container) { const wrap = document.createElement("div"); wrap.className = "dashboardQueueMini"; wrap.dataset.dynamic = "log"; container.appendChild(wrap); updateActionLogMini(wrap); } function updateQueueMini(node) { const status = window.MissionsApp?.getRunnerStatus?.(); const queue = status?.queue || []; node.innerHTML = queue.length ? queue .map( (q) => `
${q.mission_name || q.mission_id} ${q.state}
` ) .join("") : `Queue trống`; } function updateActionLogMini(node) { const status = window.MissionsApp?.getRunnerStatus?.(); const log = status?.action_log || []; node.innerHTML = [...log] .reverse() .slice(0, 12) .map((row) => `
${row.message || ""}
`) .join("") || ``; } async function refreshWidgetsDynamic() { if (window.MissionsApp?.refreshRunnerStatus) await window.MissionsApp.refreshRunnerStatus(); canvasEl?.querySelectorAll("[data-dynamic=queue]").forEach(updateQueueMini); canvasEl?.querySelectorAll("[data-dynamic=log]").forEach(updateActionLogMini); } function addWidget(type) { const widget = { id: newWidgetId(), type }; if (type === "mission_button") { widget.mission_id = missions()[0]?.id || ""; } if (type === "mission_group") { widget.group = groups()[0] || "Missions"; } store.widgets.push(widget); renderCanvas(); } function bindEvents() { document.querySelectorAll(".dashboardAddWidget").forEach((btn) => { btn.addEventListener("click", () => addWidget(btn.dataset.widget)); }); saveBtn?.addEventListener("click", async () => { try { await saveDashboard(); alert("Đã lưu dashboard."); } catch (e) { alert(e.message); } }); modeSelectEl?.addEventListener("change", async () => { try { await api("/api/mission_runner/mode", { method: "PUT", body: JSON.stringify({ mode: modeSelectEl.value }), }); await refreshWidgetsDynamic(); } catch (e) { alert(e.message); } }); } function startPolling() { if (store.pollTimer) clearInterval(store.pollTimer); store.pollTimer = setInterval(() => { const page = el("pageDashboard"); if (page && !page.hidden) refreshWidgetsDynamic(); }, 2000); } async function init() { bindEvents(); try { await loadDashboard(); } catch { store.widgets = [ { id: newWidgetId(), type: "mission_queue" }, { id: newWidgetId(), type: "pause_continue" }, ]; renderCanvas(); } startPolling(); } window.DashboardApp = { init, onPageShow() { loadDashboard().catch(() => renderCanvas()); refreshWidgetsDynamic(); }, }; init(); })();