(() => {
  const { useState, useEffect, apiFetch, sleep, fmtDate, fmtFullDate, escapeHtml, openPrintReport, openPrintWindow, pdfCreateDoc, pdfAddMetrics, pdfAddInfo, pdfSectionTitle, pdfAddFooters, pdfStatusColor, pdfAutoTableDefaults } = window.DC;
  const PDF_C = window.DC.pdfColors;
  const { ModalLoadingOverlay } = window.DCComponents;

  const ACTION_COLORS = {
    pending: "#64748b",
    running: "#00e5ff",
    ok: "#22d3a5",
    partial: "#fbbf24",
    skipped: "#94a3b8",
    timeout: "#f43f5e",
    error: "#f43f5e",
    http_error: "#f43f5e",
    stopped: "#94a3b8",
  };

  const ACTION_ICONS = {
    pending: "⏸",
    running: "⟳",
    ok: "✅",
    partial: "⚠",
    skipped: "⏭",
    timeout: "⏱",
    error: "❌",
    http_error: "❌",
    stopped: "⏹",
  };
  const RETRYABLE_RESULT_STATUSES = new Set(["error", "http_error", "timeout"]);
  const HIST_PAGE_SIZE = 20;
  const DETAIL_PAGE_SIZE = 30;

  function statusTone(status) {
    return ACTION_COLORS[status] || "#64748b";
  }

  function statusIcon(status) {
    return ACTION_ICONS[status] || "•";
  }

  function ActionsTabs({ tabs, activeTab, onChange }) {
    return (
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-body" style={{ padding: 12 }}>
          <div className="nav-tabs-wrap settings-tabs-wrap internal-tabs-wrap">
            <div className="nav-tabs settings-tabs internal-tabs">
              {tabs.map((tab) => (
                <button
                  key={tab.key}
                  type="button"
                  onClick={() => onChange(tab.key)}
                  className={`nav-tab${activeTab === tab.key ? " active" : ""}`}
                  title={tab.label}
                >
                  <span className="nav-tab-icon" aria-hidden="true">{tab.icon}</span>
                  <span className="nav-tab-text">{tab.label}</span>
                </button>
              ))}
            </div>
          </div>
        </div>
      </div>
    );
  }

  function buildActionReportHtml(detail) {
    const batch = detail?.batch || {};
    const results = Array.isArray(detail?.results) ? detail.results : [];
    const failedCount = results.filter((result) => RETRYABLE_RESULT_STATUSES.has(result.status)).length;
    const skippedResults = results.filter((result) => result.status === "skipped");

    return `
      <section class="report-section">
        <div class="metrics">
          <div class="metric">
            <div class="metric-label">Lote</div>
            <div class="metric-value">#${escapeHtml(batch.id || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Estado</div>
            <div class="metric-value status-${escapeHtml(batch.status || "pending")}">${escapeHtml(batch.status || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">OK</div>
            <div class="metric-value status-ok">${escapeHtml(batch.ok_count || 0)}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Fallidos</div>
            <div class="metric-value status-error">${failedCount}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Omitidos</div>
            <div class="metric-value status-warn">${escapeHtml(batch.skipped_count || skippedResults.length || 0)}</div>
          </div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen</h2>
        <div class="meta-list">
          <div class="meta-item">Servidor: <strong>${escapeHtml(batch.server_label || "—")}</strong></div>
          <div class="meta-item">Etiqueta: <strong>${escapeHtml(batch.action_label || `${batch.method || "GET"} ${batch.route_path || ""}`)}</strong></div>
          <div class="meta-item">Ruta: <strong>${escapeHtml(batch.route_path || "—")}</strong></div>
          <div class="meta-item">Método: <strong>${escapeHtml(batch.method || "—")}</strong></div>
          <div class="meta-item">Timeout: <strong>${escapeHtml(Math.round((batch.timeout_ms || 0) / 1000))}s</strong></div>
          <div class="meta-item">Disparado por: <strong>${escapeHtml(batch.triggered_by || "—")}</strong></div>
          <div class="meta-item">Inicio: <strong>${escapeHtml(fmtFullDate(batch.started_at))}</strong></div>
          <div class="meta-item">Fin: <strong>${escapeHtml(fmtFullDate(batch.finished_at))}</strong></div>
          <div class="meta-item">Objetivos: <strong>${escapeHtml(batch.total_targets || results.length || 0)}</strong></div>
        </div>
      </section>

      <section class="report-section">
        <h2>Precheck de disponibilidad</h2>
        <div class="callout">
          Antes de ejecutar la ruta real se valida la disponibilidad base del dominio con un chequeo web a <strong>https://dominio/intranet/</strong>.
          Si el dominio responde como caído, timeout o sin DNS, la acción se omite y queda registrada como <strong>skipped</strong>.
        </div>
      </section>

      ${batch.request_body ? `
        <section class="report-section">
          <h2>Body JSON</h2>
          <div class="pre mono">${escapeHtml(batch.request_body)}</div>
        </section>
      ` : ""}

      <section class="report-section">
        <h2>Resultados por dominio</h2>
        <table>
          <thead>
            <tr>
              <th>Usuario</th>
              <th>Dominio</th>
              <th>Estado</th>
              <th>HTTP</th>
              <th>Tiempo</th>
              <th>URL</th>
              <th>Respuesta</th>
            </tr>
          </thead>
          <tbody>
            ${results.map((result) => `
              <tr>
                <td>${escapeHtml(result.virt_user || "—")}</td>
                <td>${escapeHtml(result.domain || "—")}</td>
                <td class="status-${escapeHtml(result.status || "pending")}">${escapeHtml(result.status || "—")}</td>
                <td>${escapeHtml(result.http_status > 0 ? result.http_status : "—")}</td>
                <td>${escapeHtml(result.duration_ms != null ? `${result.duration_ms}ms` : "—")}</td>
                <td class="mono">${escapeHtml(result.target_url || "—")}</td>
                <td class="mono">${escapeHtml(result.response_excerpt || "—").slice(0, 500)}</td>
              </tr>
            `).join("") || `<tr><td colspan="7" class="muted">Sin resultados registrados.</td></tr>`}
          </tbody>
        </table>
      </section>

      <section class="report-section">
        <h2>Dominios omitidos por precheck</h2>
        ${skippedResults.length ? `
          <table>
            <thead>
              <tr>
                <th>Usuario</th>
                <th>Dominio</th>
                <th>Tiempo</th>
                <th>Detalle</th>
              </tr>
            </thead>
            <tbody>
              ${skippedResults.map((result) => `
                <tr>
                  <td>${escapeHtml(result.virt_user || "—")}</td>
                  <td>${escapeHtml(result.domain || "—")}</td>
                  <td>${escapeHtml(result.duration_ms != null ? `${result.duration_ms}ms` : "—")}</td>
                  <td class="mono">${escapeHtml(result.response_excerpt || "Dominio omitido por precheck")}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted">No hubo dominios omitidos antes de ejecutar la acción.</div>'}
      </section>
    `;
  }

  function buildSessionReportHtml(sessionName, sessionDate, triggeredBy, batches, results) {
    const totalOk    = results.filter((r) => r.status === "ok").length;
    const totalErr   = results.filter((r) => RETRYABLE_RESULT_STATUSES.has(r.status)).length;
    const totalSkip  = results.filter((r) => r.status === "skipped").length;
    const total      = results.length;

    // Agrupar resultados por servidor
    const byServer = {};
    for (const r of results) {
      const key = r.server_label || `Servidor #${r.server_id}`;
      if (!byServer[key]) byServer[key] = [];
      byServer[key].push(r);
    }

    const serverSections = Object.entries(byServer).map(([serverLabel, rows]) => {
      const sOk   = rows.filter((r) => r.status === "ok").length;
      const sErr  = rows.filter((r) => RETRYABLE_RESULT_STATUSES.has(r.status)).length;
      const sSkip = rows.filter((r) => r.status === "skipped").length;
      return `
        <section class="report-section">
          <h2>${escapeHtml(serverLabel)} &mdash; ✔ ${sOk} ok · ✗ ${sErr} fallidos · ⏭ ${sSkip} omitidos</h2>
          <table>
            <thead>
              <tr><th>Usuario</th><th>Dominio</th><th>Estado</th><th>HTTP</th><th>Tiempo</th><th>Respuesta</th></tr>
            </thead>
            <tbody>
              ${rows.map((r) => `
                <tr>
                  <td>${escapeHtml(r.virt_user || "—")}</td>
                  <td>${escapeHtml(r.domain || "—")}</td>
                  <td class="status-${escapeHtml(r.status || "pending")}">${escapeHtml(r.status || "—")}</td>
                  <td>${escapeHtml(r.http_status > 0 ? r.http_status : "—")}</td>
                  <td>${escapeHtml(r.duration_ms != null ? `${r.duration_ms}ms` : "—")}</td>
                  <td class="mono">${escapeHtml((r.response_excerpt || "").slice(0, 300))}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        </section>
      `;
    }).join("");

    return `
      <section class="report-section">
        <div class="metrics">
          <div class="metric"><div class="metric-label">Total usuarios</div><div class="metric-value">${total}</div></div>
          <div class="metric"><div class="metric-label">OK</div><div class="metric-value status-ok">${totalOk}</div></div>
          <div class="metric"><div class="metric-label">Fallidos</div><div class="metric-value status-error">${totalErr}</div></div>
          <div class="metric"><div class="metric-label">Omitidos</div><div class="metric-value status-warn">${totalSkip}</div></div>
          <div class="metric"><div class="metric-label">Servidores</div><div class="metric-value">${Object.keys(byServer).length}</div></div>
        </div>
      </section>
      <section class="report-section">
        <h2>Resumen</h2>
        <div class="meta-list">
          <div class="meta-item">Sesión: <strong>${escapeHtml(sessionName)}</strong></div>
          <div class="meta-item">Fecha: <strong>${escapeHtml(fmtFullDate(sessionDate))}</strong></div>
          <div class="meta-item">Disparado por: <strong>${escapeHtml(triggeredBy || "—")}</strong></div>
        </div>
      </section>
      ${serverSections}
    `;
  }

  function ActionsView({ servers, toast, currentUser }) {
    const [activeTab, setActiveTab] = useState(() => {
      try {
        return window.localStorage.getItem("dc:tabs:actions") || "execute";
      } catch (_error) {
        return "execute";
      }
    });
    const [selected, setSelected] = useState({});
    const [targetMode, setTargetMode] = useState("server_all");
    const [serverUsers, setServerUsers] = useState({});
    const [userSelection, setUserSelection] = useState({});
    const [loadingUsers, setLoadingUsers] = useState(false);
    const [form, setForm] = useState({
      action_label: "",
      route_path: "/intranet/",
      method: "GET",
      request_body: "",
      timeout: 15,
      run_per_domain: false,
    });
    const [presets, setPresets] = useState([]);
    const [presetId, setPresetId] = useState("");
    const [presetName, setPresetName] = useState("");
    const [history, setHistory] = useState([]);
    const [histTotal, setHistTotal] = useState(0);
    const [histPage, setHistPage] = useState(0);
    const [expandedSession, setExpandedSession] = useState(null); // session_key expandida
    const [sessionBatches, setSessionBatches] = useState({}); // session_key → [batches]
    const [sessSearch, setSessSearch] = useState("");
    const [sessBatchPage, setSessBatchPage] = useState(0);
    const [sessStatusFilter, setSessStatusFilter] = useState(null); // null | "ok" | "error" | "skipped"
    const [batchStates, setBatchStates] = useState({});
    const [detailId, setDetailId] = useState(null);
    const [detailTotal, setDetailTotal] = useState(0);
    const [detailPage, setDetailPage] = useState(0);
    const [detailFilter, setDetailFilter] = useState("");
    const [detailSearch, setDetailSearch] = useState("");
    const [starting, setStarting] = useState(false);
    const [savingPreset, setSavingPreset] = useState(false);
    const [precheckState, setPrecheckState] = useState({ loading: false, summary: null });
    const [execMode, setExecMode] = useState("single");
    const [queuedPresetIds, setQueuedPresetIds] = useState([]);
    const [showActionModal, setShowActionModal] = useState(false);
    const [modalUsers, setModalUsers] = useState([]);
    const [modalLoading, setModalLoading] = useState(null);
    const isSuperAdmin = currentUser?.role === "superadmin";

    const selectedServers = servers.filter((server) => selected[server.id]);
    const selectedDetail = detailId ? batchStates[detailId] : null;
    const retryableResults = (selectedDetail?.results || []).filter((result) => RETRYABLE_RESULT_STATUSES.has(result.status));
    const skippedResults = (selectedDetail?.results || []).filter((result) => result.status === "skipped");
    const eligibleUsersForServer = (serverId) =>
      (serverUsers[serverId] || []).filter((user) => user.domain && !user.excluded);
    const selectedUserCount = selectedServers.reduce(
      (acc, server) => acc + Object.values(userSelection[server.id] || {}).filter(Boolean).length,
      0
    );
    const totalEligibleCount = selectedServers.reduce(
      (acc, server) => acc + eligibleUsersForServer(server.id).length,
      0
    );

    const selectedQueuedPresets = presets.filter((preset) => queuedPresetIds.includes(String(preset.id)));
    const tabs = [
      { key: "execute", label: "Ejecutar", icon: "⚡" },
      { key: "history", label: "Historial", icon: "🕘" },
      { key: "detail", label: "Detalle", icon: "🔎" },
    ];

    useEffect(() => {
      if (!tabs.some((tab) => tab.key === activeTab)) {
        setActiveTab("execute");
      }
    }, [activeTab, tabs]);

    useEffect(() => {
      try {
        window.localStorage.setItem("dc:tabs:actions", activeTab);
      } catch (_error) {}
    }, [activeTab]);

    const toggleQueuedPreset = (id) => {
      const normalizedId = String(id);
      setQueuedPresetIds((prev) =>
        prev.includes(normalizedId) ? prev.filter((item) => item !== normalizedId) : [...prev, normalizedId]
      );
    };

    const getSelectedUsernamesForServer = (serverId) =>
      targetMode === "selected_users"
        ? Object.entries(userSelection[serverId] || {})
            .filter(([, value]) => value)
            .map(([username]) => username)
        : [];

    const loadHistory = async (page = 0) => {
      const offset = page * HIST_PAGE_SIZE;
      const response = await apiFetch(`/http-actions/history?limit=${HIST_PAGE_SIZE}&offset=${offset}`);
      if (response?.items) {
        setHistory(response.items);
        setHistTotal(response.total || 0);
        setHistPage(page);
      }
    };

    const loadSessionBatches = async (sessionKey, search = "") => {
      const encodedKey = encodeURIComponent(sessionKey);
      const searchParam = search ? `?search=${encodeURIComponent(search)}` : "";
      const response = await apiFetch(`/http-actions/sessions/${encodedKey}/batches${searchParam}`);
      if (response?.batches) {
        setSessionBatches((prev) => ({ ...prev, [sessionKey]: response.batches }));
      }
    };

    const SESS_BATCH_PAGE_SIZE = 20;

    const toggleSession = async (sessionKey) => {
      if (expandedSession === sessionKey) {
        setExpandedSession(null);
      } else {
        setActiveTab("history");
        setExpandedSession(sessionKey);
        setSessSearch("");
        setSessBatchPage(0);
        setSessStatusFilter(null);
        await loadSessionBatches(sessionKey);
      }
    };

    const exportSessionPdf = async (session) => {
      const encodedKey = encodeURIComponent(session.session_key);
      const response = await apiFetch(`/http-actions/sessions/${encodedKey}/results`);
      if (response.error || !Array.isArray(response.results)) {
        toast("No se pudo obtener los resultados de la sesión", "err");
        return;
      }

      const { jspdf } = window;
      if (!jspdf?.jsPDF) { toast("Librería PDF no disponible", "err"); return; }

      const results = response.results;
      const sessionName = session.session_label || session.action_label || `${session.method || "GET"} ${session.route_path || ""}`;
      const doc = new jspdf.jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
      const pageW = doc.internal.pageSize.getWidth();

      // ── Paleta de colores ──
      const C = { ok: [22, 101, 52], err: [153, 27, 27], skip: [120, 113, 108], head: [15, 23, 42], muted: [71, 85, 105] };

      // ── Encabezado de portada ──
      doc.setFillColor(15, 23, 42);
      doc.rect(0, 0, pageW, 28, "F");
      doc.setFont("helvetica", "bold");
      doc.setFontSize(16);
      doc.setTextColor(255, 255, 255);
      doc.text("Reporte de sesión HTTP", 12, 11);
      doc.setFontSize(9);
      doc.setFont("helvetica", "normal");
      doc.setTextColor(148, 163, 184);
      doc.text(sessionName.slice(0, 120), 12, 18);
      doc.text(`${fmtDate(session.started_at)} · por ${session.triggered_by || "—"} · Generado ${fmtDate(new Date().toISOString())}`, 12, 24);

      // ── Métricas globales ──
      const totalOk   = results.filter((r) => r.status === "ok").length;
      const totalErr  = results.filter((r) => RETRYABLE_RESULT_STATUSES.has(r.status)).length;
      const totalSkip = results.filter((r) => r.status === "skipped").length;
      const metrics = [
        ["Total usuarios", results.length],
        ["✔ Ok", totalOk],
        ["✗ Fallidos", totalErr],
        ["⏭ Omitidos", totalSkip],
        ["Servidores", session.server_count || "—"],
        ["Lotes", session.batch_count || "—"],
      ];
      let mx = 12;
      const mw = (pageW - 24) / metrics.length;
      metrics.forEach(([label, value], idx) => {
        doc.setFillColor(248, 250, 252);
        doc.setDrawColor(203, 213, 225);
        doc.roundedRect(mx + idx * mw, 32, mw - 3, 18, 2, 2, "FD");
        doc.setFontSize(7);
        doc.setFont("helvetica", "normal");
        doc.setTextColor(...C.muted);
        doc.text(String(label).toUpperCase(), mx + idx * mw + 3, 38);
        doc.setFontSize(14);
        doc.setFont("helvetica", "bold");
        const col = label.includes("Ok") ? C.ok : label.includes("Fall") ? C.err : label.includes("Om") ? C.skip : C.head;
        doc.setTextColor(...col);
        doc.text(String(value), mx + idx * mw + 3, 46);
      });

      // ── Tabla de resultados por servidor ──
      const byServer = {};
      for (const r of results) {
        const key = r.server_label || `Servidor #${r.server_id}`;
        if (!byServer[key]) byServer[key] = [];
        byServer[key].push(r);
      }

      let startY = 56;
      for (const [serverLabel, rows] of Object.entries(byServer)) {
        const sOk   = rows.filter((r) => r.status === "ok").length;
        const sErr  = rows.filter((r) => RETRYABLE_RESULT_STATUSES.has(r.status)).length;
        const sSkip = rows.filter((r) => r.status === "skipped").length;

        doc.autoTable({
          startY,
          head: [[
            { content: `${serverLabel}   ✔ ${sOk} ok  ✗ ${sErr} fallidos  ⏭ ${sSkip} omitidos`, colSpan: 6,
              styles: { fillColor: C.head, textColor: [255,255,255], fontStyle: "bold", fontSize: 9 } }
          ], [
            "Usuario", "Dominio", "Estado", "HTTP", "Tiempo (ms)", "Respuesta"
          ]],
          body: rows.map((r) => [
            r.virt_user || "—",
            r.domain || "—",
            r.status || "—",
            r.http_status > 0 ? r.http_status : "—",
            r.duration_ms != null ? r.duration_ms : "—",
            (r.response_excerpt || "").slice(0, 200),
          ]),
          styles: { fontSize: 7, cellPadding: 2, overflow: "linebreak" },
          headStyles: { fillColor: [241, 245, 249], textColor: C.head, fontStyle: "bold", fontSize: 7 },
          columnStyles: {
            0: { cellWidth: 22 },
            1: { cellWidth: 45 },
            2: { cellWidth: 18 },
            3: { cellWidth: 14 },
            4: { cellWidth: 20 },
            5: { cellWidth: "auto" },
          },
          didParseCell(data) {
            if (data.section === "body" && data.column.index === 2) {
              const st = String(data.cell.raw || "");
              if (st === "ok") data.cell.styles.textColor = C.ok;
              else if (["error","http_error","timeout"].includes(st)) data.cell.styles.textColor = C.err;
              else if (st === "skipped") data.cell.styles.textColor = C.skip;
              data.cell.styles.fontStyle = "bold";
            }
          },
          margin: { left: 12, right: 12 },
          theme: "grid",
        });

        startY = doc.lastAutoTable.finalY + 6;
      }

      // Pie de página en cada página
      const totalPages = doc.internal.getNumberOfPages();
      for (let i = 1; i <= totalPages; i++) {
        doc.setPage(i);
        doc.setFontSize(7);
        doc.setTextColor(...C.muted);
        doc.text(`Página ${i} / ${totalPages}  ·  ${sessionName.slice(0, 80)}`, 12, doc.internal.pageSize.getHeight() - 5);
      }

      doc.save(`sesion-${session.session_key.slice(0, 10)}.pdf`);
    };

    const deleteSession = async (sessionKey, sessionName) => {
      if (!window.confirm(`¿Eliminar la sesión "${sessionName}"? Esta acción no se puede deshacer.`)) return;
      const response = await apiFetch(`/http-actions/sessions/${encodeURIComponent(sessionKey)}`, { method: "DELETE" });
      if (response.error) { toast(response.error, "err"); return; }
      // quitar de la lista local y recargar página actual
      setHistory((prev) => prev.filter((s) => s.session_key !== sessionKey));
      setHistTotal((prev) => Math.max(0, prev - 1));
      if (expandedSession === sessionKey) setExpandedSession(null);
      setSessionBatches((prev) => { const n = { ...prev }; delete n[sessionKey]; return n; });
      toast("Sesión eliminada");
      // recargar para mantener paginación correcta
      await loadHistory(histPage);
    };

    const clearHistory = async () => {
      if (!window.confirm("¿Eliminar todo el historial de acciones HTTP? Esta acción no se puede deshacer.")) return;
      const response = await apiFetch("/http-actions/history", { method: "DELETE" });
      if (response.error) {
        toast(response.error, "err");
        return;
      }
      setHistory([]);
      setHistTotal(0);
      setHistPage(0);
      setExpandedSession(null);
      setSessionBatches({});
      setBatchStates({});
      setDetailId(null);
      toast("Historial eliminado");
    };

    const loadPresets = async () => {
      const response = await apiFetch("/http-actions/presets");
      setPresets(Array.isArray(response) ? response : []);
    };

    const applyPreset = (nextPresetId) => {
      setPresetId(nextPresetId);
      if (!nextPresetId) {
        setPresetName("");
        return;
      }
      const preset = presets.find((item) => String(item.id) === String(nextPresetId));
      if (!preset) return;
      setPresetName(preset.name || "");
      setForm({
        action_label: preset.action_label || "",
        route_path: preset.route_path || "/intranet/",
        method: preset.method || "GET",
        request_body: preset.request_body || "",
        timeout: Math.max(1, Math.round((preset.timeout_ms || 15000) / 1000)),
        run_per_domain: !!preset.run_per_domain,
      });
    };

    const loadBatch = async (batchId, page = 0, filter = "", search = "") => {
      const offset = page * DETAIL_PAGE_SIZE;
      const filterParam = filter ? `&status=${filter}` : "";
      const searchParam = search ? `&search=${encodeURIComponent(search)}` : "";
      const response = await apiFetch(
        `/http-actions/${batchId}/status?limit=${DETAIL_PAGE_SIZE}&offset=${offset}${filterParam}${searchParam}`
      );
      if (!response.error) {
        setBatchStates((prev) => ({ ...prev, [batchId]: response }));
        setDetailId(batchId);
        setDetailPage(page);
        setDetailFilter(filter);
        setDetailSearch(search);
        setDetailTotal(response.total || 0);
        setActiveTab("detail");
      }
      return response;
    };

    const changeDetailPage = (newPage) => {
      if (!detailId) return;
      loadBatch(detailId, newPage, detailFilter, detailSearch);
    };

    const changeDetailFilter = (newFilter) => {
      if (!detailId) return;
      loadBatch(detailId, 0, newFilter, detailSearch);
    };

    const changeDetailSearch = (newSearch) => {
      if (!detailId) return;
      loadBatch(detailId, 0, detailFilter, newSearch);
    };

    useEffect(() => {
      loadHistory();
      loadPresets();
    }, []);

    useEffect(() => {
      if (targetMode !== "selected_users" || selectedServers.length === 0) return;

      let cancelled = false;
      const missingIds = selectedServers
        .map((server) => server.id)
        .filter((serverId) => !serverUsers[serverId]);

      if (!missingIds.length) return;

      setLoadingUsers(true);
      Promise.all(missingIds.map(async (serverId) => {
        const response = await apiFetch(`/servers/${serverId}/users`);
        return [serverId, Array.isArray(response) ? response : []];
      }))
        .then((entries) => {
          if (cancelled) return;
          setServerUsers((prev) => {
            const next = { ...prev };
            entries.forEach(([serverId, users]) => {
              next[serverId] = users;
            });
            return next;
          });
        })
        .finally(() => {
          if (!cancelled) setLoadingUsers(false);
        });

      return () => {
        cancelled = true;
      };
    }, [targetMode, selectedServers.map((server) => server.id).join(","), Object.keys(serverUsers).join(",")]);

    useEffect(() => {
      setPrecheckState((prev) => (prev.summary ? { ...prev, summary: null } : prev));
    }, [
      targetMode,
      selectedServers.map((server) => server.id).join(","),
      JSON.stringify(userSelection),
    ]);

    const openActionModal = async () => {
      if (!selectedServers.length) {
        toast("Selecciona al menos un servidor", "err");
        return;
      }
      if (execMode === "queue" && !selectedQueuedPresets.length) {
        toast("Selecciona al menos un preset para ejecutar la cola", "err");
        return;
      }
      if (execMode === "single" && !String(form.route_path || "").trim()) {
        toast("Ingresa la ruta a ejecutar", "err");
        return;
      }
      if (targetMode === "selected_users" && selectedUserCount === 0) {
        toast("Selecciona al menos un usuario", "err");
        return;
      }

      setStarting(true);
      setModalLoading({
        title: "Preparando modal",
        message: "Cargando usuarios elegibles para la accion HTTP...",
      });
      const allUsers = [];
      try {
        for (const server of selectedServers) {
          let eligible;
          if (serverUsers[server.id]) {
            eligible = eligibleUsersForServer(server.id);
          } else {
            const resp = await apiFetch(`/servers/${server.id}/users`);
            const fetched = Array.isArray(resp) ? resp : [];
            setServerUsers((prev) => ({ ...prev, [server.id]: fetched }));
            eligible = fetched.filter((u) => u.domain && !u.excluded);
          }

          const filtered = targetMode === "selected_users"
            ? eligible.filter((u) => userSelection[server.id]?.[u.username])
            : eligible;

          filtered.forEach((u) => allUsers.push({
            serverId: server.id,
            serverLabel: server.label,
            username: u.username,
            domain: u.domain,
          }));
        }

        if (!allUsers.length) {
          toast("No hay usuarios con dominio elegibles en los servidores seleccionados", "err");
          return;
        }

        setModalUsers(allUsers);
        setShowActionModal(true);
      } finally {
        setStarting(false);
        setModalLoading(null);
      }
    };

    const runPrecheck = async () => {
      if (!selectedServers.length) {
        toast("Selecciona al menos un servidor", "err");
        return;
      }
      if (loadingUsers) {
        toast("Espera a que carguen los usuarios", "err");
        return;
      }
      if (targetMode === "selected_users" && selectedUserCount === 0) {
        toast("Selecciona al menos un usuario", "err");
        return;
      }

      setPrecheckState({ loading: true, summary: null });
      const serversSummary = [];

      for (const server of selectedServers) {
        const usernames = getSelectedUsernamesForServer(server.id);
        if (targetMode === "selected_users" && usernames.length === 0) continue;

        const response = await apiFetch("/http-actions/precheck", {
          method: "POST",
          body: {
            server_id: server.id,
            usernames,
          },
        });

        if (response.error) {
          toast(`${server.label}: ${response.error}`, "err");
          continue;
        }

        serversSummary.push(response);
      }

      const total = serversSummary.reduce((acc, item) => acc + (item.total || 0), 0);
      const readyCount = serversSummary.reduce((acc, item) => acc + (item.ready_count || 0), 0);
      const skippedCount = serversSummary.reduce((acc, item) => acc + (item.skipped_count || 0), 0);
      const inactiveRows = serversSummary.flatMap((item) =>
        (item.rows || [])
          .filter((row) => row.action_status === "skipped")
          .map((row) => ({ ...row, server_label: item.server_label }))
      );

      const summary = {
        checked_at: new Date().toISOString(),
        server_count: serversSummary.length,
        total,
        ready_count: readyCount,
        skipped_count: skippedCount,
        inactive_rows: inactiveRows,
        servers: serversSummary,
      };

      setPrecheckState({ loading: false, summary });
      toast(`Precheck listo · ${readyCount} listos · ${skippedCount} omitidos`);
    };

    const stopBatch = async (batchId) => {
      const response = await apiFetch(`/http-actions/${batchId}/stop`, { method: "POST" });
      if (response.error) {
        toast(response.error, "err");
        return;
      }
      await loadBatch(batchId, detailPage, detailFilter);
      await loadHistory(histPage);
      toast("Lote detenido");
    };

    const retryFailed = async (batchId, usernames = []) => {
      const response = await apiFetch(`/http-actions/${batchId}/retry`, {
        method: "POST",
        body: usernames.length ? { usernames } : {},
      });

      if (response.error) {
        toast(response.error, "err");
        return;
      }

      const summary = [
        `${response.retried_count || 0} reintento${response.retried_count === 1 ? "" : "s"}`,
      ];
      if (response.skipped_count > 0) summary.push(`${response.skipped_count} omitido${response.skipped_count === 1 ? "" : "s"}`);
      toast(summary.join(" · "));
      await loadHistory(0);
      await loadBatch(response.batch_id, 0, "");
    };

    const exportSelectedDetailPdf = () => {
      const detail = selectedDetail;
      if (!detail?.batch) return;
      const batch = detail.batch;
      const results = detail.results || [];
      const title = `Acción HTTP #${batch.id} — ${batch.server_label || "Servidor"}`;
      const subtitle = `${batch.action_label || batch.route_path || ""} · ${batch.status || ""}`;

      const ctx = pdfCreateDoc({ title, subtitle });
      if (!ctx) { toast("Librería PDF no disponible", "err"); return; }
      const { doc, pageW } = ctx;
      let y = ctx.startY;

      const errCount = (batch.error_count || 0) + (batch.timeout_count || 0);
      y = pdfAddMetrics({ doc, pageW, startY: y, metrics: [
        { label: "Lote", value: `#${batch.id}` },
        { label: "Estado", value: batch.status || "—", color: pdfStatusColor(batch.status) },
        { label: "OK", value: batch.ok_count || 0, color: PDF_C.ok },
        { label: "Fallidos", value: errCount, color: errCount > 0 ? PDF_C.err : PDF_C.muted },
        { label: "Omitidos", value: batch.skipped_count || 0, color: PDF_C.skip },
        { label: "Total", value: batch.total_targets || results.length },
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Información de la acción" });
      y = pdfAddInfo({ doc, pageW, startY: y, rows: [
        ["Servidor", batch.server_label],
        ["Etiqueta", batch.action_label || `${batch.method} ${batch.route_path}`],
        ["Ruta", batch.route_path],
        ["Método", batch.method],
        ["Timeout", `${Math.round((batch.timeout_ms || 0) / 1000)}s`],
        ["Disparado por", batch.triggered_by],
        ["Inicio", fmtFullDate(batch.started_at)],
        ["Fin", fmtFullDate(batch.finished_at)],
      ]});

      const executed = results.filter(r => r.status !== "skipped");
      const skipped  = results.filter(r => r.status === "skipped");

      if (executed.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Resultados ejecutados (${executed.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            { header: "Usuario",   dataKey: "virt_user" },
            { header: "Dominio",   dataKey: "domain" },
            { header: "Estado",    dataKey: "__status__" },
            { header: "HTTP",      dataKey: "http_status" },
            { header: "Tiempo ms", dataKey: "duration_ms" },
            { header: "Respuesta", dataKey: "response_excerpt" },
          ],
          body: executed.map(r => ({
            virt_user: r.virt_user || "—",
            domain: r.domain || "—",
            __status__: r.status || "—",
            http_status: r.http_status > 0 ? r.http_status : "—",
            duration_ms: r.duration_ms != null ? r.duration_ms : "—",
            response_excerpt: (r.response_excerpt || "").slice(0, 200),
          })),
          columnStyles: {
            0: { cellWidth: 22 }, 1: { cellWidth: 50 }, 2: { cellWidth: 18 },
            3: { cellWidth: 14 }, 4: { cellWidth: 18 }, 5: { cellWidth: "auto" },
          },
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      if (skipped.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Dominios ignorados (${skipped.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          headStyles: { fillColor: PDF_C.skip, textColor: [255,255,255], fontStyle: "bold", fontSize: 7 },
          columns: [
            { header: "Usuario",  dataKey: "virt_user" },
            { header: "Dominio",  dataKey: "domain" },
            { header: "Tiempo ms",dataKey: "duration_ms" },
            { header: "Detalle",  dataKey: "response_excerpt" },
          ],
          body: skipped.map(r => ({
            virt_user: r.virt_user || "—",
            domain: r.domain || "—",
            duration_ms: r.duration_ms != null ? r.duration_ms : "—",
            response_excerpt: (r.response_excerpt || "Dominio ignorado").slice(0, 200),
          })),
          columnStyles: { 0: { cellWidth: 22 }, 1: { cellWidth: 55 }, 2: { cellWidth: 18 }, 3: { cellWidth: "auto" } },
        });
      }

      pdfAddFooters({ doc, title });
      doc.save(`accion-http-${batch.id}.pdf`);
    };

    const savePreset = async (mode) => {
      if (!presetName.trim()) {
        toast("Ingresa un nombre para el preset", "err");
        return;
      }
      if (!String(form.route_path || "").trim()) {
        toast("Ingresa la ruta del preset", "err");
        return;
      }
      if (form.method !== "GET" && String(form.request_body || "").trim()) {
        try {
          JSON.parse(form.request_body);
        } catch (_error) {
          toast("El body del preset debe ser JSON válido", "err");
          return;
        }
      }

      setSavingPreset(true);
      const payload = {
        name: presetName.trim(),
        action_label: form.action_label,
        route_path: form.route_path,
        method: form.method,
        request_body: form.method === "GET" ? "" : form.request_body,
        timeout_ms: Number(form.timeout || 15) * 1000,
        run_per_domain: form.run_per_domain ? 1 : 0,
      };
      const response = mode === "update" && presetId
        ? await apiFetch(`/http-actions/presets/${presetId}`, { method: "PUT", body: payload })
        : await apiFetch("/http-actions/presets", { method: "POST", body: payload });

      setSavingPreset(false);

      if (response.error) {
        toast(response.error, "err");
        return;
      }

      await loadPresets();
      setPresetId(String(response.preset.id));
      setPresetName(response.preset.name || "");
      toast(mode === "update" ? "Preset actualizado" : "Preset guardado");
    };

    const deletePreset = async () => {
      if (!presetId) {
        toast("Selecciona un preset", "err");
        return;
      }
      const preset = presets.find((item) => String(item.id) === String(presetId));
      if (!window.confirm(`¿Eliminar el preset "${preset?.name || "seleccionado"}"?`)) return;

      const response = await apiFetch(`/http-actions/presets/${presetId}`, { method: "DELETE" });
      if (response.error) {
        toast(response.error, "err");
        return;
      }

      setPresetId("");
      setPresetName("");
      await loadPresets();
      toast("Preset eliminado");
    };

    return (
      <div style={{ animation: "fadeIn .3s ease" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14, flexWrap: "wrap", gap: 8 }}>
          <div>
            <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 800, fontSize: 20 }}>Acciones HTTP</div>
            <div style={{ fontSize: 11, color: "#64748b", marginTop: 2 }}>
              Ejecuta una ruta relativa por lote sobre los usuarios con dominio y no excluidos.
            </div>
          </div>
          <button onClick={loadHistory} className="btn btn-ghost">
            ↻ Actualizar historial
          </button>
        </div>

        <div className="alert alert-info">
          La acción siempre se ejecuta como <strong style={{ color: "#e2e8f0" }}>https://dominio{form.route_path || "/ruta"}</strong>. Úsala para
          reset, sync, limpieza de cache u otros endpoints internos de tus apps. Antes de dispararla, el sistema hace un precheck web en
          <strong style={{ color: "#e2e8f0" }}> https://dominio/intranet/</strong> para omitir dominios realmente caídos. No ejecuta comandos SSH.
        </div>

        <ActionsTabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />

        {activeTab === "execute" && (
        <div className="card">
          <div className="card-head">⚙ Configuración de la acción</div>
          <div className="card-body">

            {/* Modo de ejecución - Mejorado para mayor visibilidad */}
            <div className="field">
              <label>
                Modo de ejecución 
                <span style={{ fontSize: 10, color: "#00e5ff", marginLeft: 6, fontWeight: 700 }}>
                  {execMode === "queue" ? "📋 MÚLTIPLE" : "⚡ ÚNICA"}
                </span>
              </label>
              <div style={{ display: "flex", gap: 0, borderRadius: 10, overflow: "hidden", border: "1px solid #1e2a3a", marginBottom: 8 }}>
                {[["single", "⚡ Una acción"], ["queue", "📋 Cola de presets"]].map(([mode, label]) => (
                  <button
                    key={mode}
                    onClick={() => setExecMode(mode)}
                    className="btn"
                    style={{
                      flex: 1,
                      borderRadius: 0,
                      border: "none",
                      background: execMode === mode ? "#00e5ff18" : "transparent",
                      color: execMode === mode ? "#00e5ff" : "#64748b",
                      fontWeight: execMode === mode ? 700 : 400,
                      borderRight: mode === "single" ? "1px solid #1e2a3a" : "none",
                      padding: "10px 12px",
                      fontSize: 13,
                    }}
                  >
                    {label}
                  </button>
                ))}
              </div>
              <div style={{ fontSize: 11, color: "#64748b", marginTop: 4 }}>
                {execMode === "single" 
                  ? "Ejecuta una sola acción HTTP sobre los servidores seleccionados." 
                  : "Ejecuta múltiples acciones en secuencia usando presets guardados."}
              </div>
            </div>

            {execMode === "single" && (
              <div
                style={{
                  background: "#131820",
                  border: "1px solid #00e5ff22",
                  borderRadius: 10,
                  padding: 14,
                  marginBottom: 14,
                }}
              >
                <div style={{ display: "grid", gridTemplateColumns: "minmax(220px,1.4fr) minmax(180px,1fr)", gap: 12, alignItems: "end", marginBottom: 10 }}>
                  <div className="field" style={{ marginBottom: 0 }}>
                    <label>Preset guardado</label>
                    <select
                      value={presetId}
                      onChange={(event) => applyPreset(event.target.value)}
                      className="input"
                    >
                      <option value="">Sin preset</option>
                      {presets.map((preset) => (
                        <option key={preset.id} value={preset.id}>
                          {preset.name}
                        </option>
                      ))}
                    </select>
                  </div>
                  <div className="field" style={{ marginBottom: 0 }}>
                    <label>Nombre del preset</label>
                    <input
                      value={presetName}
                      onChange={(event) => setPresetName(event.target.value)}
                      className="input"
                      placeholder="Reset cache de intranet"
                    />
                  </div>
                </div>
                <div className="btn-row">
                  <button onClick={() => savePreset("create")} disabled={savingPreset} className="btn btn-secondary">
                    {savingPreset ? "⟳ Guardando..." : "💾 Guardar nuevo"}
                  </button>
                  <button onClick={() => savePreset("update")} disabled={savingPreset || !presetId} className="btn btn-secondary">
                    ✏ Actualizar preset
                  </button>
                  {isSuperAdmin && (
                    <button onClick={deletePreset} disabled={!presetId} className="btn btn-secondary" style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}>
                      🗑 Eliminar
                    </button>
                  )}
                </div>
                <div style={{ fontSize: 11, color: "#64748b", marginTop: 8 }}>
                  Los presets guardan método, ruta, body JSON opcional, timeout y etiqueta para reutilizar acciones frecuentes.
                </div>
              </div>
            )}

            <div className="field">
              <label>Alcance</label>
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                <button
                  onClick={() => {
                    setTargetMode("server_all");
                    // Seleccionar automáticamente todos los servidores cuando se elige "Todo el servidor"
                    const allSelected = {};
                    servers.forEach((server) => {
                      allSelected[server.id] = true;
                    });
                    setSelected(allSelected);
                  }}
                  className="btn btn-secondary"
                  style={{
                    borderColor: targetMode === "server_all" ? "#22d3a5" : "#1e2a3a",
                    color: targetMode === "server_all" ? "#22d3a5" : "#e2e8f0",
                    background: targetMode === "server_all" ? "#22d3a512" : "transparent",
                  }}
                >
                  ⛁ Todo el servidor
                </button>
                <button
                  onClick={() => setTargetMode("selected_users")}
                  className="btn btn-secondary"
                  style={{
                    borderColor: targetMode === "selected_users" ? "#00e5ff" : "#1e2a3a",
                    color: targetMode === "selected_users" ? "#00e5ff" : "#e2e8f0",
                    background: targetMode === "selected_users" ? "#00e5ff12" : "transparent",
                  }}
                >
                  ◎ Usuarios específicos
                </button>
              </div>
            </div>

            {execMode === "single" && (
              <>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(180px,1fr))", gap: 12 }}>
                  <div className="field">
                    <label>Etiqueta</label>
                    <input
                      value={form.action_label}
                      onChange={(event) => setForm((prev) => ({ ...prev, action_label: event.target.value }))}
                      className="input"
                      placeholder="Restablecer sesión"
                    />
                  </div>
                  <div className="field">
                    <label>Método</label>
                    <select
                      value={form.method}
                      onChange={(event) => setForm((prev) => ({ ...prev, method: event.target.value }))}
                      className="input"
                    >
                      {["GET", "POST", "PUT", "PATCH", "DELETE"].map((method) => (
                        <option key={method} value={method}>{method}</option>
                      ))}
                    </select>
                  </div>
                  <div className="field">
                    <label>Timeout (seg)</label>
                    <input
                      type="number"
                      min="1"
                      max="120"
                      value={form.timeout}
                      onChange={(event) => setForm((prev) => ({ ...prev, timeout: event.target.value }))}
                      className="input"
                    />
                  </div>
                </div>

                <div className="field">
                  <label>Ruta relativa</label>
                  <input
                    value={form.route_path}
                    onChange={(event) => setForm((prev) => ({ ...prev, route_path: event.target.value }))}
                    className="input"
                    placeholder="/intranet/Api_extention/control_total/reset_cache"
                  />
                </div>

                <div className="field">
                  <label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }}>
                    <input
                      type="checkbox"
                      checked={!!form.run_per_domain}
                      onChange={(event) => setForm((prev) => ({ ...prev, run_per_domain: event.target.checked }))}
                    />
                    Ejecutar por-dominio (apenas termina el deploy de cada cliente, en segundo plano)
                  </label>
                  <div className="hint mono" style={{ marginTop: "4px", opacity: 0.7 }}>
                    Si está activado, esta acción se dispara después del deploy de cada dominio (no espera al final).
                    Si está desactivado, corre una vez al final del deploy (batch).
                  </div>
                </div>

                <div className="field">
                  <label>Body JSON opcional</label>
                  <textarea
                    value={form.request_body}
                    onChange={(event) => setForm((prev) => ({ ...prev, request_body: event.target.value }))}
                    className="input"
                    rows={5}
                    disabled={form.method === "GET"}
                    placeholder='{"reset":true}'
                    style={{ resize: "vertical" }}
                  />
                </div>
              </>
            )}

            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 14 }}>
              <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                {selectedServers.length} servidor{selectedServers.length !== 1 ? "es" : ""} seleccionado{selectedServers.length !== 1 ? "s" : ""}
              </span>
              <span className="tag" style={{ borderColor: targetMode === "server_all" ? "#22d3a544" : "#00e5ff44", color: targetMode === "server_all" ? "#22d3a5" : "#00e5ff", background: targetMode === "server_all" ? "#22d3a515" : "#00e5ff15" }}>
                {targetMode === "server_all" ? "Todo el servidor" : `${selectedUserCount} usuario${selectedUserCount !== 1 ? "s" : ""} elegido${selectedUserCount !== 1 ? "s" : ""}`}
              </span>
              <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                Solo usuarios con dominio
              </span>
              <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                Excluidos no participan
              </span>
            </div>

            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(220px,1fr))", gap: 10, marginBottom: 14 }}>
              {servers.map((server) => {
                const isSelected = Boolean(selected[server.id]);
                return (
                  <button
                    key={server.id}
                    onClick={() => setSelected((prev) => ({ ...prev, [server.id]: !prev[server.id] }))}
                    className="btn btn-secondary"
                    style={{
                      textAlign: "left",
                      padding: 12,
                      borderColor: isSelected ? "#00e5ff" : "#1e2a3a",
                      color: isSelected ? "#00e5ff" : "#e2e8f0",
                      background: isSelected ? "#00e5ff10" : "transparent",
                    }}
                  >
                    <div style={{ fontWeight: 700 }}>{server.label}</div>
                    <div style={{ fontSize: 11, color: isSelected ? "#c8f9ff" : "#64748b", marginTop: 4 }}>{server.host}</div>
                  </button>
                );
              })}
            </div>

            {targetMode === "selected_users" && selectedServers.length > 0 && (
              <div style={{ marginBottom: 14 }}>
                <div style={{ fontSize: 10, color: "#64748b", letterSpacing: "1px", textTransform: "uppercase", fontWeight: 700, marginBottom: 8 }}>
                  Selección manual de usuarios
                </div>
                {loadingUsers && (
                  <div className="alert alert-info" style={{ marginBottom: 10 }}>
                    Cargando usuarios de los servidores seleccionados...
                  </div>
                )}
                <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                  {selectedServers.map((server) => {
                    const eligibleUsers = eligibleUsersForServer(server.id);
                    const selectedMap = userSelection[server.id] || {};

                    return (
                      <div key={server.id} style={{ background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 12 }}>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 8 }}>
                          <div>
                            <div style={{ fontWeight: 700, fontSize: 12 }}>{server.label}</div>
                            <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                              {eligibleUsers.length} usuario{eligibleUsers.length !== 1 ? "s" : ""} elegible{eligibleUsers.length !== 1 ? "s" : ""}
                            </div>
                          </div>
                          <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                            <button
                              onClick={() => {
                                const next = {};
                                eligibleUsers.forEach((user) => {
                                  next[user.username] = true;
                                });
                                setUserSelection((prev) => ({ ...prev, [server.id]: next }));
                              }}
                              className="btn btn-ghost btn-sm"
                            >
                              Todos
                            </button>
                            <button
                              onClick={() => setUserSelection((prev) => ({ ...prev, [server.id]: {} }))}
                              className="btn btn-ghost btn-sm"
                            >
                              Ninguno
                            </button>
                          </div>
                        </div>

                        {eligibleUsers.length === 0 ? (
                          <div style={{ fontSize: 11, color: "#64748b" }}>No hay usuarios con dominio disponibles en este servidor.</div>
                        ) : (
                          <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                            {eligibleUsers.map((user) => {
                              const isChecked = Boolean(selectedMap[user.username]);
                              return (
                                <button
                                  key={user.username}
                                  onClick={() => setUserSelection((prev) => ({
                                    ...prev,
                                    [server.id]: {
                                      ...prev[server.id],
                                      [user.username]: !prev[server.id]?.[user.username],
                                    },
                                  }))}
                                  className="btn btn-secondary btn-sm"
                                  style={{
                                    borderColor: isChecked ? "#00e5ff" : "#1e2a3a",
                                    color: isChecked ? "#00e5ff" : "#cbd5e1",
                                    background: isChecked ? "#00e5ff12" : "transparent",
                                  }}
                                  title={user.domain}
                                >
                                  {isChecked ? "✓ " : ""}{user.username}
                                </button>
                              );
                            })}
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              </div>
            )}

            {execMode === "queue" && (
              <div style={{ marginBottom: 14, background: "#0b1016", border: "1px solid #a78bfa44", borderRadius: 12, padding: 14 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, flexWrap: "wrap", marginBottom: 10 }}>
                  <div>
                    <div style={{ fontSize: 12, fontWeight: 700, color: "#a78bfa" }}>Cola de presets</div>
                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                      Se ejecutarán en orden. Cada acción espera que la anterior termine en todos los servidores.
                    </div>
                  </div>
                  <div style={{ display: "flex", gap: 6 }}>
                    <button onClick={() => setQueuedPresetIds(presets.map((p) => String(p.id)))} className="btn btn-ghost btn-sm">
                      Todos
                    </button>
                    <button onClick={() => setQueuedPresetIds([])} className="btn btn-ghost btn-sm">
                      Ninguno
                    </button>
                  </div>
                </div>
                {presets.length === 0 ? (
                  <div style={{ fontSize: 11, color: "#64748b" }}>No hay presets guardados. Crea uno primero desde "Una acción".</div>
                ) : (
                  <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                    {presets.map((preset) => {
                      const isQueued = queuedPresetIds.includes(String(preset.id));
                      const queueIndex = queuedPresetIds.indexOf(String(preset.id));
                      return (
                        <button
                          key={preset.id}
                          onClick={() => toggleQueuedPreset(preset.id)}
                          className="btn btn-secondary"
                          style={{
                            textAlign: "left",
                            padding: "10px 12px",
                            borderColor: isQueued ? "#a78bfa" : "#1e2a3a",
                            color: isQueued ? "#a78bfa" : "#e2e8f0",
                            background: isQueued ? "#a78bfa12" : "transparent",
                            display: "flex",
                            alignItems: "center",
                            gap: 10,
                          }}
                        >
                          {isQueued ? (
                            <span style={{ minWidth: 22, height: 22, borderRadius: 6, background: "#a78bfa", color: "#0a0f14", fontSize: 11, fontWeight: 800, display: "flex", alignItems: "center", justifyContent: "center" }}>
                              {queueIndex + 1}
                            </span>
                          ) : (
                            <span style={{ minWidth: 22, height: 22, borderRadius: 6, border: "1px solid #1e2a3a", display: "inline-block" }} />
                          )}
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <div style={{ fontWeight: 700, fontSize: 12 }}>{preset.name}</div>
                            <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                              {preset.method} {preset.route_path} · timeout {Math.round((preset.timeout_ms || 15000) / 1000)}s
                            </div>
                          </div>
                        </button>
                      );
                    })}
                  </div>
                )}
              </div>
            )}

            <div className="btn-row">
              {execMode === "single" && (
                <button
                  onClick={runPrecheck}
                  disabled={starting || loadingUsers}
                  className="btn btn-secondary"
                >
                  {precheckState.loading ? "⟳ Revisando..." : "🔎 Precheck previo"}
                </button>
              )}
              <button
                onClick={openActionModal}
                disabled={starting || loadingUsers}
                className="btn btn-primary"
              >
                {starting
                  ? "⟳ Cargando usuarios..."
                  : execMode === "queue"
                    ? `📋 Ejecutar cola (${selectedQueuedPresets.length} preset${selectedQueuedPresets.length !== 1 ? "s" : ""})`
                    : "⚡ Ejecutar acción HTTP"}
              </button>
            </div>

            {precheckState.summary && (
              <div style={{ marginTop: 14, background: "#0b1016", border: "1px solid #1e2a3a", borderRadius: 12, padding: 14 }}>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap", marginBottom: 10 }}>
                  <div>
                    <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>Resultado del precheck</div>
                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                      Revisado en {fmtDate(precheckState.summary.checked_at)} sobre {precheckState.summary.server_count} servidor{precheckState.summary.server_count !== 1 ? "es" : ""}
                    </div>
                  </div>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                    <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                      listos {precheckState.summary.ready_count}
                    </span>
                    <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                      omitidos {precheckState.summary.skipped_count}
                    </span>
                    <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                      total {precheckState.summary.total}
                    </span>
                  </div>
                </div>

                {precheckState.summary.inactive_rows.length > 0 ? (
                  <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                    <div style={{ fontSize: 11, color: "#94a3b8" }}>
                      Estos dominios quedarían omitidos si ejecutas la acción ahora:
                    </div>
                    <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 220, overflowY: "auto", paddingRight: 4 }}>
                      {precheckState.summary.inactive_rows.map((row, index) => (
                        <div key={`${row.server_label}-${row.virt_user}-${index}`} style={{ background: "#131820", border: "1px solid #1e2a3a", borderRadius: 10, padding: 10 }}>
                          <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
                            <div>
                              <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>{row.virt_user} · {row.domain}</div>
                              <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                                {row.server_label} · estado base {row.status} · dns {row.dns_ok ? "ok" : "fail"} · https {row.https_ok ? "ok" : "fail"}
                              </div>
                            </div>
                            <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                              omitido
                            </span>
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                ) : (
                  <div className="alert alert-success" style={{ marginTop: 8 }}>
                    ✔ Todos los dominios revisados están listos para ejecutar la acción.
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
        )}

        {activeTab === "history" && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 12, alignItems: "start" }}>
          <div className="card">
            <div className="card-head">
              <span>🕘 Historial</span>
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                {histTotal > 0 && (
                  <span style={{ fontSize: 11, color: "#64748b" }}>
                    {histPage * HIST_PAGE_SIZE + 1}–{Math.min((histPage + 1) * HIST_PAGE_SIZE, histTotal)} de {histTotal}
                  </span>
                )}
                {isSuperAdmin && (
                  <button
                    onClick={clearHistory}
                    className="btn btn-ghost btn-sm"
                    style={{ color: "#f43f5e", borderColor: "#f43f5e44", fontSize: 10 }}
                  >
                    🗑 Limpiar
                  </button>
                )}
              </div>
            </div>
            <div>
              {history.length === 0 && (
                <div style={{ padding: "18px 16px", color: "#64748b", fontSize: 12 }}>Sin acciones registradas todavía.</div>
              )}
              {history.map((session) => {
                const tone = statusTone(session.status);
                const isExpanded = expandedSession === session.session_key;
                const sessionName = session.session_label
                  || session.action_label
                  || (session.route_path ? `${session.method || "GET"} ${session.route_path}` : "Acción HTTP");
                const errCount = (session.error_count || 0) + (session.timeout_count || 0);
                const isMulti = (session.batch_count || 1) > 1;
                const batches = sessionBatches[session.session_key] || [];

                return (
                  <div key={session.session_key} style={{ borderBottom: "1px solid #1e2a3a15" }}>
                    {/* Fila de sesión */}
                    <button
                      onClick={() => toggleSession(session.session_key)}
                      className="btn"
                      style={{
                        width: "100%",
                        textAlign: "left",
                        background: isExpanded ? "#00e5ff08" : "transparent",
                        borderRadius: 0,
                        borderLeft: isExpanded ? "3px solid #00e5ff" : "3px solid transparent",
                        padding: "12px 16px",
                        color: "#e2e8f0",
                      }}
                    >
                      <div style={{ display: "flex", gap: 8, justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap" }}>
                        <div style={{ minWidth: 0, flex: 1 }}>
                          <div style={{ fontSize: 12, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{sessionName}</div>
                          <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>
                            {isMulti
                              ? `${session.batch_count} lotes · ${session.server_count} servidor${session.server_count !== 1 ? "es" : ""}`
                              : `${session.server_count} servidor${session.server_count !== 1 ? "es" : ""}`}
                            {" · "}{fmtDate(session.started_at)}
                            {session.triggered_by ? ` · por ${session.triggered_by}` : ""}
                          </div>
                        </div>
                        <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                          <span className="tag" style={{ borderColor: `${tone}44`, color: tone, background: `${tone}15` }}>
                            {statusIcon(session.status)} {session.status}
                          </span>
                          {isSuperAdmin && (
                            <button
                              onClick={(e) => { e.stopPropagation(); deleteSession(session.session_key, sessionName); }}
                              className="btn btn-ghost btn-sm"
                              style={{ padding: "2px 6px", fontSize: 11, color: "#f43f5e", borderColor: "#f43f5e33", lineHeight: 1 }}
                              title="Eliminar esta sesión"
                            >
                              🗑
                            </button>
                          )}
                          <span style={{ fontSize: 10, color: "#475569" }}>{isExpanded ? "▲" : "▼"}</span>
                        </div>
                      </div>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 8 }}>
                        <button
                          onClick={(e) => { e.stopPropagation(); if (!isExpanded) { toggleSession(session.session_key); } setSessStatusFilter((f) => f === "ok" ? null : "ok"); setSessBatchPage(0); }}
                          className="tag"
                          style={{ borderColor: "#22d3a544", color: "#22d3a5", background: sessStatusFilter === "ok" && isExpanded ? "#22d3a525" : "#22d3a515", cursor: "pointer", outline: sessStatusFilter === "ok" && isExpanded ? "1px solid #22d3a5" : "none" }}
                          title="Filtrar lotes con éxitos"
                        >
                          ✔ {session.ok_count || 0}
                        </button>
                        <button
                          onClick={(e) => { e.stopPropagation(); if (!isExpanded) { toggleSession(session.session_key); } setSessStatusFilter((f) => f === "error" ? null : "error"); setSessBatchPage(0); }}
                          className="tag"
                          style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: sessStatusFilter === "error" && isExpanded ? "#f43f5e25" : "#f43f5e15", cursor: "pointer", outline: sessStatusFilter === "error" && isExpanded ? "1px solid #f43f5e" : "none" }}
                          title="Filtrar lotes con errores"
                        >
                          ✗ {errCount}
                        </button>
                        {(session.skipped_count || 0) > 0 && (
                          <button
                            onClick={(e) => { e.stopPropagation(); if (!isExpanded) { toggleSession(session.session_key); } setSessStatusFilter((f) => f === "skipped" ? null : "skipped"); setSessBatchPage(0); }}
                            className="tag"
                            style={{ borderColor: "#94a3b844", color: "#94a3b8", background: sessStatusFilter === "skipped" && isExpanded ? "#94a3b825" : "#94a3b815", cursor: "pointer", outline: sessStatusFilter === "skipped" && isExpanded ? "1px solid #94a3b8" : "none" }}
                            title="Filtrar lotes con omitidos"
                          >
                            ⏭ {session.skipped_count}
                          </button>
                        )}
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          {session.total_targets || 0} usuarios
                        </span>
                      </div>
                    </button>

                    {/* Lotes expandidos */}
                    {isExpanded && (() => {
                      // El backend ya filtra por texto (search). Solo aplicamos filtro de estado client-side.
                      const filtered = batches.filter((b) => {
                        if (sessStatusFilter === "ok" && (b.error_count || 0) + (b.timeout_count || 0) > 0) return false;
                        if (sessStatusFilter === "error" && (b.error_count || 0) + (b.timeout_count || 0) === 0) return false;
                        if (sessStatusFilter === "skipped" && (b.skipped_count || 0) === 0) return false;
                        return true;
                      });
                      const totalBatches = filtered.length;
                      const paginated = filtered.slice(sessBatchPage * SESS_BATCH_PAGE_SIZE, (sessBatchPage + 1) * SESS_BATCH_PAGE_SIZE);
                      const totalPages = Math.ceil(totalBatches / SESS_BATCH_PAGE_SIZE);

                      return (
                        <div style={{ background: "#060810", borderTop: "1px solid #1e2a3a22" }}>
                          {/* Barra de acciones + buscador */}
                          <div style={{ padding: "8px 12px", display: "flex", gap: 8, alignItems: "center", borderBottom: "1px solid #1e2a3a22", flexWrap: "wrap" }}>
                            <button
                              onClick={() => exportSessionPdf(session)}
                              className="btn btn-ghost btn-sm"
                              style={{ fontSize: 11 }}
                            >
                              🧾 PDF sesión
                            </button>
                            {sessStatusFilter && (
                              <button
                                onClick={() => { setSessStatusFilter(null); setSessBatchPage(0); }}
                                className="tag"
                                style={{
                                  cursor: "pointer", fontSize: 10,
                                  borderColor: sessStatusFilter === "ok" ? "#22d3a544" : sessStatusFilter === "error" ? "#f43f5e44" : "#94a3b844",
                                  color: sessStatusFilter === "ok" ? "#22d3a5" : sessStatusFilter === "error" ? "#f43f5e" : "#94a3b8",
                                  background: sessStatusFilter === "ok" ? "#22d3a520" : sessStatusFilter === "error" ? "#f43f5e20" : "#94a3b820",
                                }}
                                title="Quitar filtro"
                              >
                                {sessStatusFilter === "ok" ? "✔ ok" : sessStatusFilter === "error" ? "✗ error" : "⏭ omitidos"} ✕
                              </button>
                            )}
                            <div style={{ flex: 1, minWidth: 160, position: "relative" }}>
                              <input
                                type="text"
                                value={sessSearch}
                                onChange={(e) => {
                                  const val = e.target.value;
                                  setSessSearch(val);
                                  setSessBatchPage(0);
                                  loadSessionBatches(session.session_key, val);
                                }}
                                placeholder="Buscar servidor, acción, dominio, usuario…"
                                style={{
                                  width: "100%", boxSizing: "border-box",
                                  background: "#0d1117", border: "1px solid #1e2a3a",
                                  borderRadius: 8, padding: "5px 28px 5px 10px",
                                  color: "#e2e8f0", fontSize: 11, outline: "none",
                                }}
                              />
                              {sessSearch && (
                                <button
                                  onClick={() => { setSessSearch(""); setSessBatchPage(0); loadSessionBatches(session.session_key, ""); }}
                                  style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", background: "none", border: "none", color: "#64748b", cursor: "pointer", fontSize: 12, padding: 0 }}
                                >✕</button>
                              )}
                            </div>
                            <span style={{ fontSize: 10, color: "#475569", whiteSpace: "nowrap" }}>
                              {sessSearch ? `${totalBatches} de ${batches.length}` : `${batches.length}`} lotes
                            </span>
                          </div>

                          {/* Lista paginada */}
                          {batches.length === 0 ? (
                            <div style={{ padding: "10px 16px", fontSize: 11, color: "#475569" }}>⟳ Cargando lotes...</div>
                          ) : paginated.length === 0 ? (
                            <div style={{ padding: "10px 16px", fontSize: 11, color: "#475569" }}>Sin resultados para "{sessSearch}"</div>
                          ) : paginated.map((batch) => {
                            const bTone = statusTone(batch.status);
                            const bErr = (batch.error_count || 0) + (batch.timeout_count || 0);
                            return (
                              <button
                                key={batch.id}
                                onClick={() => { setDetailSearch(""); loadBatch(batch.id, 0, "", ""); }}
                                className="btn"
                                style={{
                                  width: "100%",
                                  textAlign: "left",
                                  background: detailId === batch.id ? "#00e5ff10" : "transparent",
                                  borderRadius: 0,
                                  borderBottom: "1px solid #1e2a3a10",
                                  borderLeft: detailId === batch.id ? "3px solid #00e5ff" : "3px solid transparent",
                                  padding: "8px 16px 8px 24px",
                                  color: "#e2e8f0",
                                }}
                              >
                                <div style={{ display: "flex", gap: 8, justifyContent: "space-between", alignItems: "center", flexWrap: "wrap" }}>
                                  <div style={{ minWidth: 0, flex: 1 }}>
                                    <div style={{ fontSize: 11, fontWeight: 700 }}>{batch.server_label || `Servidor #${batch.server_id}`}</div>
                                    <div style={{ fontSize: 10, color: "#475569" }}>{batch.action_label || `${batch.method} ${batch.route_path}`}</div>
                                  </div>
                                  <div style={{ display: "flex", gap: 5, flexWrap: "wrap", alignItems: "center" }}>
                                    <span className="tag" style={{ fontSize: 10, borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>✔ {batch.ok_count || 0}</span>
                                    {bErr > 0 && <span className="tag" style={{ fontSize: 10, borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>✗ {bErr}</span>}
                                    {(batch.skipped_count || 0) > 0 && <span className="tag" style={{ fontSize: 10, borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>⏭ {batch.skipped_count}</span>}
                                    <span className="tag" style={{ fontSize: 10, borderColor: `${bTone}44`, color: bTone, background: `${bTone}15` }}>{batch.status}</span>
                                  </div>
                                </div>
                              </button>
                            );
                          })}

                          {/* Paginador de lotes */}
                          {totalPages > 1 && (
                            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 16px", borderTop: "1px solid #1e2a3a22" }}>
                              <button
                                onClick={() => setSessBatchPage((p) => Math.max(0, p - 1))}
                                disabled={sessBatchPage === 0}
                                className="btn btn-ghost btn-sm"
                                style={{ opacity: sessBatchPage === 0 ? 0.3 : 1, fontSize: 11 }}
                              >← Ant</button>
                              <span style={{ fontSize: 11, color: "#64748b" }}>
                                {sessBatchPage + 1} / {totalPages}
                                <span style={{ color: "#475569", marginLeft: 6 }}>
                                  ({sessBatchPage * SESS_BATCH_PAGE_SIZE + 1}–{Math.min((sessBatchPage + 1) * SESS_BATCH_PAGE_SIZE, totalBatches)} de {totalBatches})
                                </span>
                              </span>
                              <button
                                onClick={() => setSessBatchPage((p) => Math.min(totalPages - 1, p + 1))}
                                disabled={sessBatchPage >= totalPages - 1}
                                className="btn btn-ghost btn-sm"
                                style={{ opacity: sessBatchPage >= totalPages - 1 ? 0.3 : 1, fontSize: 11 }}
                              >Sig →</button>
                            </div>
                          )}
                        </div>
                      );
                    })()}
                  </div>
                );
              })}

              {histTotal > HIST_PAGE_SIZE && (
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "10px 16px", borderTop: "1px solid #1e2a3a22" }}>
                  <button
                    onClick={() => loadHistory(histPage - 1)}
                    disabled={histPage === 0}
                    className="btn btn-ghost btn-sm"
                    style={{ opacity: histPage === 0 ? 0.3 : 1 }}
                  >
                    ← Anterior
                  </button>
                  <span style={{ fontSize: 11, color: "#64748b" }}>
                    Página {histPage + 1} / {Math.ceil(histTotal / HIST_PAGE_SIZE)}
                  </span>
                  <button
                    onClick={() => loadHistory(histPage + 1)}
                    disabled={(histPage + 1) * HIST_PAGE_SIZE >= histTotal}
                    className="btn btn-ghost btn-sm"
                    style={{ opacity: (histPage + 1) * HIST_PAGE_SIZE >= histTotal ? 0.3 : 1 }}
                  >
                    Siguiente →
                  </button>
                </div>
              )}
            </div>
          </div>
        </div>
        )}

        {activeTab === "detail" && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 12, alignItems: "start" }}>
          <div className="card">
            <div className="card-head">🔎 Detalle del lote</div>
            {!selectedDetail && (
              <div style={{ padding: "18px 16px", color: "#64748b", fontSize: 12 }}>
                Selecciona un lote del historial para ver respuestas por usuario.
              </div>
            )}
            {selectedDetail && (
              <div>
                <div style={{ padding: "14px 16px", borderBottom: "1px solid #1e2a3a22" }}>
                    <div style={{ display: "flex", gap: 8, justifyContent: "space-between", flexWrap: "wrap", marginBottom: 8 }}>
                      <div>
                        <div style={{ fontWeight: 700, fontSize: 13 }}>
                          {selectedDetail.batch.action_label || `${selectedDetail.batch.method} ${selectedDetail.batch.route_path}`}
                      </div>
                      <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                        {selectedDetail.batch.server_label} · {fmtDate(selectedDetail.batch.started_at)}
                      </div>
                    </div>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "flex-start" }}>
                      <span className="tag" style={{ borderColor: `${statusTone(selectedDetail.batch.status)}44`, color: statusTone(selectedDetail.batch.status), background: `${statusTone(selectedDetail.batch.status)}15` }}>
                        {statusIcon(selectedDetail.batch.status)} {selectedDetail.batch.status}
                      </span>
                      <button onClick={exportSelectedDetailPdf} className="btn btn-ghost btn-sm">
                        🧾 Exportar PDF
                      </button>
                      {retryableResults.length > 0 && !["pending", "running"].includes(selectedDetail.batch.status) && (
                        <button onClick={() => retryFailed(selectedDetail.batch.id)} className="btn btn-ghost btn-sm">
                          ↻ Reintentar fallidos
                        </button>
                      )}
                      {["pending", "running"].includes(selectedDetail.batch.status) && (
                        <button onClick={() => stopBatch(selectedDetail.batch.id)} className="btn btn-ghost btn-sm" style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}>
                          ⏹ Detener
                        </button>
                      )}
                    </div>
                  </div>

                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                    <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                      {selectedDetail.batch.method} {selectedDetail.batch.route_path}
                    </span>
                    <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                      timeout {Math.round((selectedDetail.batch.timeout_ms || 0) / 1000)}s
                    </span>
                    <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                      {detailTotal} resultado{detailTotal !== 1 ? "s" : ""}
                      {detailFilter ? ` (filtro: ${detailFilter})` : ""}
                    </span>
                  </div>

                  {/* Buscador texto */}
                  <div style={{ position: "relative", marginTop: 10 }}>
                    <input
                      type="text"
                      value={detailSearch}
                      onChange={(e) => changeDetailSearch(e.target.value)}
                      placeholder="Buscar usuario, dominio, URL…"
                      style={{
                        width: "100%", boxSizing: "border-box",
                        background: "#0d1117", border: "1px solid #1e2a3a",
                        borderRadius: 8, padding: "6px 28px 6px 10px",
                        color: "#e2e8f0", fontSize: 11, outline: "none",
                      }}
                    />
                    {detailSearch && (
                      <button
                        onClick={() => changeDetailSearch("")}
                        style={{ position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)", background: "none", border: "none", color: "#64748b", cursor: "pointer", fontSize: 12, padding: 0 }}
                      >✕</button>
                    )}
                  </div>

                  {/* Filtros de estado */}
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 8 }}>
                    {[
                      ["", "Todos", "#64748b"],
                      ["ok", "✔ OK", "#22d3a5"],
                      ["failed", "✗ Fallidos", "#f43f5e"],
                      ["skipped", "⏭ Omitidos", "#94a3b8"],
                    ].map(([value, label, color]) => (
                      <button
                        key={value}
                        onClick={() => changeDetailFilter(value)}
                        className="btn btn-ghost btn-sm"
                        style={{
                          color: detailFilter === value ? color : "#64748b",
                          borderColor: detailFilter === value ? `${color}66` : "#1e2a3a",
                          background: detailFilter === value ? `${color}15` : "transparent",
                          fontSize: 11,
                        }}
                      >
                        {label}
                      </button>
                    ))}
                  </div>
                </div>

                {(selectedDetail.results || []).map((result) => (
                  <div
                    key={result.id}
                    style={{
                      padding: "12px 16px",
                      borderBottom: "1px solid #1e2a3a15",
                      display: "flex",
                      gap: 10,
                      flexWrap: "wrap",
                      alignItems: "flex-start",
                    }}
                  >
                    <span style={{ fontSize: 14, color: statusTone(result.status) }}>{statusIcon(result.status)}</span>
                    <div style={{ flex: 1, minWidth: 140 }}>
                      <div style={{ fontSize: 12, fontWeight: 700 }}>{result.virt_user}</div>
                      <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>{result.domain}</div>
                      <div style={{ fontSize: 10, color: "#475569", marginTop: 4, wordBreak: "break-all" }}>{result.target_url}</div>
                      {result.response_excerpt && (
                        <div style={{ fontSize: 10, color: "#94a3b8", marginTop: 6, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                          {result.response_excerpt.slice(0, 240)}
                        </div>
                      )}
                    </div>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
                      <span className="tag" style={{ borderColor: `${statusTone(result.status)}44`, color: statusTone(result.status), background: `${statusTone(result.status)}15` }}>
                        {result.status}
                      </span>
                      {result.http_status > 0 && (
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          HTTP {result.http_status}
                        </span>
                      )}
                      {result.duration_ms != null && (
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          {result.duration_ms}ms
                        </span>
                      )}
                      {RETRYABLE_RESULT_STATUSES.has(result.status) && !["pending", "running"].includes(selectedDetail.batch.status) && (
                        <button
                          onClick={() => retryFailed(selectedDetail.batch.id, [result.virt_user])}
                          className="btn btn-ghost btn-sm"
                        >
                          ↻ Reintentar
                        </button>
                      )}
                    </div>
                  </div>
                ))}

                {detailTotal > DETAIL_PAGE_SIZE && (
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "10px 16px", borderTop: "1px solid #1e2a3a22" }}>
                    <button
                      onClick={() => changeDetailPage(detailPage - 1)}
                      disabled={detailPage === 0}
                      className="btn btn-ghost btn-sm"
                      style={{ opacity: detailPage === 0 ? 0.3 : 1 }}
                    >
                      ← Anterior
                    </button>
                    <span style={{ fontSize: 11, color: "#64748b" }}>
                      {detailPage * DETAIL_PAGE_SIZE + 1}–{Math.min((detailPage + 1) * DETAIL_PAGE_SIZE, detailTotal)} de {detailTotal}
                    </span>
                    <button
                      onClick={() => changeDetailPage(detailPage + 1)}
                      disabled={(detailPage + 1) * DETAIL_PAGE_SIZE >= detailTotal}
                      className="btn btn-ghost btn-sm"
                      style={{ opacity: (detailPage + 1) * DETAIL_PAGE_SIZE >= detailTotal ? 0.3 : 1 }}
                    >
                      Siguiente →
                    </button>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
        )}

        {/* HttpActionModal — se abre al ejecutar una acción o cola */}
        {showActionModal && (() => {
          const HttpActionModal = window.DCComponents.HttpActionModal;
          return (
            <HttpActionModal
              users={modalUsers}
              presets={presets}
              initialForm={
                execMode === "queue"
                  ? { mode: "multiple", queued_preset_ids: queuedPresetIds }
                  : {
                      mode: "single",
                      action_label: form.action_label,
                      route_path: form.route_path,
                      method: form.method,
                      request_body: form.request_body,
                      timeout: Number(form.timeout),
                      preset_id: presetId,
                    }
              }
              onClose={() => { setShowActionModal(false); loadHistory(histPage); }}
            />
          );
        })()}

        {modalLoading && (
          <ModalLoadingOverlay
            title={modalLoading.title}
            message={modalLoading.message}
          />
        )}

      </div>
    );
  }

  window.DCViews.ActionsView = ActionsView;
})();
