(() => {
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 });
})();