chức năng dashboard

This commit is contained in:
2026-06-13 13:20:57 +07:00
parent c116b30bea
commit 6f6d925fdd
9 changed files with 746 additions and 167 deletions

View File

@@ -435,33 +435,70 @@
store.queue = Array.isArray(data.queue) ? data.queue : [];
store.runner = data.runner && typeof data.runner === "object" ? data.runner : { state: "idle", message: "" };
renderQueuePanel();
notifyQueueUpdate();
} catch (e) {
if (missionQueueRunnerEl) missionQueueRunnerEl.textContent = `Không tải được queue: ${e.message}`;
}
}
function renderQueuePanel() {
if (!missionQueueListEl) return;
missionQueueListEl.innerHTML = "";
if (missionQueueEmptyEl) missionQueueEmptyEl.hidden = store.queue.length > 0;
const queueListeners = new Set();
function notifyQueueUpdate() {
queueListeners.forEach((fn) => {
try {
fn(getQueueSnapshot());
} catch {
/* ignore */
}
});
}
if (missionQueueRunnerEl) {
function getQueueSnapshot() {
return {
queue: JSON.parse(JSON.stringify(store.queue)),
runner: JSON.parse(JSON.stringify(store.runner)),
};
}
function renderQueueInto(target, options = {}) {
const listEl = target.listEl;
const runnerEl = target.runnerEl;
const emptyEl = target.emptyEl;
const compact = !!options.compact;
if (!listEl) return;
listEl.innerHTML = "";
if (emptyEl) emptyEl.hidden = store.queue.length > 0;
if (runnerEl) {
const st = store.runner.state || "idle";
missionQueueRunnerEl.classList.toggle("running", st === "running");
runnerEl.classList.toggle("running", st === "running" || st === "paused");
runnerEl.classList.toggle("paused", st === "paused");
const action = store.runner.current_action ? `${store.runner.current_action}` : "";
missionQueueRunnerEl.textContent = store.runner.message
runnerEl.textContent = store.runner.message
? `${store.runner.message}${action}`
: st === "idle"
? "Robot sẵn sàng — queue trống hoặc chờ mission mới."
? compact
? "Sẵn sàng"
: "Robot sẵn sàng — queue trống hoặc chờ mission mới."
: "—";
}
store.queue.forEach((entry, index) => {
const row = document.createElement("div");
row.className = `missionQueueItem status-${entry.status || "pending"}`;
row.className = `missionQueueItem status-${entry.status || "pending"}${compact ? " compact" : ""}`;
const paramHtml = formatQueueParameters(entry);
const canReorder = entry.status === "pending";
row.innerHTML = `
const canReorder = entry.status === "pending" && !compact;
row.innerHTML = compact
? `
<div>
<div class="missionQueueItemTitle">${escapeHtml(entry.mission_name || "Mission")}</div>
<div class="missionQueueItemMeta">${queueStatusLabel(entry.status)} • #${index + 1}</div>
${paramHtml ? `<div class="missionQueueItemParams">${paramHtml}</div>` : ""}
</div>
<div class="missionQueueWidgetActions">
${entry.status === "pending" ? `<button type="button" class="iconBtn danger" data-queue-remove="${entry.id}" title="Xóa">×</button>` : `<span class="missionQueueStatus ${escapeHtml(entry.status || "pending")}">${queueStatusLabel(entry.status)}</span>`}
</div>`
: `
<div class="missionQueueOrder">
<button type="button" class="iconBtn" data-queue-up="${entry.id}" title="Lên" ${canReorder && index > 0 ? "" : "disabled"}>↑</button>
<button type="button" class="iconBtn" data-queue-down="${entry.id}" title="Xuống" ${canReorder && index < store.queue.length - 1 ? "" : "disabled"}>↓</button>
@@ -479,10 +516,21 @@
row.querySelector("[data-queue-up]")?.addEventListener("click", () => moveQueueItem(entry.id, -1));
row.querySelector("[data-queue-down]")?.addEventListener("click", () => moveQueueItem(entry.id, 1));
row.querySelector("[data-queue-remove]")?.addEventListener("click", () => removeQueueItem(entry.id));
missionQueueListEl.appendChild(row);
listEl.appendChild(row);
});
}
function renderQueuePanel() {
renderQueueInto(
{
listEl: missionQueueListEl,
runnerEl: missionQueueRunnerEl,
emptyEl: missionQueueEmptyEl,
},
{ compact: false }
);
}
async function moveQueueItem(id, delta) {
const ids = store.queue.map((q) => q.id);
const idx = ids.indexOf(id);
@@ -522,6 +570,27 @@
}
}
async function enqueueMission(missionId, parameters = {}) {
const mission = findMission(missionId);
if (!mission) throw new Error("Mission không tồn tại");
const payload = {
mission: resolveMissionSnapshot(mission),
parameters,
};
await missionApi("/api/mission_queue", { method: "POST", body: JSON.stringify(payload) });
await refreshQueue();
}
async function pauseRunner() {
await missionApi("/api/mission_queue/pause", { method: "POST", body: "{}" });
await refreshQueue();
}
async function continueRunner() {
await missionApi("/api/mission_queue/continue", { method: "POST", body: "{}" });
await refreshQueue();
}
function openQueueDialog(missionId) {
const mission = findMission(missionId);
if (!mission) return;
@@ -1192,6 +1261,23 @@
window.MissionsApp = {
init,
getMissions: () => [...store.missions],
getGroups: () => allGroups(),
getMissionById: findMission,
queueMission: openQueueDialog,
enqueueMission,
pauseRunner,
continueRunner,
refreshQueue,
clearQueue,
getQueueSnapshot,
renderQueueInto,
onQueueUpdate(fn) {
queueListeners.add(fn);
return () => queueListeners.delete(fn);
},
startQueuePoll,
stopQueuePoll,
onPageShow() {
if (!missionEditorViewEl?.hidden) renderMissionEditor();
else {