(() => { const PAGE_SIZE = 10; const POLL_MS = 2500; const ICONS = { mission: ``, action: ``, view: ``, download: ``, delete: ``, }; const VIEW_IDS = [ "monitoringAnalyticsView", "monitoringSystemLogView", "monitoringErrorLogsView", "monitoringHardwareView", "monitoringSafetyView", "monitoringPlaceholderView", "monitoringMissionLogView", "monitoringActionLogView", ]; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; const store = { section: "analytics", view: "analytics", queue: [], runner: {}, runs: [], runActions: new Map(), selectedEntryId: null, missionFilter: "", missionPage: 1, actionFilter: "", actionPage: 1, systemLogItems: [], systemLogFilter: "", systemLogPage: 1, errorLogItems: [], errorLogFilter: "", hardwareGroups: [], hardwareExpanded: new Set(), analytics: null, analyticsChartMode: "bar", safety: null, pollTimer: null, }; function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } async function apiJson(url, opts = {}) { const res = await fetch(url, { credentials: "include", ...opts }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = null; } if (!res.ok) throw new Error((data && data.error) || text || res.statusText); return data; } function hideAllViews() { VIEW_IDS.forEach((id) => { const node = el(id); if (!node) return; node.hidden = true; node.setAttribute("aria-hidden", "true"); }); } function showView(node) { hideAllViews(); if (!node) return; node.hidden = false; node.removeAttribute("aria-hidden"); } function pageCount(total) { return Math.max(1, Math.ceil(total / PAGE_SIZE)); } function formatTime(iso) { if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return String(iso); return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } function formatDateYmd(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function addDaysYmd(ymd, delta) { const d = new Date(`${ymd}T12:00:00`); d.setDate(d.getDate() + delta); return formatDateYmd(d); } function startPoll(fn) { stopPoll(); store.pollTimer = window.setInterval(() => { fn().catch(() => {}); }, POLL_MS); } function stopPoll() { if (store.pollTimer) { window.clearInterval(store.pollTimer); store.pollTimer = null; } } function stateDotClass(state) { if (state === "error") return "monMirStateDot--error"; if (state === "warn") return "monMirStateDot--warn"; if (state === "info") return "monMirStateDot--info"; return "monMirStateDot--ok"; } function hwStatusClass(status) { if (status === "error") return "monHwStatus--error"; if (status === "warn") return "monHwStatus--warn"; return "monHwStatus--ok"; } // ——— Analytics ——— function initAnalyticsPresets() { const wrap = el("analyticsPresets"); if (!wrap) return; const presets = [ { key: "week", label: "monitoring.analytics.presetWeek", days: -6 }, { key: "7d", label: "monitoring.analytics.preset7d", days: -6 }, { key: "30d", label: "monitoring.analytics.preset30d", days: -29 }, { key: "365d", label: "monitoring.analytics.preset365d", days: -364 }, ]; wrap.innerHTML = presets .map( (p) => ``, ) .join(""); } function setAnalyticsDateRange(start, end) { if (el("analyticsStartDate")) el("analyticsStartDate").value = start; if (el("analyticsEndDate")) el("analyticsEndDate").value = end; } async function refreshAnalytics() { const start = el("analyticsStartDate")?.value || ""; const end = el("analyticsEndDate")?.value || ""; const grouping = el("analyticsGrouping")?.value || "day"; const qs = new URLSearchParams(); if (start) qs.set("start", start); if (end) qs.set("end", end); if (grouping) qs.set("grouping", grouping); store.analytics = await apiJson(`/api/monitoring/analytics?${qs}`); renderAnalyticsChart(); } function renderAnalyticsChart() { const data = store.analytics; const svg = el("analyticsChart"); const empty = el("analyticsChartEmpty"); const totalEl = el("analyticsTotalLabel"); if (!svg || !data) return; const buckets = Array.isArray(data.buckets) ? data.buckets : []; const mode = el("analyticsChartMode")?.value || store.analyticsChartMode || "bar"; store.analyticsChartMode = mode; if (totalEl) { totalEl.textContent = t("monitoring.analytics.totalPeriod", { n: (data.total_meters || 0).toFixed(1), lifetime: (data.lifetime_meters || 0).toFixed(1), }); } if (empty) empty.hidden = buckets.length > 0; svg.hidden = buckets.length === 0; if (!buckets.length) { svg.innerHTML = ""; return; } const values = buckets.map((b) => (mode === "accumulated" ? b.accumulated : b.meters) || 0); const maxVal = Math.max(...values, 1); const padL = 48; const padR = 16; const padT = 16; const padB = 40; const w = 800; const h = 280; const chartW = w - padL - padR; const chartH = h - padT - padB; const barGap = 4; const barW = Math.max(8, (chartW - barGap * (buckets.length - 1)) / buckets.length); let svgBody = ""; values.forEach((val, i) => { const barH = (val / maxVal) * chartH; const x = padL + i * (barW + barGap); const y = padT + chartH - barH; const label = String(buckets[i].label || "").slice(5); svgBody += `${escapeHtml(buckets[i].label)}: ${val.toFixed(1)} m`; svgBody += `${escapeHtml(label)}`; }); svgBody += ``; svgBody += `${maxVal.toFixed(0)}m`; svg.innerHTML = svgBody; } function showAnalytics() { store.view = "analytics"; store.section = "analytics"; showView(el("monitoringAnalyticsView")); const today = formatDateYmd(new Date()); if (!el("analyticsStartDate")?.value) setAnalyticsDateRange(addDaysYmd(today, -6), today); startPoll(refreshAnalytics); void refreshAnalytics(); } // ——— System log ——— function filteredSystemLog() { const q = store.systemLogFilter.trim().toLowerCase(); let items = [...store.systemLogItems]; if (q) { items = items.filter((e) => { return ( String(e.module || "").toLowerCase().includes(q) || String(e.message || "").toLowerCase().includes(q) || String(e.state || "").toLowerCase().includes(q) ); }); } return items; } function renderSystemLog() { const listEl = el("systemLogList"); if (!listEl) return; const items = filteredSystemLog(); const total = items.length; const pages = pageCount(total); if (store.systemLogPage > pages) store.systemLogPage = pages; const start = (store.systemLogPage - 1) * PAGE_SIZE; const pageItems = items.slice(start, start + PAGE_SIZE); if (el("systemLogFilterCount")) el("systemLogFilterCount").textContent = t("monitoring.itemsFound", { n: total }); if (el("systemLogPageLabel")) el("systemLogPageLabel").textContent = t("monitoring.pageOf", { page: store.systemLogPage, total: pages }); listEl.innerHTML = ""; const tableEl = el("systemLogTable"); const emptyEl = el("systemLogListEmpty"); if (tableEl) tableEl.hidden = total === 0; if (emptyEl) { emptyEl.hidden = total > 0; emptyEl.textContent = store.systemLogFilter ? t("monitoring.systemLog.emptyFilter") : t("monitoring.systemLog.empty"); } pageItems.forEach((entry) => { const tr = document.createElement("tr"); tr.className = "monMirRow"; tr.innerHTML = ` ${escapeHtml(entry.module || "—")} ${escapeHtml(entry.message || "—")} ${escapeHtml(formatTime(entry.ts))}`; listEl.appendChild(tr); }); } async function refreshSystemLog() { const data = await apiJson("/api/monitoring/system_log"); store.systemLogItems = Array.isArray(data.items) ? data.items : []; renderSystemLog(); } function showSystemLog() { store.view = "system-log"; store.section = "monitoring-log"; showView(el("monitoringSystemLogView")); startPoll(refreshSystemLog); void refreshSystemLog(); } // ——— Error logs ——— function filteredErrorLogs() { const q = store.errorLogFilter.trim().toLowerCase(); let items = [...store.errorLogItems]; if (q) { items = items.filter((e) => { return ( String(e.description || "").toLowerCase().includes(q) || String(e.module || "").toLowerCase().includes(q) ); }); } return items; } function renderErrorLogs() { const listEl = el("errorLogsList"); if (!listEl) return; const items = filteredErrorLogs(); const total = items.length; if (el("errorLogsFilterCount")) el("errorLogsFilterCount").textContent = t("monitoring.itemsFound", { n: total }); listEl.innerHTML = ""; const tableEl = el("errorLogsTable"); const emptyEl = el("errorLogsListEmpty"); if (tableEl) tableEl.hidden = total === 0; if (emptyEl) { emptyEl.hidden = total > 0; emptyEl.textContent = store.errorLogFilter ? t("monitoring.errorLogs.emptyFilter") : t("monitoring.errorLogs.empty"); } items.forEach((entry) => { const tr = document.createElement("tr"); tr.className = "monMirRow"; tr.innerHTML = ` ${escapeHtml(entry.description || "—")} ${escapeHtml(entry.module || "—")} ${escapeHtml(formatTime(entry.ts))} ${ICONS.download} `; listEl.appendChild(tr); }); } async function refreshErrorLogs() { const data = await apiJson("/api/monitoring/error_logs"); store.errorLogItems = Array.isArray(data.items) ? data.items : []; renderErrorLogs(); } function showErrorLogs() { store.view = "error-logs"; store.section = "error-logs"; showView(el("monitoringErrorLogsView")); startPoll(refreshErrorLogs); void refreshErrorLogs(); } // ——— Hardware health ——— function renderHardware() { const listEl = el("hardwareGroupsList"); const emptyEl = el("hardwareListEmpty"); if (!listEl) return; const groups = store.hardwareGroups; listEl.innerHTML = ""; if (emptyEl) emptyEl.hidden = groups.length > 0; if (!groups.length) return; groups.forEach((group) => { const gid = group.id || group.name; const expanded = store.hardwareExpanded.has(gid); const section = document.createElement("section"); section.className = "monHwGroup"; section.innerHTML = `
`; const body = section.querySelector(".monHwGroupBody"); const components = Array.isArray(group.components) ? group.components : []; components.forEach((c) => { const row = document.createElement("div"); row.className = "monHwComponent"; row.innerHTML = ` ${escapeHtml(c.name || c.id || "—")} ${escapeHtml(c.message || c.status_label || "")}`; body?.appendChild(row); }); listEl.appendChild(section); }); } async function refreshHardware() { const data = await apiJson("/api/monitoring/hardware_health"); store.hardwareGroups = Array.isArray(data.groups) ? data.groups : []; renderHardware(); } function showHardware() { store.view = "hardware-health"; store.section = "hardware-health"; showView(el("monitoringHardwareView")); startPoll(refreshHardware); void refreshHardware(); } // ——— Safety ——— function renderSafety() { const s = store.safety || {}; const estop = s.emergency_stop || "released"; const front = s.front_scanner || "free"; const rear = s.rear_scanner || "free"; const setCard = (statusEl, cardEl, value, okValues, okKey, badKey) => { if (!statusEl || !cardEl) return; const ok = okValues.includes(value); statusEl.textContent = ok ? t(okKey) : t(badKey); cardEl.classList.toggle("monSafetyCard--ok", ok); cardEl.classList.toggle("monSafetyCard--bad", !ok); }; setCard(el("safetyEstopStatus"), el("safetyEstopCard"), estop, ["released"], "monitoring.safety.estopReleased", "monitoring.safety.estopActivated"); setCard(el("safetyFrontStatus"), el("safetyFrontCard"), front, ["free"], "monitoring.safety.scannerFree", "monitoring.safety.scannerBlocked"); setCard(el("safetyRearStatus"), el("safetyRearCard"), rear, ["free"], "monitoring.safety.scannerFree", "monitoring.safety.scannerBlocked"); } async function refreshSafety() { store.safety = await apiJson("/api/monitoring/safety"); renderSafety(); } function showSafety() { store.view = "safety-system"; store.section = "safety-system"; showView(el("monitoringSafetyView")); startPoll(refreshSafety); void refreshSafety(); } // ——— Mission log (Phase 1) ——— const missionLogViewEl = () => el("monitoringMissionLogView"); const actionLogViewEl = () => el("monitoringActionLogView"); const missionLogListEl = () => el("missionLogList"); const actionLogListEl = () => el("actionLogList"); function findEntry(id) { return ( store.queue.find((e) => e && e.id === id) || store.runs.find((e) => e && e.id === id) || null ); } function missionLogEntries() { const items = [...store.runs, ...store.queue]; const seen = new Set(); return items.filter((e) => { if (!e || typeof e !== "object") return false; if (seen.has(e.id)) return false; seen.add(e.id); const status = String(e.status || ""); return status === "executing" || status === "completed" || status === "failed" || status === "cancelled"; }); } function formatDuration(ms) { if (!Number.isFinite(ms) || ms < 0) return "—"; const sec = Math.floor(ms / 1000); if (sec < 60) return `${sec}s`; const min = Math.floor(sec / 60); const rem = sec % 60; if (min < 60) return `${min}m ${rem}s`; const hr = Math.floor(min / 60); return `${hr}h ${min % 60}m`; } function entryDuration(entry) { const start = entry.started_at ? new Date(entry.started_at).getTime() : NaN; if (!Number.isFinite(start)) return "—"; const end = entry.finished_at ? new Date(entry.finished_at).getTime() : Date.now(); return formatDuration(end - start); } function entryMessage(entry) { if (entry.status === "executing") { const cur = store.runner?.current_action; if (cur) return String(cur); return store.runner?.message || t("monitoring.missionLog.running"); } const log = Array.isArray(entry.log) ? entry.log : []; const last = log.length ? log[log.length - 1] : null; if (last?.message) return String(last.message); if (entry.status === "failed") return t("monitoring.missionLog.stateFailed"); if (entry.status === "cancelled") return t("monitoring.missionLog.stateCancelled"); if (entry.status === "completed") return t("monitoring.missionLog.stateCompleted"); return "—"; } function formatStartedBy(entry) { const src = String(entry.source || "ui"); const key = `monitoring.missionLog.source.${src}`; const label = t(key); return label === key ? src : label; } function stateLabel(status) { const key = `monitoring.missionLog.state.${status}`; const label = t(key); return label === key ? status : label; } function stateClass(status) { if (status === "executing") return "monMirState--running"; if (status === "completed") return "monMirState--ok"; if (status === "failed") return "monMirState--error"; if (status === "cancelled") return "monMirState--warn"; return ""; } function levelLabel(level) { const key = `monitoring.actionLog.level.${level || "info"}`; const label = t(key); return label === key ? level || "info" : label; } function levelClass(level) { if (level === "error") return "monMirState--error"; if (level === "warn") return "monMirState--warn"; if (level === "user") return "monMirState--user"; return "monMirState--ok"; } function filteredMissionEntries() { const q = store.missionFilter.trim().toLowerCase(); let items = missionLogEntries(); if (q) { items = items.filter((e) => { const name = String(e.mission_name || "").toLowerCase(); const state = stateLabel(e.status).toLowerCase(); return name.includes(q) || state.includes(q) || String(e.status).includes(q); }); } return items.sort((a, b) => { const ta = new Date(a.started_at || a.created_at || 0).getTime(); const tb = new Date(b.started_at || b.created_at || 0).getTime(); return tb - ta; }); } function renderMissionLog() { const listEl = missionLogListEl(); if (!listEl) return; const items = filteredMissionEntries(); const total = items.length; const pages = pageCount(total); if (store.missionPage > pages) store.missionPage = pages; const start = (store.missionPage - 1) * PAGE_SIZE; const pageItems = items.slice(start, start + PAGE_SIZE); if (el("missionLogFilterCount")) el("missionLogFilterCount").textContent = t("monitoring.itemsFound", { n: total }); if (el("missionLogPageLabel")) el("missionLogPageLabel").textContent = t("monitoring.pageOf", { page: store.missionPage, total: pages }); listEl.innerHTML = ""; const tableEl = el("missionLogTable"); const emptyEl = el("missionLogListEmpty"); if (tableEl) tableEl.hidden = total === 0; if (emptyEl) { emptyEl.hidden = total > 0; emptyEl.textContent = store.missionFilter ? t("monitoring.missionLog.emptyFilter") : t("monitoring.missionLog.empty"); } pageItems.forEach((entry) => { const status = entry.status || "—"; const isPersisted = entry.__persisted === true; const tr = document.createElement("tr"); tr.className = "monMirRow"; tr.innerHTML = ` ${ICONS.mission} ${escapeHtml(entry.mission_name || entry.mission_id || "—")} ${escapeHtml(stateLabel(status))} ${escapeHtml(entryMessage(entry))} ${escapeHtml(formatTime(entry.started_at || entry.created_at))} ${escapeHtml(entryDuration(entry))} ${escapeHtml(formatStartedBy(entry))} ${ isPersisted && status !== "executing" ? `` : "" } ${ isPersisted && status !== "executing" ? `${ICONS.download}` : "" } `; listEl.appendChild(tr); }); } function buildActionRows(entry) { const rows = []; const cached = store.runActions.get(entry?.id); const log = Array.isArray(entry?.log) ? entry.log : Array.isArray(cached) ? cached : []; log.forEach((line, idx) => { const next = log[idx + 1]; const ts = line.ts ? new Date(line.ts).getTime() : NaN; const nextTs = next?.ts ? new Date(next.ts).getTime() : NaN; const ranFor = Number.isFinite(ts) && Number.isFinite(nextTs) ? formatDuration(nextTs - ts) : "—"; rows.push({ action: line.action || line.message || "—", state: line.level || "info", message: line.message || "—", start: line.ts, ranFor, current: false, }); }); if (entry?.status === "executing" && store.runner?.current_queue_id === entry.id) { const cur = store.runner.current_action; if (cur) { rows.unshift({ action: cur, state: "executing", message: store.runner.message || cur, start: store.runner.updated_at || null, ranFor: "—", current: true, }); } } return rows; } function filteredActionRows(entry) { const q = store.actionFilter.trim().toLowerCase(); let rows = buildActionRows(entry); if (q) { rows = rows.filter((r) => { return ( String(r.action).toLowerCase().includes(q) || String(r.message).toLowerCase().includes(q) || levelLabel(r.state).toLowerCase().includes(q) ); }); } return rows; } function renderActionLog() { const listEl = actionLogListEl(); if (!listEl) return; const entry = findEntry(store.selectedEntryId); if (!entry) { listEl.innerHTML = ""; if (el("actionLogListEmpty")) { el("actionLogListEmpty").hidden = false; el("actionLogListEmpty").textContent = t("monitoring.actionLog.missingEntry"); } return; } const rows = filteredActionRows(entry); const total = rows.length; const pages = pageCount(total); if (store.actionPage > pages) store.actionPage = pages; const start = (store.actionPage - 1) * PAGE_SIZE; const pageItems = rows.slice(start, start + PAGE_SIZE); if (el("actionLogFilterCount")) el("actionLogFilterCount").textContent = t("monitoring.itemsFound", { n: total }); if (el("actionLogPageLabel")) el("actionLogPageLabel").textContent = t("monitoring.pageOf", { page: store.actionPage, total: pages }); listEl.innerHTML = ""; const tableEl = el("actionLogTable"); const emptyEl = el("actionLogListEmpty"); if (tableEl) tableEl.hidden = total === 0; if (emptyEl) { emptyEl.hidden = total > 0; emptyEl.textContent = store.actionFilter ? t("monitoring.actionLog.emptyFilter") : t("monitoring.actionLog.empty"); } pageItems.forEach((row) => { const tr = document.createElement("tr"); tr.className = `monMirRow${row.current ? " monMirRow--current" : ""}`; tr.innerHTML = ` ${ICONS.action} ${escapeHtml(row.action)} ${escapeHtml(row.state === "executing" ? t("monitoring.actionLog.state.executing") : levelLabel(row.state))} ${escapeHtml(row.message)} ${escapeHtml(formatTime(row.start))} ${escapeHtml(row.ranFor)}`; listEl.appendChild(tr); }); } async function refreshMissionLog() { const [queueData, runsData] = await Promise.all([ apiJson("/api/mission_queue"), apiJson("/api/monitoring/mission_runs?limit=500"), ]); store.queue = Array.isArray(queueData.queue) ? queueData.queue : []; store.runner = queueData.runner && typeof queueData.runner === "object" ? queueData.runner : {}; store.runs = (Array.isArray(runsData.runs) ? runsData.runs : []).map((r) => ({ ...r, __persisted: true })); if (store.view === "mission-log") renderMissionLog(); else if (store.view === "action-log") renderActionLog(); } function showMissionLogList() { store.view = "mission-log"; store.section = "mission-log"; store.selectedEntryId = null; showView(missionLogViewEl()); startPoll(refreshMissionLog); void refreshMissionLog(); } function showActionLog(entryId) { store.view = "action-log"; store.selectedEntryId = entryId; store.actionFilter = ""; store.actionPage = 1; if (el("actionLogFilterInput")) el("actionLogFilterInput").value = ""; const entry = findEntry(entryId); if (el("actionLogSubtitle")) { el("actionLogSubtitle").textContent = entry ? t("monitoring.actionLog.subtitleMission", { name: entry.mission_name || entry.mission_id || "—" }) : ""; } const dl = el("actionLogDownloadBtn"); if (dl) { const persisted = entry && entry.__persisted === true; dl.hidden = !persisted; if (persisted) dl.href = `/api/monitoring/mission_runs/${encodeURIComponent(entryId)}/download`; else dl.href = "#"; } showView(actionLogViewEl()); startPoll(refreshMissionLog); if (entry && (!Array.isArray(entry.log) || entry.log.length === 0) && !store.runActions.has(entryId)) { apiJson(`/api/monitoring/mission_runs/${encodeURIComponent(entryId)}/actions`) .then((data) => { const items = Array.isArray(data.items) ? data.items : []; store.runActions.set(entryId, items); renderActionLog(); }) .catch(() => {}); } renderActionLog(); } function showSection(section) { store.section = section; if (section === "analytics") showAnalytics(); else if (section === "monitoring-log") showSystemLog(); else if (section === "error-logs") showErrorLogs(); else if (section === "hardware-health") showHardware(); else if (section === "safety-system") showSafety(); else if (section === "mission-log") showMissionLogList(); else showAnalytics(); } function bindPager(prefix, getTotal, getPage, setPage, render) { const bind = (id, fn) => el(id)?.addEventListener("click", fn); bind(`${prefix}PageFirst`, () => { setPage(1); render(); }); bind(`${prefix}PagePrev`, () => { setPage(Math.max(1, getPage() - 1)); render(); }); bind(`${prefix}PageNext`, () => { setPage(getPage() + 1); render(); }); bind(`${prefix}PageLast`, () => { setPage(pageCount(getTotal())); render(); }); } function bindEvents() { initAnalyticsPresets(); el("analyticsRefreshBtn")?.addEventListener("click", () => refreshAnalytics().catch((e) => alert(e.message))); el("analyticsHelpBtn")?.addEventListener("click", () => alert(t("monitoring.analytics.helpBody"))); ["analyticsStartDate", "analyticsEndDate", "analyticsGrouping", "analyticsChartMode"].forEach((id) => { el(id)?.addEventListener("change", () => refreshAnalytics().catch(() => {})); }); el("analyticsPresets")?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-preset-days]"); if (!btn) return; const days = Number(btn.dataset.presetDays); const today = formatDateYmd(new Date()); setAnalyticsDateRange(addDaysYmd(today, days), today); refreshAnalytics().catch(() => {}); }); el("systemLogFilterInput")?.addEventListener("input", () => { store.systemLogFilter = el("systemLogFilterInput").value; store.systemLogPage = 1; renderSystemLog(); }); el("systemLogClearFiltersBtn")?.addEventListener("click", () => { store.systemLogFilter = ""; store.systemLogPage = 1; if (el("systemLogFilterInput")) el("systemLogFilterInput").value = ""; renderSystemLog(); }); el("systemLogRefreshBtn")?.addEventListener("click", () => refreshSystemLog().catch((e) => alert(e.message))); bindPager("systemLog", () => filteredSystemLog().length, () => store.systemLogPage, (p) => { store.systemLogPage = p; }, renderSystemLog); el("errorLogsFilterInput")?.addEventListener("input", () => { store.errorLogFilter = el("errorLogsFilterInput").value; renderErrorLogs(); }); el("errorLogsRefreshBtn")?.addEventListener("click", () => refreshErrorLogs().catch((e) => alert(e.message))); el("errorLogsGenerateBtn")?.addEventListener("click", () => { apiJson("/api/monitoring/error_logs/generate", { method: "POST" }) .then(() => refreshErrorLogs()) .catch((e) => alert(e.message)); }); el("errorLogsDeleteAllBtn")?.addEventListener("click", () => { if (!window.confirm(t("monitoring.errorLogs.deleteAllConfirm"))) return; apiJson("/api/monitoring/error_logs", { method: "DELETE" }) .then(() => refreshErrorLogs()) .catch((e) => alert(e.message)); }); el("errorLogsList")?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-delete-error]"); if (!btn?.dataset.deleteError) return; apiJson(`/api/monitoring/error_logs/${encodeURIComponent(btn.dataset.deleteError)}`, { method: "DELETE" }) .then(() => refreshErrorLogs()) .catch((e) => alert(e.message)); }); el("hardwareRefreshBtn")?.addEventListener("click", () => refreshHardware().catch((e) => alert(e.message))); el("hardwareGroupsList")?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-hw-toggle]"); if (!btn?.dataset.hwToggle) return; const gid = btn.dataset.hwToggle; if (store.hardwareExpanded.has(gid)) store.hardwareExpanded.delete(gid); else store.hardwareExpanded.add(gid); renderHardware(); }); el("missionLogFilterInput")?.addEventListener("input", () => { store.missionFilter = el("missionLogFilterInput").value; store.missionPage = 1; renderMissionLog(); }); el("missionLogClearFiltersBtn")?.addEventListener("click", () => { store.missionFilter = ""; store.missionPage = 1; if (el("missionLogFilterInput")) el("missionLogFilterInput").value = ""; renderMissionLog(); }); el("missionLogRefreshBtn")?.addEventListener("click", () => refreshMissionLog().catch((e) => alert(e.message))); el("missionLogClearHistoryBtn")?.addEventListener("click", () => { if (!window.confirm(t("monitoring.missionLog.clearHistoryConfirm"))) return; apiJson("/api/monitoring/mission_runs", { method: "DELETE" }) .then(() => refreshMissionLog()) .catch((e) => alert(e.message)); }); el("missionLogHelpBtn")?.addEventListener("click", () => alert(t("monitoring.missionLog.helpBody"))); missionLogListEl()?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-view-log]"); if (!btn?.dataset.viewLog) return; showActionLog(btn.dataset.viewLog); }); missionLogListEl()?.addEventListener("click", (evt) => { const btn = evt.target.closest("[data-delete-run]"); if (!btn?.dataset.deleteRun) return; if (!window.confirm(t("monitoring.missionLog.deleteRunConfirm"))) return; apiJson(`/api/monitoring/mission_runs/${encodeURIComponent(btn.dataset.deleteRun)}`, { method: "DELETE" }) .then(() => refreshMissionLog()) .catch((e) => alert(e.message)); }); el("actionLogBackBtn")?.addEventListener("click", () => showMissionLogList()); el("actionLogFilterInput")?.addEventListener("input", () => { store.actionFilter = el("actionLogFilterInput").value; store.actionPage = 1; renderActionLog(); }); el("actionLogClearFiltersBtn")?.addEventListener("click", () => { store.actionFilter = ""; store.actionPage = 1; if (el("actionLogFilterInput")) el("actionLogFilterInput").value = ""; renderActionLog(); }); bindPager("missionLog", () => filteredMissionEntries().length, () => store.missionPage, (p) => { store.missionPage = p; }, renderMissionLog); bindPager("actionLog", () => { const entry = findEntry(store.selectedEntryId); return entry ? filteredActionRows(entry).length : 0; }, () => store.actionPage, (p) => { store.actionPage = p; }, renderActionLog); window.addEventListener("lm:locale-change", () => { if (store.view === "analytics") renderAnalyticsChart(); else if (store.view === "system-log") renderSystemLog(); else if (store.view === "error-logs") renderErrorLogs(); else if (store.view === "hardware-health") renderHardware(); else if (store.view === "safety-system") renderSafety(); else if (store.view === "mission-log") renderMissionLog(); else if (store.view === "action-log") renderActionLog(); }); } function onPageShow() { const section = window.NavApp?.getActiveSection?.() || store.section || "analytics"; showSection(section); } function onPageHide() { stopPoll(); } window.MonitoringApp = { init: bindEvents, onPageShow, onPageHide, showSection, refresh: refreshMissionLog }; function boot() { bindEvents(); } if (window.AuthApp?.isReady()) boot(); else window.addEventListener("lm:auth-ready", boot, { once: true }); })();