Files
App/www/dashboard.js
2026-06-13 12:06:48 +07:00

323 lines
9.6 KiB
JavaScript
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.
(() => {
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 = `<span class="dashboardWidgetTitle">${widget.type.replace(/_/g, " ")}</span>`;
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) =>
`<div><strong>${q.mission_name || q.mission_id}</strong> <span class="mutedNote">${q.state}</span></div>`
)
.join("")
: `<span class="mutedNote">Queue trống</span>`;
}
function updateActionLogMini(node) {
const status = window.MissionsApp?.getRunnerStatus?.();
const log = status?.action_log || [];
node.innerHTML = [...log]
.reverse()
.slice(0, 12)
.map((row) => `<div>${row.message || ""}</div>`)
.join("") || `<span class="mutedNote">—</span>`;
}
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();
})();