(() => {
  const STORAGE_KEY_V3 = "phenikaax_dashboard_v3";
  const STORAGE_KEY_V2 = "phenikaax_dashboard_v2";
  const PAGE_SIZE = 10;
  const DEFAULT_ID = "dashboard_default";
  const GRID_COLS = 12;
  const GRID_ROW_PX = 52;
  const GRID_GAP = 10;
  const DEFAULT_W = 4;
  const DEFAULT_H = 3;
  const MIN_W = 2;
  const MIN_H = 2;
  const MAX_W = 12;
  const MAX_H = 8;

  let dragSession = null;
  let resizeSession = null;

  const el = (id) => document.getElementById(id);
  const t = (key, vars) => window.I18n?.t(key, vars) ?? key;

  const listViewEl = el("dashboardListView");
  const createViewEl = el("dashboardCreateView");
  const designerViewEl = el("dashboardDesignerView");
  const gridEl = el("dashboardGrid");
  const designerEmptyEl = el("dashboardDesignerEmpty");
  const tableBodyEl = el("dashboardTableBody");
  const tableWrapEl = el("dashboardTableWrap");
  const filterInputEl = el("dashboardFilterInput");
  const listCountEl = el("dashboardListCount");
  const pageLabelEl = el("dashboardPageLabel");
  const designerTitleEl = el("dashboardDesignerTitle");
  const designerToolbarEl = el("dashboardDesignerToolbar");
  const editModeBtnEl = el("dashboardEditModeBtn");
  const saveBtnEl = el("dashboardSaveBtn");
  let activeWidgetTab = "missions";
  const editDialogEl = el("dashboardEditDialog");
  const permissionsDialogEl = el("dashboardPermissionsDialog");
  const addDialogEl = el("dashboardAddWidgetDialog");
  const editWidgetDialogEl = el("dashboardEditWidgetDialog");
  const addTypeEl = el("dashboardWidgetType");
  const addFieldsEl = el("dashboardAddWidgetFields");
  const editFieldsEl = el("dashboardEditWidgetFields");
  const editWidgetIdEl = el("dashboardEditWidgetId");
  const editWidgetTypeEl = el("dashboardEditWidgetType");

  const store = {
    dashboards: [],
    activeDashboardId: null,
    view: "list",
    filter: "",
    page: 1,
    editMode: false,
    pollActive: false,
    queueUnsub: null,
    userGroups: [],
    maps: [],
    mapsLoaded: false,
    ioModules: [],
    robotPose: null,
    robotPoseAt: 0,
    mapPollTimer: null,
  };

  function hasMapWidget(widgets = activeWidgets()) {
    return widgets.some((w) => w.type === "map" || w.type === "map_locked");
  }

  async function ensureMapsLoaded(force = false) {
    if (store.mapsLoaded && !force) return store.maps;
    try {
      const res = await fetch("/api/maps", { credentials: "include" });
      if (res.ok) {
        const data = await res.json();
        store.maps = Array.isArray(data.maps) ? data.maps : [];
      } else {
        store.maps = [];
      }
    } catch {
      store.maps = [];
    }
    store.mapsLoaded = true;
    return store.maps;
  }

  async function fetchRobotPose(force = false) {
    const now = Date.now();
    if (!force && store.robotPoseAt && now - store.robotPoseAt < 1200) return store.robotPose;
    try {
      const res = await fetch("/api/robot/status", { credentials: "include" });
      if (res.ok) {
        const data = await res.json();
        const pose = data.pose && typeof data.pose === "object" ? data.pose : {};
        store.robotPose = {
          x: Number(pose.x) || 0,
          y: Number(pose.y) || 0,
          yaw: Number(pose.yaw) || 0,
          mapId: data.active_map_id || null,
        };
      }
    } catch {
      /* keep last pose */
    }
    store.robotPoseAt = Date.now();
    return store.robotPose;
  }

  function mapMeta(map) {
    return {
      resolution: Number(map?.resolution) || 0.05,
      originX: Number(map?.origin_x) || 0,
      originY: Number(map?.origin_y) || 0,
    };
  }

  function worldToPixel(map, imgW, imgH, wx, wy) {
    const { resolution, originX, originY } = mapMeta(map);
    return {
      x: (wx - originX) / resolution,
      y: imgH - (wy - originY) / resolution,
    };
  }

  function defaultMapPose(map, imgW, imgH) {
    const { resolution, originX, originY } = mapMeta(map);
    return {
      x: originX + (imgW * resolution) / 2,
      y: originY + (imgH * resolution) / 2,
      yaw: 0,
    };
  }

  function resolveWidgetMap(widget) {
    const preferred = normalizeStr(widget.map_id) || store.robotPose?.mapId || "";
    if (preferred) {
      const hit = store.maps.find((m) => m.id === preferred);
      if (hit) return hit;
    }
    return store.maps[0] || null;
  }

  function poseForWidget(map, imgW, imgH) {
    const pose = store.robotPose;
    if (pose && (!pose.mapId || pose.mapId === map.id)) {
      return { x: pose.x, y: pose.y, yaw: pose.yaw };
    }
    return defaultMapPose(map, imgW, imgH);
  }

  function mapOptions(selected = "", includeActive = true) {
    const opts = [];
    if (includeActive) {
      opts.push(
        `<option value="" ${!selected ? "selected" : ""}>${escapeHtml(t("dashboard.widget.mapActive"))}</option>`
      );
    }
    store.maps.forEach((map) => {
      opts.push(
        `<option value="${escapeHtml(map.id)}" ${map.id === selected ? "selected" : ""}>${escapeHtml(map.name || map.id)}</option>`
      );
    });
    return opts.join("");
  }

  function mapImageUrl(map) {
    if (!map?.id || !map.image_file) return null;
    return `/api/maps/${encodeURIComponent(map.id)}/image`;
  }

  function applyMapLayout(viewportEl, map, locked, pose, imgW, imgH) {
    if (!viewportEl || !map) return;
    const width = imgW || 800;
    const height = imgH || 600;
    const px = worldToPixel(map, width, height, pose.x, pose.y);
    const vw = viewportEl.clientWidth || 1;
    const vh = viewportEl.clientHeight || 1;
    const layer = viewportEl.querySelector("[data-map-layer]");
    const marker = viewportEl.querySelector("[data-map-marker]");
    if (!layer || !marker) return;

    if (locked) {
      const scale = Math.max(vw / width, vh / height) * 1.35;
      const tx = vw / 2 - px.x * scale;
      const ty = vh / 2 - px.y * scale;
      layer.style.width = `${width}px`;
      layer.style.height = `${height}px`;
      layer.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
      marker.style.left = `${vw / 2}px`;
      marker.style.top = `${vh / 2}px`;
    } else {
      const scale = Math.min(vw / width, vh / height);
      const offsetX = (vw - width * scale) / 2;
      const offsetY = (vh - height * scale) / 2;
      layer.style.width = `${width}px`;
      layer.style.height = `${height}px`;
      layer.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
      marker.style.left = `${offsetX + px.x * scale}px`;
      marker.style.top = `${offsetY + px.y * scale}px`;
    }
    marker.style.transform = `translate(-50%, -50%) rotate(${pose.yaw}rad)`;
    viewportEl.dataset.mapId = map.id;
  }

  function mountMapViewport(viewportEl, map, locked) {
    const imgUrl = mapImageUrl(map);
    const label = escapeHtml(map.name || map.id);
    viewportEl.classList.toggle("dashboardMapViewport--locked", locked);
    viewportEl.innerHTML = imgUrl
      ? `
      <div class="dashboardMapLayer" data-map-layer>
        <img class="dashboardMapImage" data-map-image src="${escapeHtml(imgUrl)}" alt="${label}" draggable="false" />
      </div>
      <div class="dashboardMapMarker" data-map-marker aria-hidden="true"></div>`
      : `
      <div class="dashboardMapLayer dashboardMapLayer--grid" data-map-layer>
        <div class="dashboardMapGridFallback">
          <span class="dashboardMapGridTitle">${label}</span>
          <span class="mutedNote">${escapeHtml(t("dashboard.widget.mapNoImage"))}</span>
        </div>
      </div>
      <div class="dashboardMapMarker" data-map-marker aria-hidden="true"></div>`;

    const imgEl = viewportEl.querySelector("[data-map-image]");
    const layout = () => {
      const w = imgEl?.naturalWidth || 800;
      const h = imgEl?.naturalHeight || 600;
      const pose = poseForWidget(map, w, h);
      applyMapLayout(viewportEl, map, locked, pose, w, h);
    };

    if (imgEl) {
      if (imgEl.complete) layout();
      else {
        imgEl.addEventListener("load", layout);
        imgEl.addEventListener("error", () => {
          viewportEl.innerHTML = `<div class="dashboardMapEmpty">${escapeHtml(t("dashboard.widget.mapImageError"))}</div>`;
        });
      }
    } else {
      layout();
    }

    if (!viewportEl.dataset.roObserved) {
      viewportEl.dataset.roObserved = "1";
      if (typeof ResizeObserver !== "undefined") {
        const ro = new ResizeObserver(() => layout());
        ro.observe(viewportEl);
        viewportEl._mapRo = ro;
      }
    }
  }

  async function layoutMapWidget(widget, bodyEl, locked) {
    let viewportEl = bodyEl.querySelector("[data-map-viewport]");
    if (!viewportEl) {
      bodyEl.innerHTML = `<div class="dashboardMapViewport${locked ? " dashboardMapViewport--locked" : ""}" data-map-viewport></div>`;
      viewportEl = bodyEl.querySelector("[data-map-viewport]");
    }

    await ensureMapsLoaded();
    await fetchRobotPose();

    const map = resolveWidgetMap(widget);
    if (!map) {
      bodyEl.innerHTML = `<div class="dashboardMapEmpty">${escapeHtml(t("dashboard.widget.mapEmpty"))}</div>`;
      return;
    }

    mountMapViewport(viewportEl, map, locked);
  }

  function refreshMapWidgets() {
    activeWidgets().forEach((widget) => {
      if (widget.type !== "map" && widget.type !== "map_locked") return;
      const bodyEl = gridEl?.querySelector(`[data-widget-id="${widget.id}"] .dashboardWidgetBody`);
      if (!bodyEl) return;
      void layoutMapWidget(widget, bodyEl, widget.type === "map_locked");
    });
  }

  async function ensureIoModulesLoaded() {
    if (store.ioModules.length) return store.ioModules;
    try {
      if (window.IoModulesCatalog?.refresh) {
        store.ioModules = await window.IoModulesCatalog.refresh();
      } else {
        const res = await fetch("/api/io_modules", { credentials: "include" });
        if (res.ok) {
          const data = await res.json();
          store.ioModules = Array.isArray(data.io_modules) ? data.io_modules : [];
        }
      }
    } catch {
      store.ioModules = [];
    }
    return store.ioModules;
  }

  function ioModuleOptions(selectedId) {
    const list = store.ioModules || [];
    if (!list.length) return `<option value="">${escapeHtml(t("ioModules.empty"))}</option>`;
    return list
      .map(
        (m) =>
          `<option value="${escapeHtml(m.id)}" ${m.id === selectedId ? "selected" : ""}>${escapeHtml(m.name || m.id)}</option>`
      )
      .join("");
  }

  function ioModuleById(id) {
    return (store.ioModules || []).find((m) => m.id === id) || null;
  }

  async function setIoModuleConnected(id, connect) {
    const path = connect ? "connect" : "disconnect";
    const res = await fetch(`/api/io_modules/${encodeURIComponent(id)}/${path}`, {
      method: "POST",
      credentials: "include",
    });
    if (!res.ok) {
      const text = await res.text();
      throw new Error(text || res.statusText);
    }
    const data = await res.json();
    const idx = store.ioModules.findIndex((m) => m.id === id);
    if (idx >= 0) store.ioModules[idx] = data;
    return data;
  }

  function renderIoConnectWidget(widget, bodyEl) {
    if (!bodyEl) return;
    const mod = ioModuleById(widget.io_module_id);
    const connected = !!mod?.connected;
    const label = widget.title || mod?.name || t("dashboard.widget.io_connect");
    bodyEl.innerHTML = `
      <button type="button" class="dashboardMirMissionBtn dashboardIoConnectBtn" data-io-connect="${escapeHtml(widget.io_module_id || "")}">
        ${escapeHtml(connected ? t("dashboard.widget.io_disconnect") : t("dashboard.widget.io_connectAction"))}
      </button>
      <p class="mutedNote dashboardIoWidgetMeta">${escapeHtml(label)} · ${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</p>`;
    const btn = bodyEl.querySelector("[data-io-connect]");
    btn?.addEventListener("click", async () => {
      if (!widget.io_module_id) return;
      try {
        await setIoModuleConnected(widget.io_module_id, !connected);
        renderIoConnectWidget(widget, bodyEl);
      } catch (e) {
        alert(e.message);
      }
    });
  }

  function renderIoStatusWidget(widget, bodyEl) {
    if (!bodyEl) return;
    const mod = ioModuleById(widget.io_module_id);
    const label = widget.title || mod?.name || t("dashboard.widget.io_status");
    bodyEl.innerHTML = `
      <div class="dashboardIoStatusCard">
        <div class="dashboardIoStatusName">${escapeHtml(label)}</div>
        <div class="dashboardIoStatusLine">${escapeHtml(mod?.module_type ? t(`ioModules.type.${mod.module_type}`) : "—")} · ${escapeHtml(mod?.ip_address || "—")}</div>
        <div class="dashboardIoStatusBadge ${mod?.connected ? "is-on" : "is-off"}">${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</div>
      </div>`;
  }

  async function fetchIoRuntime(moduleId) {
    const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/runtime`, { credentials: "include" });
    if (!res.ok) throw new Error(res.statusText);
    return res.json();
  }

  async function setIoOutput(moduleId, port, value, timeoutMs = 0) {
    const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/output`, {
      method: "POST",
      credentials: "include",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ output: port, value, timeout_ms: timeoutMs }),
    });
    if (!res.ok) {
      const text = await res.text();
      throw new Error(text || res.statusText);
    }
    return res.json();
  }

  function ioPortRange(mod) {
    return mod?.module_type === "wise" ? [0, 1, 2, 3] : [1, 2, 3, 4];
  }

  function renderIoConfigurationWidget(widget, bodyEl) {
    if (!bodyEl) return;
    const mod = ioModuleById(widget.io_module_id);
    const label = widget.title || mod?.name || t("dashboard.widget.io_configuration");
    if (!mod) {
      bodyEl.innerHTML = `<p class="mutedNote">${escapeHtml(t("dashboard.widget.configHint"))}</p>`;
      return;
    }
    const ports = ioPortRange(mod);
    bodyEl.innerHTML = `
      <div class="dashboardIoConfigHead">
        <div class="dashboardIoStatusName">${escapeHtml(label)}</div>
        <div class="dashboardIoStatusBadge ${mod.connected ? "is-on" : "is-off"}">${escapeHtml(mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected"))}</div>
      </div>
      <div class="dashboardIoConfigPorts" data-io-config-ports></div>`;
    const portsEl = bodyEl.querySelector("[data-io-config-ports]");
    ports.forEach((port) => {
      const row = document.createElement("div");
      row.className = "dashboardIoConfigRow";
      row.innerHTML = `
        <span class="dashboardIoConfigPortLabel">${escapeHtml(t("dashboard.widget.ioPort", { port }))}</span>
        <button type="button" class="dashboardIoConfigToggle" data-port="${port}" data-value="off">${escapeHtml(t("dashboard.widget.ioOff"))}</button>
        <button type="button" class="dashboardIoConfigToggle is-primary" data-port="${port}" data-value="on">${escapeHtml(t("dashboard.widget.ioOn"))}</button>`;
      portsEl.appendChild(row);
    });
    const refresh = async () => {
      try {
        const rt = await fetchIoRuntime(mod.id);
        portsEl.querySelectorAll("[data-port]").forEach((btn) => {
          const port = Number(btn.dataset.port);
          const outputs = Array.isArray(rt.outputs) ? rt.outputs : [];
          const idx = mod.module_type === "wise" ? port : port;
          const on = !!outputs[idx];
          btn.classList.toggle("is-active", (btn.dataset.value === "on") === on);
        });
      } catch {
        /* ignore */
      }
    };
    portsEl.addEventListener("click", async (evt) => {
      const btn = evt.target.closest("[data-port]");
      if (!btn || !widget.io_module_id) return;
      const port = Number(btn.dataset.port);
      const on = btn.dataset.value === "on";
      try {
        await setIoOutput(widget.io_module_id, port, on);
        await refresh();
      } catch (e) {
        alert(e.message);
      }
    });
    void refresh();
  }

  function widgetTypeLabel(type) {
    return t(`dashboard.widget.${type}`) || type;
  }

  function newId(prefix = "d") {
    if (typeof crypto !== "undefined" && crypto.randomUUID) return `${prefix}_${crypto.randomUUID()}`;
    return `${prefix}_${Date.now().toString(36)}`;
  }

  function missions() {
    return window.MissionsApp || null;
  }

  function currentUser() {
    return window.AuthApp?.getUser?.() || null;
  }

  function canEditDashboardsModule() {
    const user = currentUser();
    if (!user?.permissions) return true;
    const level = user.permissions.dashboard;
    return level === "write" || level === undefined;
  }

  function activeDashboard() {
    return store.dashboards.find((d) => d.id === store.activeDashboardId) || null;
  }

  function dashboardCanEdit(dashboard) {
    if (!dashboard) return false;
    if (!canEditDashboardsModule()) return false;
    const user = currentUser();
    if (!user) return true;
    const groups = Array.isArray(dashboard.editGroups) ? dashboard.editGroups : [];
    if (!groups.length) return true;
    return groups.includes(user.group_id);
  }

  function normalizeStr(value) {
    return String(value || "").trim();
  }

  function escapeHtml(str) {
    return String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function activeWidgets() {
    return activeDashboard()?.widgets || [];
  }

  function setActiveWidgets(widgets) {
    const db = activeDashboard();
    if (db) db.widgets = widgets;
  }

  function clamp(n, min, max) {
    return Math.min(max, Math.max(min, n));
  }

  function hasGridPos(widget) {
    return Number.isFinite(widget?.col) && Number.isFinite(widget?.row);
  }

  function normalizeWidget(widget) {
    if (!widget || typeof widget !== "object") return widget;
    widget.w = clamp(Number(widget.w) || DEFAULT_W, MIN_W, MAX_W);
    widget.h = clamp(Number(widget.h) || DEFAULT_H, MIN_H, MAX_H);
    if (widget.col != null) widget.col = clamp(Math.round(Number(widget.col)), 1, GRID_COLS);
    if (widget.row != null) widget.row = Math.max(1, Math.round(Number(widget.row)));
    return widget;
  }

  function gridMetrics() {
    const rect = gridEl.getBoundingClientRect();
    const colW = (rect.width - GRID_GAP * (GRID_COLS - 1)) / GRID_COLS;
    return { rect, colW, rowH: GRID_ROW_PX, gap: GRID_GAP };
  }

  function pointerToGrid(clientX, clientY) {
    const { rect, colW, rowH, gap } = gridMetrics();
    const x = Math.max(0, clientX - rect.left);
    const y = Math.max(0, clientY - rect.top);
    const col = clamp(Math.floor(x / (colW + gap)) + 1, 1, GRID_COLS);
    const row = Math.max(1, Math.floor(y / (rowH + gap)) + 1);
    return { col, row };
  }

  function rectsOverlap(c1, r1, w1, h1, c2, r2, w2, h2) {
    return c1 < c2 + w2 && c1 + w1 > c2 && r1 < r2 + h2 && r1 + h1 > r2;
  }

  function widgetFits(widgets, ignoreId, col, row, w, h) {
    if (col < 1 || row < 1 || col + w - 1 > GRID_COLS) return false;
    return !widgets.some((other) => {
      if (other.id === ignoreId || !hasGridPos(other)) return false;
      return rectsOverlap(col, row, w, h, other.col, other.row, other.w, other.h);
    });
  }

  function findFreeGridSpot(widgets, w, h) {
    const maxRow = widgets.reduce((max, item) => {
      if (!hasGridPos(item)) return max;
      return Math.max(max, item.row + item.h);
    }, 4);
    for (let row = 1; row <= maxRow + 6; row += 1) {
      for (let col = 1; col <= GRID_COLS - w + 1; col += 1) {
        if (widgetFits(widgets, null, col, row, w, h)) return { col, row };
      }
    }
    return { col: 1, row: maxRow + 1 };
  }

  function ensureWidgetPositions(widgets) {
    let cursorCol = 1;
    let cursorRow = 1;
    let rowHeight = 0;

    widgets.forEach((widget) => {
      normalizeWidget(widget);
      if (hasGridPos(widget)) return;

      if (cursorCol + widget.w - 1 > GRID_COLS) {
        cursorRow += rowHeight || widget.h;
        cursorCol = 1;
        rowHeight = 0;
      }

      widget.col = cursorCol;
      widget.row = cursorRow;
      cursorCol += widget.w;
      rowHeight = Math.max(rowHeight, widget.h);

      if (cursorCol > GRID_COLS) {
        cursorRow += rowHeight;
        cursorCol = 1;
        rowHeight = 0;
      }
    });
  }

  function updateGridCanvasHeight(widgets) {
    if (!gridEl) return;
    let maxRowEnd = 4;
    widgets.forEach((widget) => {
      if (hasGridPos(widget)) maxRowEnd = Math.max(maxRowEnd, widget.row + widget.h - 1);
    });
    const h = maxRowEnd * GRID_ROW_PX + Math.max(0, maxRowEnd - 1) * GRID_GAP;
    gridEl.style.minHeight = `${h}px`;
  }

  function normalizeDashboards() {
    store.dashboards.forEach((db) => {
      if (!Array.isArray(db.widgets)) db.widgets = [];
      ensureWidgetPositions(db.widgets);
    });
  }

  function bootstrapDefaultDashboard(widgets = []) {
    store.dashboards = [
      {
        id: DEFAULT_ID,
        name: "Default Dashboard",
        createdBy: t("dashboard.createdBy.system"),
        createdByUser: null,
        isDefault: true,
        editGroups: ["group_administrators", "group_distributors", "group_users"],
        widgets: Array.isArray(widgets) ? widgets : [],
      },
    ];
    store.activeDashboardId = DEFAULT_ID;
    persistStore();
  }

  function migrateFromV2() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY_V2);
      if (!raw) return false;
      const data = JSON.parse(raw);
      const widgets = Array.isArray(data.widgets) ? data.widgets : [];
      bootstrapDefaultDashboard(widgets);
      return true;
    } catch {
      return false;
    }
  }

  function loadStoreLocal() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY_V3);
      if (!raw) {
        if (!migrateFromV2()) bootstrapDefaultDashboard();
        return;
      }
      const data = JSON.parse(raw);
      store.dashboards = Array.isArray(data.dashboards) ? data.dashboards : [];
      store.activeDashboardId = data.activeDashboardId || store.dashboards[0]?.id || null;
      if (!store.dashboards.length) bootstrapDefaultDashboard();
      else if (!store.activeDashboardId) store.activeDashboardId = store.dashboards[0].id;
      normalizeDashboards();
    } catch {
      bootstrapDefaultDashboard();
    }
  }

  let persistTimer = null;

  async function loadStoreFromBackend() {
    try {
      const res = await fetch("/api/dashboards", { credentials: "include" });
      if (!res.ok) {
        loadStoreLocal();
        return;
      }
      const data = await res.json();
      store.dashboards = Array.isArray(data.dashboards) ? data.dashboards : [];
      store.activeDashboardId = data.activeDashboardId || store.dashboards[0]?.id || null;
      if (!store.dashboards.length) bootstrapDefaultDashboard();
      else if (!store.activeDashboardId) store.activeDashboardId = store.dashboards[0].id;
      normalizeDashboards();
    } catch {
      loadStoreLocal();
    }
  }

  function persistStore() {
    clearTimeout(persistTimer);
    persistTimer = setTimeout(syncStoreToBackend, 400);
  }

  async function syncStoreToBackend() {
    try {
      await fetch("/api/dashboards", {
        credentials: "include",
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          dashboards: store.dashboards,
          activeDashboardId: store.activeDashboardId,
        }),
      });
    } catch {
      /* keep in-memory state; retry on next persist */
    }
  }

  async function loadUserGroups() {
    try {
      const res = await fetch("/api/user_groups", { credentials: "include" });
      if (!res.ok) return;
      const data = await res.json();
      store.userGroups = Array.isArray(data.groups) ? data.groups : [];
    } catch {
      store.userGroups = [];
    }
  }

  function filteredDashboards() {
    const q = normalizeStr(store.filter).toLowerCase();
    const list = [...store.dashboards];
    list.sort((a, b) => {
      if (a.isDefault && !b.isDefault) return -1;
      if (!a.isDefault && b.isDefault) return 1;
      return a.name.localeCompare(b.name);
    });
    if (!q) return list;
    return list.filter((d) => d.name.toLowerCase().includes(q));
  }

  function paginatedDashboards() {
    const all = filteredDashboards();
    const totalPages = Math.max(1, Math.ceil(all.length / PAGE_SIZE));
    if (store.page > totalPages) store.page = totalPages;
    if (store.page < 1) store.page = 1;
    const start = (store.page - 1) * PAGE_SIZE;
    return { all, pageItems: all.slice(start, start + PAGE_SIZE), totalPages };
  }

  function renderPermissionsChecklist(container, selected = []) {
    if (!container) return;
    container.replaceChildren();
    const selectedSet = new Set(selected);
    store.userGroups.forEach((group) => {
      const label = document.createElement("label");
      const input = document.createElement("input");
      input.type = "checkbox";
      input.value = group.id;
      input.checked = selectedSet.has(group.id);
      label.appendChild(input);
      label.appendChild(document.createTextNode(group.name || group.id));
      container.appendChild(label);
    });
    if (!store.userGroups.length) {
      const note = document.createElement("p");
      note.className = "mutedNote";
      note.textContent = "—";
      container.appendChild(note);
    }
  }

  function readPermissionsChecklist(container) {
    const ids = [];
    container?.querySelectorAll('input[type="checkbox"]:checked').forEach((node) => {
      ids.push(node.value);
    });
    return ids;
  }

  function refreshNav() {
    window.NavApp?.refreshFlyout?.();
  }

  function setView(view) {
    store.view = view;
    document.querySelector(".dashboardShell")?.classList.toggle("dashboardShell--designer", view === "designer");
    if (listViewEl) listViewEl.hidden = view !== "list";
    if (createViewEl) createViewEl.hidden = view !== "create";
    if (designerViewEl) designerViewEl.hidden = view !== "designer";
    if (view === "list") {
      stopDashboardPoll();
      renderListUI();
    } else if (view === "create") {
      stopDashboardPoll();
    } else {
      syncDesignerEditMode();
      renderDesignerChrome();
      renderDashboard();
      startDashboardPoll();
    }
  }

  function setActiveDashboard(id) {
    if (!store.dashboards.some((d) => d.id === id)) return;
    store.activeDashboardId = id;
    persistStore();
    refreshNav();
  }

  function renderListUI() {
    const { all, pageItems, totalPages } = paginatedDashboards();

    if (listCountEl) listCountEl.textContent = t("dashboard.list.itemsFound", { count: all.length });
    if (pageLabelEl) pageLabelEl.textContent = t("dashboard.list.pageOf", { page: store.page, total: totalPages });

    document.querySelectorAll(".dashboardPageBtn").forEach((btn) => {
      const action = btn.dataset.pageAction;
      if (action === "first" || action === "prev") btn.disabled = store.page <= 1;
      else if (action === "next" || action === "last") btn.disabled = store.page >= totalPages;
    });

    if (tableBodyEl) {
      tableBodyEl.replaceChildren();
      if (!pageItems.length) {
        const tr = document.createElement("tr");
        tr.className = "dashboardTableEmptyRow";
        tr.innerHTML = `<td colspan="4">${escapeHtml(t("dashboard.list.empty"))}</td>`;
        tableBodyEl.appendChild(tr);
      } else {
        pageItems.forEach((dashboard) => {
          const tr = document.createElement("tr");
          tr.className = "dashboardTableRow";
          if (dashboard.isDefault) tr.classList.add("is-default");
          if (dashboard.id === store.activeDashboardId) tr.classList.add("is-active");
          const canEdit = dashboardCanEdit(dashboard);
          const isActive = dashboard.id === store.activeDashboardId;
          tr.innerHTML = `
            <td class="dashboardTableStatusCol">
              ${isActive ? `<span class="dashboardActiveMark" aria-label="${escapeHtml(t("dashboard.list.active"))}">✓</span>` : `<span class="dashboardActiveMark dashboardActiveMark--placeholder" aria-hidden="true"></span>`}
            </td>
            <td class="dashboardTableNameCell">${escapeHtml(dashboard.name)}</td>
            <td class="dashboardTableCreatedCell">${escapeHtml(dashboard.createdBy || "—")}</td>
            <td class="dashboardTableFuncCol">
              <div class="dashboardFuncBtns">
                <button type="button" class="dashboardFuncBtn" data-action="design" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.design"))}" aria-label="${escapeHtml(t("dashboard.list.design"))}">
                  <span class="dashboardFuncIcon dashboardFuncIcon--design" aria-hidden="true"></span>
                </button>
                <button type="button" class="dashboardFuncBtn" data-action="edit" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.edit"))}" aria-label="${escapeHtml(t("dashboard.list.edit"))}" ${canEdit ? "" : "disabled"}>
                  <span class="dashboardFuncIcon dashboardFuncIcon--edit" aria-hidden="true"></span>
                </button>
                <button type="button" class="dashboardFuncBtn dashboardFuncBtn--delete" data-action="delete" data-id="${escapeHtml(dashboard.id)}" title="${escapeHtml(t("dashboard.list.delete"))}" aria-label="${escapeHtml(t("dashboard.list.delete"))}" ${canEdit && !dashboard.isDefault ? "" : "disabled"}>
                  <span class="dashboardFuncIcon dashboardFuncIcon--delete" aria-hidden="true"></span>
                </button>
              </div>
            </td>`;
          tableBodyEl.appendChild(tr);
        });
      }
    }

    if (tableWrapEl) tableWrapEl.classList.toggle("is-empty", all.length === 0);

    const createBtn = el("dashboardCreateBtn");
    if (createBtn) createBtn.disabled = !canEditDashboardsModule();
  }

  function setWidgetTab(tab) {
    activeWidgetTab = tab;
    designerToolbarEl?.querySelectorAll("[data-widget-tab]").forEach((btn) => {
      const active = btn.dataset.widgetTab === tab;
      btn.classList.toggle("is-active", active);
      btn.setAttribute("aria-selected", active ? "true" : "false");
    });
    designerToolbarEl?.querySelectorAll("[data-panel]").forEach((panel) => {
      panel.hidden = panel.dataset.panel !== tab;
    });
  }

  function renderDesignerChrome() {
    const db = activeDashboard();
    if (designerTitleEl) designerTitleEl.textContent = db?.name || "—";
    const canEdit = dashboardCanEdit(db);
    if (designerToolbarEl) designerToolbarEl.hidden = !canEdit || !store.editMode;
    if (saveBtnEl) saveBtnEl.hidden = !canEdit || !store.editMode;
    if (editModeBtnEl) {
      editModeBtnEl.hidden = !canEdit;
      editModeBtnEl.textContent = store.editMode ? t("dashboard.editDone") : t("dashboard.editLayout");
      editModeBtnEl.setAttribute("aria-pressed", store.editMode ? "true" : "false");
    }
    if (!designerToolbarEl?.hidden) setWidgetTab(activeWidgetTab);
  }

  function syncDesignerEditMode() {
    /* operate mode by default — edit toggled explicitly */
  }

  function addWidget(type) {
    const db = activeDashboard();
    if (!db || !store.editMode || !dashboardCanEdit(db)) return;
    const isMap = type === "map_locked" || type === "map";
    const widget = normalizeWidget({
      id: newId("w"),
      type,
      title: type === "mission_queue" ? "Mission queue" : "",
      w: isMap ? 6 : DEFAULT_W,
      h: isMap ? 6 : DEFAULT_H,
    });
    const spot = findFreeGridSpot(db.widgets, widget.w, widget.h);
    widget.col = spot.col;
    widget.row = spot.row;
    db.widgets.push(widget);
    persistStore();
    renderDashboard();
    if (type === "mission_button" || type === "mission_group") openEditDialog(widget.id);
    else if (type === "map_locked" || type === "map") openEditDialog(widget.id);
  }

  function applyWidgetGridStyle(card, widget) {
    normalizeWidget(widget);
    card.style.setProperty("--dw", String(widget.w));
    card.style.setProperty("--dh", String(widget.h));
    if (hasGridPos(widget)) {
      card.style.gridColumn = `${widget.col} / span ${widget.w}`;
      card.style.gridRow = `${widget.row} / span ${widget.h}`;
    } else {
      card.style.gridColumn = `span ${widget.w}`;
      card.style.gridRow = `span ${widget.h}`;
    }
  }

  function startWidgetMove(evt, widget, card) {
    if (!store.editMode || resizeSession || dragSession) return;
    if (evt.button !== 0) return;
    if (!hasGridPos(widget)) return;

    evt.preventDefault();
    const startCol = widget.col;
    const startRow = widget.row;
    const grab = pointerToGrid(evt.clientX, evt.clientY);
    const grabColOff = grab.col - widget.col;
    const grabRowOff = grab.row - widget.row;

    dragSession = { widgetId: widget.id };
    card.classList.add("is-dragging");
    document.body.classList.add("dashboard-widget-dragging");

    const onMove = (e) => {
      const pt = pointerToGrid(e.clientX, e.clientY);
      let col = pt.col - grabColOff;
      let row = pt.row - grabRowOff;
      col = clamp(col, 1, GRID_COLS - widget.w + 1);
      row = Math.max(1, row);
      widget.col = col;
      widget.row = row;
      applyWidgetGridStyle(card, widget);
      updateGridCanvasHeight(activeWidgets());
    };

    const onUp = () => {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      card.classList.remove("is-dragging");
      document.body.classList.remove("dashboard-widget-dragging");
      dragSession = null;

      const widgets = activeWidgets();
      if (!widgetFits(widgets, widget.id, widget.col, widget.row, widget.w, widget.h)) {
        widget.col = startCol;
        widget.row = startRow;
        applyWidgetGridStyle(card, widget);
      } else {
        persistStore();
      }
      updateGridCanvasHeight(widgets);
    };

    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }

  function startWidgetResize(evt, widget, card) {
    if (!store.editMode) return;
    evt.preventDefault();
    evt.stopPropagation();
    const startX = evt.clientX;
    const startY = evt.clientY;
    const startW = widget.w;
    const startH = widget.h;
    const colW = gridEl.clientWidth / GRID_COLS;

    const onMove = (e) => {
      const dw = Math.round((e.clientX - startX) / colW);
      const dh = Math.round((e.clientY - startY) / GRID_ROW_PX);
      widget.w = clamp(startW + dw, MIN_W, MAX_W);
      widget.h = clamp(startH + dh, MIN_H, MAX_H);
      applyWidgetGridStyle(card, widget);
    };
    const onUp = () => {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      persistStore();
      resizeSession = null;
    };
    resizeSession = { widgetId: widget.id };
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }

  function attachWidgetInteractions(card, widget) {
    applyWidgetGridStyle(card, widget);
    const header = card.querySelector(".dashboardWidgetHeader");
    const pen = card.querySelector("[data-widget-config]");
    const resize = card.querySelector("[data-widget-resize]");

    pen?.addEventListener("click", (evt) => {
      evt.stopPropagation();
      openEditDialog(widget.id);
    });

    resize?.addEventListener("mousedown", (evt) => startWidgetResize(evt, widget, card));

    if (!store.editMode) return;

    header?.addEventListener("mousedown", (evt) => startWidgetMove(evt, widget, card));
  }

  function openCreateView() {
    if (!canEditDashboardsModule()) return;
    const nameInput = el("dashboardCreateName");
    if (nameInput) nameInput.value = "";
    renderPermissionsChecklist(el("dashboardCreatePermissions"), currentUser()?.group_id ? [currentUser().group_id] : []);
    setView("create");
    requestAnimationFrame(() => nameInput?.focus());
  }

  function openCreatePermissionsDialog() {
    const container = el("dashboardCreatePermissions");
    const current = readPermissionsChecklist(container);
    const defaults = current.length ? current : currentUser()?.group_id ? [currentUser().group_id] : [];
    renderPermissionsChecklist(container, defaults);
    permissionsDialogEl?.showModal();
  }

  function closeCreateView() {
    setView("list");
    window.NavApp?.syncDashboardSection?.("dashboard-list");
  }

  function openEditDashboardDialog(id) {
    const dashboard = store.dashboards.find((d) => d.id === id);
    if (!dashboard || !dashboardCanEdit(dashboard)) {
      alert(t("dashboard.list.noEditPermission"));
      return;
    }
    el("dashboardEditId").value = dashboard.id;
    el("dashboardEditName").value = dashboard.name;
    renderPermissionsChecklist(el("dashboardEditPermissions"), dashboard.editGroups || []);
    editDialogEl?.showModal();
  }

  function openDesignerFor(id, { edit = false } = {}) {
    setActiveDashboard(id);
    const db = activeDashboard();
    store.editMode = edit && dashboardCanEdit(db);
    if (store.editMode) setWidgetTab("missions");
    setView("designer");
    window.NavApp?.syncDashboardSection?.(`dashboard-${id}`);
  }

  function deleteDashboard(id) {
    const dashboard = store.dashboards.find((d) => d.id === id);
    if (!dashboard) return;
    if (dashboard.isDefault) {
      alert(t("dashboard.list.cannotDeleteDefault"));
      return;
    }
    if (!dashboardCanEdit(dashboard)) {
      alert(t("dashboard.list.noEditPermission"));
      return;
    }
    if (!confirm(t("dashboard.list.deleteConfirm", { name: dashboard.name }))) return;
    store.dashboards = store.dashboards.filter((d) => d.id !== id);
    if (store.activeDashboardId === id) {
      store.activeDashboardId = store.dashboards[0]?.id || null;
    }
    persistStore();
    refreshNav();
    renderListUI();
  }

  function widgetTitle(widget) {
    if (widget.title) return widget.title;
    return widgetTypeLabel(widget.type);
  }

  function missionOptions(selected) {
    const list = missions()?.getMissions?.() || [];
    return list
      .map(
        (m) =>
          `<option value="${escapeHtml(m.id)}" ${m.id === selected ? "selected" : ""}>${escapeHtml(m.name)} (${escapeHtml(m.group)})</option>`
      )
      .join("");
  }

  function groupOptions(selected) {
    const groups = missions()?.getGroups?.() || ["Missions"];
    return groups
      .map((g) => `<option value="${escapeHtml(g)}" ${g === selected ? "selected" : ""}>${escapeHtml(g)}</option>`)
      .join("");
  }

  function fillTypeFields(container, type, widget = {}) {
    if (!container) return;
    container.innerHTML = "";
    if (type === "mission_button") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.mission")}</label>
          <select data-field="mission_id">${missionOptions(widget.mission_id || "")}</select>
        </div>
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" placeholder="${escapeHtml(t("dashboard.widget.titlePlaceholder"))}" />
        </div>`;
    } else if (type === "mission_group") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.group")}</label>
          <select data-field="group">${groupOptions(widget.group || "Missions")}</select>
        </div>
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
        </div>`;
    } else if (type === "mission_queue") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "Mission queue")}" />
        </div>`;
    } else if (type === "pause_continue") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" />
        </div>
        <p class="mutedNote">${escapeHtml(t("dashboard.widget.pauseHint"))}</p>`;
    } else if (type === "mission_action_log") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "Mission action log")}" />
        </div>`;
    } else if (type === "logout_button") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || t("dashboard.widget.logout_button"))}" />
        </div>`;
    } else if (type === "map_locked" || type === "map") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.map")}</label>
          <select data-field="map_id">${mapOptions(widget.map_id || "")}</select>
        </div>
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || widgetTypeLabel(type))}" />
        </div>
        <p class="mutedNote">${escapeHtml(t("dashboard.widget.mapHint"))}</p>`;
    } else if (type === "robot_summary") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || t("dashboard.widget.robot_summary"))}" />
        </div>`;
    } else if (type === "io_connect" || type === "io_status" || type === "io_configuration") {
      container.innerHTML = `
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.ioModule")}</label>
          <select data-field="io_module_id">${ioModuleOptions(widget.io_module_id || "")}</select>
        </div>
        <div class="row rowWide">
          <label>${t("dashboard.widget.field.title")}</label>
          <input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" placeholder="${escapeHtml(widgetTypeLabel(type))}" />
        </div>`;
    }
  }

  function readFields(container) {
    const out = {};
    container?.querySelectorAll("[data-field]").forEach((node) => {
      out[node.dataset.field] = node.value;
    });
    return out;
  }

  function renderMissionButtonWidget(widget, bodyEl) {
    const m = missions()?.getMissionById?.(widget.mission_id);
    const label = m?.name || t("dashboard.widget.selectMission");
    bodyEl.innerHTML = `
      <button type="button" class="dashboardMissionBtn" data-run-mission="${escapeHtml(widget.mission_id || "")}">
        <span class="dashboardMissionBtnIcon">▶</span>
        <span>${escapeHtml(label)}</span>
      </button>
      ${!m ? `<p class="mutedNote dashboardWidgetHint">${t("dashboard.widget.configHint")}</p>` : ""}`;
    bodyEl.querySelector("[data-run-mission]")?.addEventListener("click", () => {
      if (!widget.mission_id) return;
      missions()?.queueMission?.(widget.mission_id);
    });
  }

  function renderMissionGroupWidget(widget, bodyEl) {
    const group = widget.group || "Missions";
    const list = (missions()?.getMissions?.() || []).filter((m) => m.group === group);
    if (!list.length) {
      bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.emptyGroup", { group })}</p>`;
      return;
    }
    bodyEl.innerHTML = `<div class="dashboardMissionGroupList"></div>`;
    const listEl = bodyEl.querySelector(".dashboardMissionGroupList");
    list.forEach((m) => {
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = "dashboardMissionGroupBtn";
      btn.innerHTML = `<span class="dashboardMissionBtnIcon">▶</span><span>${escapeHtml(m.name)}</span>`;
      btn.addEventListener("click", () => missions()?.queueMission?.(m.id));
      listEl.appendChild(btn);
    });
  }

  function renderMissionQueueWidget(widget, bodyEl) {
    bodyEl.innerHTML = `
      <div class="dashboardQueueRunner mutedNote" data-role="runner">—</div>
      <div class="dashboardQueueList" data-role="list"></div>
      <p class="mutedNote dashboardQueueEmpty" data-role="empty">${t("dashboard.widget.queueEmpty")}</p>
      <button type="button" class="btn subtle btnBlock dashboardQueueClear">${t("dashboard.widget.clearQueue")}</button>`;
    bodyEl.querySelector(".dashboardQueueClear")?.addEventListener("click", () => missions()?.clearQueue?.());
    refreshQueueWidget(bodyEl);
  }

  function refreshQueueWidget(bodyEl) {
    const snap = missions()?.getQueueSnapshot?.();
    if (!snap) return;
    missions()?.renderQueueInto?.(
      {
        listEl: bodyEl.querySelector('[data-role="list"]'),
        runnerEl: bodyEl.querySelector('[data-role="runner"]'),
        emptyEl: bodyEl.querySelector('[data-role="empty"]'),
      },
      { compact: true }
    );
  }

  function renderMissionActionLogWidget(widget, bodyEl) {
    const snap = missions()?.getQueueSnapshot?.();
    const runner = snap?.runner || {};
    const executing = (snap?.queue || []).find((e) => e.status === "executing");
    const lines = [];
    if (runner.current_action) {
      lines.push({ message: runner.current_action, current: true });
    } else if (runner.message) {
      lines.push({ message: runner.message, current: true });
    }
    if (executing?.log && Array.isArray(executing.log)) {
      executing.log
        .slice(-10)
        .reverse()
        .forEach((entry) => {
          if (entry?.message) lines.push({ message: entry.message, level: entry.level || "info" });
        });
    }
    if (!lines.length) {
      bodyEl.innerHTML = `<p class="mutedNote">${escapeHtml(t("dashboard.widget.actionLog.empty"))}</p>`;
      return;
    }
    bodyEl.innerHTML = `<ul class="dashboardActionLogList"></ul>`;
    const listEl = bodyEl.querySelector(".dashboardActionLogList");
    lines.forEach((line) => {
      const li = document.createElement("li");
      li.className = `dashboardActionLogItem${line.current ? " is-current" : ""}${line.level ? ` level-${line.level}` : ""}`;
      li.textContent = line.message;
      listEl.appendChild(li);
    });
  }

  function renderLogoutButtonWidget(widget, bodyEl) {
    const label = widget.title || t("dashboard.widget.logout_button");
    bodyEl.innerHTML = `<button type="button" class="dashboardLogoutBtn">${escapeHtml(label)}</button>`;
    bodyEl.querySelector(".dashboardLogoutBtn")?.addEventListener("click", () => window.AuthApp?.logout?.());
  }

  function renderMapWidget(widget, bodyEl, locked = false) {
    bodyEl.innerHTML = `<div class="dashboardMapViewport${locked ? " dashboardMapViewport--locked" : ""}" data-map-viewport>
      <div class="dashboardMapLoading mutedNote">${escapeHtml(t("dashboard.widget.mapLoading"))}</div>
    </div>`;
    void layoutMapWidget(widget, bodyEl, locked);
  }

  function renderRobotSummaryWidget(widget, bodyEl) {
    const title = widget.title || t("dashboard.widget.robot_summary");
    bodyEl.innerHTML = `
      <div class="dashboardRobotSummary">
        <div class="dashboardRobotSummaryIcon" aria-hidden="true">⬡</div>
        <div class="dashboardRobotSummaryMeta">
          <div class="dashboardRobotSummaryName">${escapeHtml(t("app.robotName"))}</div>
          <div class="dashboardRobotSummarySub mutedNote">${escapeHtml(title)}</div>
        </div>
      </div>`;
  }

  function renderPauseContinueWidget(widget, bodyEl) {
    const snap = missions()?.getQueueSnapshot?.();
    const state = snap?.runner?.state || "idle";
    const paused = state === "paused" || snap?.runner?.paused;
    const running = state === "running" || paused;

    if (!store.editMode) {
      bodyEl.innerHTML = `
        <button type="button" class="dashboardMirToggleBtn ${paused ? "is-continue" : "is-pause"}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
          ${paused ? t("dashboard.widget.continue") : t("dashboard.widget.pause")}
        </button>`;
      bodyEl.querySelector("[data-pause-action]")?.addEventListener("click", async (evt) => {
        const action = evt.currentTarget.dataset.pauseAction;
        try {
          if (action === "pause") await missions()?.pauseRunner?.();
          else await missions()?.continueRunner?.();
        } catch (e) {
          alert(e.message);
        }
      });
      return;
    }

    bodyEl.innerHTML = `
      <div class="dashboardRunnerControls">
        <button type="button" class="dashboardPauseBtn ${paused ? "is-paused" : ""}" data-pause-action="${paused ? "continue" : "pause"}" ${running ? "" : "disabled"}>
          ${paused ? t("dashboard.widget.continue") : t("dashboard.widget.pause")}
        </button>
        <button type="button" class="dashboardCancelBtn" data-cancel-mission ${running ? "" : "disabled"}>
          ${t("dashboard.widget.cancelMission")}
        </button>
      </div>
      <p class="mutedNote dashboardWidgetHint">${escapeHtml(t("dashboard.widget.pauseHint"))}</p>`;
    bodyEl.querySelector("[data-pause-action]")?.addEventListener("click", async (evt) => {
      const action = evt.currentTarget.dataset.pauseAction;
      try {
        if (action === "pause") await missions()?.pauseRunner?.();
        else await missions()?.continueRunner?.();
      } catch (e) {
        alert(e.message);
      }
    });
    bodyEl.querySelector("[data-cancel-mission]")?.addEventListener("click", async () => {
      try {
        await missions()?.cancelRunner?.();
      } catch (e) {
        alert(e.message);
      }
    });
  }

  function renderWidget(widget) {
    normalizeWidget(widget);
    const card = document.createElement("article");
    card.className = `dashboardWidget dashboardWidget--${widget.type}`;
    card.classList.toggle("dashboardWidget--operate", !store.editMode);
    card.classList.toggle("dashboardWidget--edit", store.editMode);
    card.dataset.widgetId = widget.id;

    card.innerHTML = `
      ${store.editMode ? `
      <div class="dashboardWidgetHeader" title="${escapeHtml(t("dashboard.designer.dragHint"))}">
        <div class="dashboardWidgetTitle">${escapeHtml(widgetTitle(widget))}</div>
        <span class="dashboardWidgetDragHint" aria-hidden="true">⠿</span>
      </div>` : ""}
      <div class="dashboardWidgetBody"></div>
      <button type="button" class="dashboardWidgetPen" data-widget-config title="${escapeHtml(t("dashboard.designer.configure"))}" aria-label="${escapeHtml(t("dashboard.designer.configure"))}" ${store.editMode ? "" : "hidden"}>✎</button>
      <div class="dashboardWidgetResize" data-widget-resize title="${escapeHtml(t("dashboard.designer.resize"))}" aria-label="${escapeHtml(t("dashboard.designer.resize"))}" ${store.editMode ? "" : "hidden"}>↘</div>`;

    const bodyEl = card.querySelector(".dashboardWidgetBody");
    switch (widget.type) {
      case "mission_button":
        renderMissionButtonWidget(widget, bodyEl);
        break;
      case "mission_group":
        renderMissionGroupWidget(widget, bodyEl);
        break;
      case "mission_queue":
        renderMissionQueueWidget(widget, bodyEl);
        break;
      case "pause_continue":
        renderPauseContinueWidget(widget, bodyEl);
        break;
      case "mission_action_log":
        renderMissionActionLogWidget(widget, bodyEl);
        break;
      case "logout_button":
        renderLogoutButtonWidget(widget, bodyEl);
        break;
      case "map_locked":
        renderMapWidget(widget, bodyEl, true);
        break;
      case "map":
        renderMapWidget(widget, bodyEl, false);
        break;
      case "robot_summary":
        renderRobotSummaryWidget(widget, bodyEl);
        break;
      case "io_connect":
        renderIoConnectWidget(widget, bodyEl);
        break;
      case "io_status":
        renderIoStatusWidget(widget, bodyEl);
        break;
      case "io_configuration":
        renderIoConfigurationWidget(widget, bodyEl);
        break;
      default:
        bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.unsupported")}</p>`;
    }

    attachWidgetInteractions(card, widget);
    return card;
  }

  function renderDashboard() {
    if (!gridEl) return;
    const widgets = activeWidgets();
    gridEl.innerHTML = "";
    if (designerEmptyEl) {
      designerEmptyEl.hidden = widgets.length > 0;
      designerEmptyEl.textContent = store.editMode
        ? t("dashboard.designer.emptyEdit")
        : t("dashboard.designer.empty");
    }
    gridEl.classList.toggle("dashboardGrid--edit", store.editMode);
    ensureWidgetPositions(widgets);
    widgets.forEach((w) => gridEl.appendChild(renderWidget(w)));
    updateGridCanvasHeight(widgets);
    renderDesignerChrome();
    if (hasMapWidget(widgets)) {
      void ensureMapsLoaded().then(() => refreshMapWidgets());
    }
  }

  function refreshDynamicWidgets() {
    activeWidgets().forEach((widget) => {
      const card = gridEl?.querySelector(`[data-widget-id="${widget.id}"]`);
      if (!card) return;
      const bodyEl = card.querySelector(".dashboardWidgetBody");
      if (widget.type === "mission_queue") refreshQueueWidget(bodyEl);
      if (widget.type === "pause_continue") renderPauseContinueWidget(widget, bodyEl);
      if (widget.type === "mission_action_log") renderMissionActionLogWidget(widget, bodyEl);
      if (widget.type === "io_connect") renderIoConnectWidget(widget, bodyEl);
      if (widget.type === "io_status") renderIoStatusWidget(widget, bodyEl);
      if (widget.type === "io_configuration") renderIoConfigurationWidget(widget, bodyEl);
    });
    if (hasMapWidget()) refreshMapWidgets();
  }

  function openEditDialog(widgetId) {
    const widget = activeWidgets().find((w) => w.id === widgetId);
    if (!widget) return;
    editWidgetIdEl.value = widget.id;
    editWidgetTypeEl.value = widgetTypeLabel(widget.type);
    const open = () => {
      fillTypeFields(editFieldsEl, widget.type, widget);
      editWidgetDialogEl.showModal();
    };
    if (widget.type === "map" || widget.type === "map_locked") {
      void ensureMapsLoaded().then(open);
    } else if (widget.type === "io_connect" || widget.type === "io_status" || widget.type === "io_configuration") {
      void ensureIoModulesLoaded().then(open);
    } else {
      open();
    }
  }

  function deleteWidget(widgetId) {
    if (!confirm(t("dashboard.widget.deleteConfirm"))) return;
    const db = activeDashboard();
    if (!db) return;
    db.widgets = db.widgets.filter((w) => w.id !== widgetId);
    persistStore();
    renderDashboard();
    editWidgetDialogEl.close();
  }

  function handleNav(section) {
    if (section === "dashboard-list") {
      setView("list");
      return;
    }
    if (section.startsWith("dashboard-")) {
      const id = section.slice("dashboard-".length);
      if (store.dashboards.some((d) => d.id === id)) {
        setActiveDashboard(id);
        store.editMode = false;
        setView("designer");
      }
    }
  }

  function getNavItems() {
    const items = [{ section: "dashboard-list", page: "dashboard", label: t("nav.dashboardsList") }];
    store.dashboards.forEach((dashboard) => {
      items.push({
        section: `dashboard-${dashboard.id}`,
        page: "dashboard",
        label: dashboard.name,
      });
    });
    return items;
  }

  function bindEvents() {
    el("dashboardCreateBtn")?.addEventListener("click", openCreateView);
    el("dashboardCreateBackBtn")?.addEventListener("click", closeCreateView);
    el("dashboardCreateCancelBtn")?.addEventListener("click", closeCreateView);
    el("dashboardCreatePermissionsBtn")?.addEventListener("click", openCreatePermissionsDialog);
    el("dashboardPermissionsForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      permissionsDialogEl?.close();
    });
    el("dashboardClearFiltersBtn")?.addEventListener("click", () => {
      store.filter = "";
      store.page = 1;
      if (filterInputEl) filterInputEl.value = "";
      renderListUI();
    });
    filterInputEl?.addEventListener("input", () => {
      store.filter = filterInputEl.value;
      store.page = 1;
      renderListUI();
    });

    document.getElementById("dashboardPagination")?.addEventListener("click", (evt) => {
      const btn = evt.target.closest("[data-page-action]");
      if (!btn || btn.disabled) return;
      const { totalPages } = paginatedDashboards();
      const action = btn.dataset.pageAction;
      if (action === "first") store.page = 1;
      else if (action === "prev") store.page = Math.max(1, store.page - 1);
      else if (action === "next") store.page = Math.min(totalPages, store.page + 1);
      else if (action === "last") store.page = totalPages;
      renderListUI();
    });

    tableBodyEl?.addEventListener("click", (evt) => {
      const btn = evt.target.closest("[data-action]");
      if (!btn) return;
      const id = btn.dataset.id;
      const action = btn.dataset.action;
      if (action === "design") openDesignerFor(id, { edit: true });
      else if (action === "edit") openEditDashboardDialog(id);
      else if (action === "delete") deleteDashboard(id);
    });

    el("dashboardBackToListBtn")?.addEventListener("click", () => {
      setView("list");
      window.NavApp?.syncDashboardSection?.("dashboard-list");
    });

    el("dashboardCreateForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const name = normalizeStr(el("dashboardCreateName").value);
      if (!name) return;
      const user = currentUser();
      const dashboard = {
        id: newId("dashboard"),
        name,
        createdBy: user?.group_name || user?.display_name || user?.username || "—",
        createdByUser: user?.username || null,
        isDefault: false,
        editGroups: readPermissionsChecklist(el("dashboardCreatePermissions")),
        widgets: [],
      };
      store.dashboards.push(dashboard);
      store.activeDashboardId = dashboard.id;
      persistStore();
      refreshNav();
      openDesignerFor(dashboard.id);
    });

    el("dashboardEditForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const id = el("dashboardEditId").value;
      const dashboard = store.dashboards.find((d) => d.id === id);
      if (!dashboard || !dashboardCanEdit(dashboard)) return;
      dashboard.name = normalizeStr(el("dashboardEditName").value);
      dashboard.editGroups = readPermissionsChecklist(el("dashboardEditPermissions"));
      persistStore();
      editDialogEl?.close();
      refreshNav();
      if (store.view === "list") renderListUI();
      else renderDesignerChrome();
    });

    addTypeEl?.addEventListener("change", () => fillTypeFields(addFieldsEl, addTypeEl.value));

    el("dashboardAddWidgetForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const db = activeDashboard();
      if (!db) return;
      const type = addTypeEl.value;
      const fields = readFields(addFieldsEl);
      const widget = normalizeWidget({ id: newId("w"), type, ...fields, w: DEFAULT_W, h: DEFAULT_H });
      const spot = findFreeGridSpot(db.widgets, widget.w, widget.h);
      widget.col = spot.col;
      widget.row = spot.row;
      db.widgets.push(widget);
      persistStore();
      addDialogEl.close();
      renderDashboard();
    });

    el("dashboardEditWidgetForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const id = editWidgetIdEl.value;
      const widget = activeWidgets().find((w) => w.id === id);
      if (!widget) return;
      Object.assign(widget, readFields(editFieldsEl));
      persistStore();
      editWidgetDialogEl.close();
      renderDashboard();
    });

    el("dashboardDeleteWidgetBtn")?.addEventListener("click", () => deleteWidget(editWidgetIdEl.value));

    editModeBtnEl?.addEventListener("click", async () => {
      if (!dashboardCanEdit(activeDashboard())) return;
      if (store.editMode) {
        clearTimeout(persistTimer);
        await syncStoreToBackend();
      }
      store.editMode = !store.editMode;
      if (store.editMode) setWidgetTab("missions");
      renderDashboard();
    });

    saveBtnEl?.addEventListener("click", async () => {
      clearTimeout(persistTimer);
      await syncStoreToBackend();
      saveBtnEl.classList.add("is-saved");
      saveBtnEl.textContent = t("dashboard.designer.saved");
      setTimeout(() => {
        saveBtnEl.classList.remove("is-saved");
        saveBtnEl.textContent = t("dashboard.designer.save");
      }, 1600);
    });

    designerToolbarEl?.addEventListener("click", (evt) => {
      const tabBtn = evt.target.closest("[data-widget-tab]");
      if (tabBtn) {
        setWidgetTab(tabBtn.dataset.widgetTab);
        return;
      }
      const btn = evt.target.closest("[data-add-widget]");
      if (!btn) return;
      addWidget(btn.dataset.addWidget);
    });
  }

  function startDashboardPoll() {
    if (window.AuthApp && !window.AuthApp.isReady()) return;
    if (store.view !== "designer") return;
    stopDashboardPoll();
    missions()?.refreshQueue?.();
    store.queueUnsub = missions()?.onQueueUpdate?.(() => refreshDynamicWidgets());
    missions()?.startQueuePoll?.();
    store.pollActive = true;
    if (hasMapWidget()) {
      void fetchRobotPose(true);
      store.mapPollTimer = setInterval(() => {
        void fetchRobotPose(true).then(() => refreshMapWidgets());
      }, 2000);
    }
  }

  function stopDashboardPoll() {
    if (store.pollActive) {
      missions()?.stopQueuePoll?.();
      store.pollActive = false;
    }
    if (store.queueUnsub) {
      store.queueUnsub();
      store.queueUnsub = null;
    }
    if (store.mapPollTimer) {
      clearInterval(store.mapPollTimer);
      store.mapPollTimer = null;
    }
  }

  async function init() {
    await loadStoreFromBackend();
    await loadUserGroups();
    await ensureIoModulesLoaded();
    bindEvents();
    setView("list");
  }

  window.DashboardApp = {
    init,
    getNavItems,
    handleNav,
    onPageShow() {
      void ensureIoModulesLoaded();
      if (store.view === "designer") {
        syncDesignerEditMode();
        renderDesignerChrome();
        renderDashboard();
        startDashboardPoll();
      } else {
        renderListUI();
      }
    },
    onPageHide() {
      stopDashboardPoll();
    },
    refresh() {
      if (store.view === "list") renderListUI();
      else renderDashboard();
    },
  };

  async function boot() {
    await init();
    refreshNav();
  }

  window.addEventListener("lm:locale-change", () => {
    if (store.view === "list") renderListUI();
    else {
      renderDesignerChrome();
      renderDashboard();
    }
    refreshNav();
  });

  if (window.AuthApp?.isReady()) boot();
  else window.addEventListener("lm:auth-ready", boot, { once: true });
  window.addEventListener("lm:auth-logout", stopDashboardPoll);
})();
