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

  function TemplatesPicker({ onApply }) {
    const [templates, setTemplates] = useState([]);
    const [open, setOpen] = useState(false);

    useEffect(() => {
      apiFetch("/templates").then((data) => setTemplates(Array.isArray(data) ? data : []));
    }, []);

    if (!templates.length) return null;

    return (
      <div style={{ position: "relative", display: "inline-block" }}>
        <button
          type="button"
          onClick={() => setOpen((p) => !p)}
          className="btn btn-ghost btn-sm"
          style={{ color: "#a78bfa", borderColor: "#a78bfa44" }}
        >
          📋 Cargar template
        </button>
        {open && (
          <div style={{
            position: "absolute",
            top: "100%",
            right: 0,
            zIndex: 100,
            background: "#131820",
            border: "1px solid #1e2a3a",
            borderRadius: 10,
            padding: 8,
            minWidth: 240,
            boxShadow: "0 8px 24px rgba(0,0,0,.5)",
            marginTop: 4,
          }}>
            {templates.map((tmpl) => (
              <button
                key={tmpl.id}
                type="button"
                onClick={() => { onApply(tmpl); setOpen(false); }}
                style={{
                  display: "block",
                  width: "100%",
                  textAlign: "left",
                  padding: "8px 10px",
                  background: "transparent",
                  border: "none",
                  borderRadius: 6,
                  color: "#e2e8f0",
                  cursor: "pointer",
                  fontSize: 12,
                }}
                onMouseOver={(e) => { e.currentTarget.style.background = "#1e2a3a"; }}
                onMouseOut={(e) => { e.currentTarget.style.background = "transparent"; }}
              >
                <div style={{ fontWeight: 700 }}>{tmpl.name}</div>
                {tmpl.description && (
                  <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>{tmpl.description}</div>
                )}
                <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                  branch: {tmpl.branch}
                </div>
              </button>
            ))}
          </div>
        )}
      </div>
    );
  }

  function DeployTabs({ 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 buildDeployPrecheckReportHtml(precheck, branch) {
    if (!precheck) return "";

    return `
      <section class="report-section">
        <div class="metrics">
          <div class="metric">
            <div class="metric-label">Branch</div>
            <div class="metric-value">${escapeHtml(branch || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Servidores</div>
            <div class="metric-value">${escapeHtml(precheck.server_count || 0)}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Listos</div>
            <div class="metric-value status-ok">${escapeHtml(precheck.ready_count || 0)}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Omitidos</div>
            <div class="metric-value status-skipped">${escapeHtml(precheck.skipped_count || 0)}</div>
          </div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen previo</h2>
        <div class="callout">
          Este reporte muestra los dominios candidatos para la acción HTTP post-deploy antes de ejecutar el deploy.
          Los marcados como omitidos se saltarán automáticamente si siguen caídos, sin DNS o en timeout al momento de correr la acción.
        </div>
        <div class="meta-list" style="margin-top:12px">
          <div class="meta-item">Revisado: <strong>${escapeHtml(fmtFullDate(precheck.checked_at))}</strong></div>
          <div class="meta-item">Total revisados: <strong>${escapeHtml(precheck.total || 0)}</strong></div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen por servidor</h2>
        <table>
          <thead>
            <tr>
              <th>Servidor</th>
              <th>Listos</th>
              <th>Omitidos</th>
              <th>Total</th>
            </tr>
          </thead>
          <tbody>
            ${(precheck.servers || []).map((server) => `
              <tr>
                <td>${escapeHtml(server.server_label || "—")}</td>
                <td class="status-ok">${escapeHtml(server.ready_count || 0)}</td>
                <td class="status-skipped">${escapeHtml(server.skipped_count || 0)}</td>
                <td>${escapeHtml(server.total || 0)}</td>
              </tr>
            `).join("") || `<tr><td colspan="4" class="muted">Sin servidores evaluados.</td></tr>`}
          </tbody>
        </table>
      </section>

      <section class="report-section">
        <h2>Dominios omitibles por precheck</h2>
        ${precheck.inactive_rows?.length ? `
          <table>
            <thead>
              <tr>
                <th>Servidor</th>
                <th>Usuario</th>
                <th>Dominio</th>
                <th>Estado base</th>
                <th>Detalle</th>
              </tr>
            </thead>
            <tbody>
              ${precheck.inactive_rows.map((row) => `
                <tr>
                  <td>${escapeHtml(row.server_label || "—")}</td>
                  <td>${escapeHtml(row.virt_user || "—")}</td>
                  <td>${escapeHtml(row.domain || "—")}</td>
                  <td class="status-skipped">${escapeHtml(row.status || "—")}</td>
                  <td class="mono">dns=${row.dns_ok ? "ok" : "fail"} · https=${row.https_ok ? "ok" : "fail"} · http=${escapeHtml(row.http_status || 0)} · ${escapeHtml(row.response_ms ?? 0)}ms</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted">No se detectaron dominios omitibles en el precheck previo.</div>'}
      </section>
    `;
  }

  function buildTestSyncHistoryDetailHtml(detail) {
    if (!detail) return "";
    const isHttp = detail.type === "http_action";
    const actions = Array.isArray(detail.details?.actions) ? detail.details.actions : [];
    const incomingFiles = Array.isArray(detail.details?.incoming_files) ? detail.details.incoming_files : [];

    return `
      <section class="report-section">
        <div class="metrics">
          <div class="metric">
            <div class="metric-label">Tipo</div>
            <div class="metric-value">${escapeHtml(isHttp ? "Acción HTTP" : "Sync")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Estado</div>
            <div class="metric-value ${detail.status === "ok" ? "status-ok" : "status-err"}">${escapeHtml(detail.status || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Actor</div>
            <div class="metric-value">${escapeHtml(detail.actor_username || "sistema")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Fecha</div>
            <div class="metric-value">${escapeHtml(fmtFullDate(detail.created_at))}</div>
          </div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen</h2>
        <div class="callout">${escapeHtml(detail.summary || "Sin resumen")}</div>
      </section>

      ${!isHttp ? `
        <section class="report-section">
          <h2>Commits</h2>
          <div class="meta-list">
            <div class="meta-item">Actual: <strong>${escapeHtml(detail.details?.current_commit || "—")}</strong></div>
            <div class="meta-item">Remoto: <strong>${escapeHtml(detail.details?.remote_commit || "—")}</strong></div>
            <div class="meta-item">Fallback aplicado: <strong>${escapeHtml(detail.fallback_used ? "sí" : "no")}</strong></div>
          </div>
        </section>

        <section class="report-section">
          <h2>Archivos pasados (${incomingFiles.length})</h2>
          ${incomingFiles.length ? `
            <table>
              <thead>
                <tr>
                  <th>Estado</th>
                  <th>Ruta</th>
                </tr>
              </thead>
              <tbody>
                ${incomingFiles.map((file) => `
                  <tr>
                    <td>${escapeHtml(file.status || "—")}</td>
                    <td>${escapeHtml(file.path || "—")}</td>
                  </tr>
                `).join("")}
              </tbody>
            </table>
          ` : '<div class="muted">Sin archivos registrados.</div>'}
        </section>
      ` : `
        <section class="report-section">
          <h2>URL base</h2>
          <div class="meta-list">
            <div class="meta-item"><strong>${escapeHtml(detail.details?.base_url || "—")}</strong></div>
          </div>
        </section>
      `}

      <section class="report-section">
        <h2>Acciones (${actions.length})</h2>
        ${actions.length ? `
          <table>
            <thead>
              <tr>
                <th>Etiqueta</th>
                <th>Método</th>
                <th>Ruta</th>
                <th>Estado</th>
              </tr>
            </thead>
            <tbody>
              ${actions.map((action) => `
                <tr>
                  <td>${escapeHtml(action.label || action.key || "—")}</td>
                  <td>${escapeHtml(action.method || "—")}</td>
                  <td>${escapeHtml(action.route_path || "—")}</td>
                  <td>${escapeHtml(action.status || "—")}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted">Sin acciones registradas.</div>'}
      </section>
    `;
  }

  function buildTestSyncHistoryDetailText(detail) {
    if (!detail) return "";
    const lines = [];
    const actions = Array.isArray(detail.details?.actions) ? detail.details.actions : [];
    const incomingFiles = Array.isArray(detail.details?.incoming_files) ? detail.details.incoming_files : [];

    lines.push(`Tipo: ${detail.type === "http_action" ? "Acción HTTP" : "Sync"}`);
    lines.push(`Estado: ${detail.status || "—"}`);
    lines.push(`Actor: ${detail.actor_username || "sistema"}`);
    lines.push(`Fecha: ${fmtFullDate(detail.created_at)}`);
    lines.push(`Resumen: ${detail.summary || "—"}`);
    lines.push("");

    if (detail.type === "sync") {
      lines.push(`Commit actual: ${detail.details?.current_commit || "—"}`);
      lines.push(`Commit remoto: ${detail.details?.remote_commit || "—"}`);
      lines.push(`Fallback aplicado: ${detail.fallback_used ? "sí" : "no"}`);
      lines.push("");
      lines.push(`Archivos pasados (${incomingFiles.length}):`);
      incomingFiles.forEach((file) => lines.push(`- [${file.status || "—"}] ${file.path || "—"}`));
      lines.push("");
    } else {
      lines.push(`URL base: ${detail.details?.base_url || "—"}`);
      lines.push("");
    }

    lines.push(`Acciones (${actions.length}):`);
    actions.forEach((action, index) => {
      lines.push(`${index + 1}. ${action.label || action.key || "—"} · ${action.method || "—"} ${action.route_path || ""} · ${action.status || "—"}`);
    });
    lines.push("");
    return lines.join("\n");
  }

  function TestHttpActionModal({ presets = [], baseUrl, toast, onClose, onExecuted }) {
    const [mode, setMode] = useState("single");
    const [presetId, setPresetId] = useState("");
    const [queuedPresetIds, setQueuedPresetIds] = useState([]);
    const [form, setForm] = useState({
      action_label: "",
      route_path: "/intranet/",
      method: "GET",
      request_body: "",
      timeout: 15,
    });
    const [submitting, setSubmitting] = useState(false);
    const [httpResult, setHttpResult] = useState(null);
    const [liveProgress, setLiveProgress] = useState(null);

    const applyPreset = (nextPresetId) => {
      setPresetId(nextPresetId);
      const preset = presets.find((item) => String(item.id) === String(nextPresetId));
      if (!preset) return;
      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)),
      });
    };

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

    const startHttpAction = async () => {
      setSubmitting(true);
      setHttpResult(null);
      setLiveProgress(null);

      const queue = mode === "multiple"
        ? presets
            .filter((preset) => queuedPresetIds.includes(String(preset.id)))
            .map((preset) => ({
              label: preset.name,
              body: {
                mode: "single",
                action_label: preset.action_label || "",
                route_path: preset.route_path,
                method: preset.method || "GET",
                request_body: preset.request_body || "",
                timeout_ms: preset.timeout_ms || 15000,
              },
            }))
        : [{
            label: form.action_label || `${form.method} ${form.route_path}`,
            body: {
              mode: "single",
              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,
            },
          }];

      const aggregated = [];
      let totalOk = 0;
      let resolvedBaseUrl = baseUrl || "";

      for (let index = 0; index < queue.length; index += 1) {
        const current = queue[index];
        setLiveProgress({
          current: index + 1,
          total: queue.length,
          label: current.label,
        });

        const response = await apiFetch("/deploy/test-sync/http-actions/run", {
          method: "POST",
          body: current.body,
        });

        if (response?.error) {
          toast(response.error, "err");
          aggregated.push({
            label: current.label,
            method: current.body.method,
            route_path: current.body.route_path,
            target_url: `${baseUrl || ""}${current.body.route_path || ""}`,
            http_status: 0,
            status: "error",
            duration_ms: 0,
            response_excerpt: response.error,
          });
        } else {
          resolvedBaseUrl = response.base_url || resolvedBaseUrl;
          totalOk += response.ok_count || 0;
          aggregated.push(...(response.actions || []));
        }
      }

      const finalOk = aggregated.filter((item) => item.status === "ok").length;
      const finalResult = {
        ok: aggregated.every((item) => item.status === "ok"),
        status: aggregated.every((item) => item.status === "ok") ? "ok" : "error",
        base_url: resolvedBaseUrl,
        total_actions: aggregated.length,
        ok_count: finalOk || totalOk,
        actions: aggregated,
      };

      setHttpResult(finalResult);
      setLiveProgress(null);
      toast(finalResult.ok ? "Acción HTTP ejecutada en test" : "Acción HTTP con errores en test", finalResult.ok ? "ok" : "err");
      if (typeof onExecuted === "function") onExecuted();
      setSubmitting(false);
    };

    return (
      <div className="modal-overlay">
        <div className="modal modal-lg">
          <div className="modal-head">
            <span>⚡ Acciones HTTP en test</span>
            {!submitting && (
              <button onClick={onClose} className="btn btn-ghost btn-sm">
                ✕
              </button>
            )}
          </div>
          <div className="modal-body">
            <div className="alert alert-info" style={{ marginBottom: 14 }}>
              Las acciones se ejecutarán sobre <strong style={{ color: "#e2e8f0" }}>{baseUrl || "sin URL base"}</strong>.
            </div>

            {presets.length > 1 && (
              <div className="field">
                <label>Modo de ejecución</label>
                <div className="btn-row">
                  <button type="button" onClick={() => setMode("single")} className={mode === "single" ? "btn btn-primary" : "btn btn-secondary"}>
                    Una acción
                  </button>
                  <button type="button" onClick={() => setMode("multiple")} className={mode === "multiple" ? "btn btn-primary" : "btn btn-secondary"}>
                    Varias acciones
                  </button>
                </div>
              </div>
            )}

            {mode === "multiple" ? (
              <div className="field">
                <label>Cola de presets</label>
                <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 260, overflowY: "auto", paddingRight: 4 }}>
                  {presets.map((preset) => {
                    const isSelected = queuedPresetIds.includes(String(preset.id));
                    return (
                      <label key={preset.id} style={{ display: "flex", gap: 10, padding: "10px 12px", borderRadius: 10, border: `1px solid ${isSelected ? "#22d3a544" : "#1e2a3a"}`, background: isSelected ? "#22d3a512" : "#131820", cursor: "pointer" }}>
                        <input type="checkbox" checked={isSelected} onChange={() => toggleQueuedPreset(preset.id)} style={{ marginTop: 3 }} />
                        <div>
                          <div style={{ fontWeight: 700, fontSize: 12 }}>{preset.name}</div>
                          <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>{preset.method || "GET"} {preset.route_path}</div>
                        </div>
                      </label>
                    );
                  })}
                </div>
              </div>
            ) : (
              <>
                {presets.length > 0 && (
                  <div className="field">
                    <label>Preset guardado</label>
                    <select value={presetId} onChange={(event) => applyPreset(event.target.value)} className="input select">
                      <option value="">Sin preset</option>
                      {presets.map((preset) => (
                        <option key={preset.id} value={preset.id}>{preset.name}</option>
                      ))}
                    </select>
                  </div>
                )}

                <div className="grid-3">
                  <div className="field">
                    <label>Etiqueta</label>
                    <input value={form.action_label} onChange={(event) => setForm((prev) => ({ ...prev, action_label: event.target.value }))} className="input" placeholder="Reset cache" />
                  </div>
                  <div className="field">
                    <label>Método</label>
                    <select value={form.method} onChange={(event) => setForm((prev) => ({ ...prev, method: event.target.value }))} className="input select">
                      {["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>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"} style={{ resize: "vertical" }} placeholder='{"reset":true}' />
                </div>
              </>
            )}

            {httpResult && (
              <div style={{ marginTop: 14, background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 12 }}>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
                  <span className="tag" style={{ borderColor: httpResult.ok ? "#22d3a544" : "#f43f5e44", color: httpResult.ok ? "#22d3a5" : "#f43f5e", background: httpResult.ok ? "#22d3a515" : "#f43f5e15" }}>
                    {httpResult.status}
                  </span>
                  <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                    {httpResult.base_url}
                  </span>
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                    {httpResult.ok_count}/{httpResult.total_actions} ok
                  </span>
                </div>
                <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                  {(httpResult.actions || []).map((action, index) => (
                    <div key={`${action.route_path}-${index}`} style={{ border: "1px solid #1e2a3a", borderRadius: 8, padding: 10, background: "#131820" }}>
                      <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                        <div style={{ fontWeight: 700, fontSize: 12 }}>{action.label || `${action.method} ${action.route_path}`}</div>
                        <div style={{ fontSize: 10, color: action.status === "ok" ? "#22d3a5" : "#f43f5e" }}>
                          {action.status} · http {action.http_status || 0} · {action.duration_ms}ms
                        </div>
                      </div>
                      <div style={{ fontSize: 10, color: "#64748b", marginBottom: 6 }}>{action.target_url}</div>
                      <pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, color: "#cbd5e1", fontFamily: "JetBrains Mono, monospace" }}>
                        {action.response_excerpt || "(sin salida)"}
                      </pre>
                    </div>
                  ))}
                </div>
              </div>
            )}

            {liveProgress && (
              <div className="alert alert-info" style={{ marginTop: 14 }}>
                Ejecutando acción {liveProgress.current} de {liveProgress.total}
                {liveProgress.label ? ` · ${liveProgress.label}` : ""}
              </div>
            )}
          </div>
          <div className="modal-foot btn-row">
            <button onClick={onClose} className="btn btn-secondary" disabled={submitting}>
              Cerrar
            </button>
            <button onClick={startHttpAction} className="btn btn-primary" disabled={submitting} style={{ opacity: submitting ? 0.7 : 1 }}>
              {submitting ? "Ejecutando..." : "Ejecutar acción"}
            </button>
          </div>
        </div>
      </div>
    );
  }

  function TestSyncCard({ toast, presets = [] }) {
    const HISTORY_PAGE_SIZE = 10;
    const [config, setConfig] = useState(null);
    const [history, setHistory] = useState([]);
    const [historyTotal, setHistoryTotal] = useState(0);
    const [historyCounts, setHistoryCounts] = useState({ all: 0, sync: 0, http_action: 0 });
    const [historyDetail, setHistoryDetail] = useState(null);
    const [historyFilter, setHistoryFilter] = useState("all");
    const [historySearch, setHistorySearch] = useState("");
    const [historyPage, setHistoryPage] = useState(1);
    const [historyReplay, setHistoryReplay] = useState({ running: false, progress: null, result: null });
    const [selectedActions, setSelectedActions] = useState({ git_status: false, sync_target_dir: false });
    const [showAdvanced, setShowAdvanced] = useState(false);
    const [forceRemote, setForceRemote] = useState(false);
    const [running, setRunning] = useState(false);
    const [previewing, setPreviewing] = useState(false);
    const [preview, setPreview] = useState(null);
    const [result, setResult] = useState(null);
    const [httpActionOpen, setHttpActionOpen] = useState(false);
    const [modalLoading, setModalLoading] = useState(null);

    const loadConfig = async () => {
      const response = await apiFetch("/deploy/test-sync");
      setConfig(response || null);
    };

    const loadHistory = async ({
      page = historyPage,
      filter = historyFilter,
      search = historySearch,
    } = {}) => {
      const safePage = Math.max(Number(page) || 1, 1);
      const params = new URLSearchParams({
        limit: String(HISTORY_PAGE_SIZE),
        offset: String((safePage - 1) * HISTORY_PAGE_SIZE),
      });
      if (filter && filter !== "all") params.set("type", filter);
      if (String(search || "").trim()) params.set("search", String(search || "").trim());
      const response = await apiFetch(`/deploy/test-sync/history?${params.toString()}`);
      const nextTotal = Number(response?.total) || 0;
      const maxPage = Math.max(1, Math.ceil(nextTotal / HISTORY_PAGE_SIZE));
      if (safePage > maxPage) {
        setHistoryPage(maxPage);
        return;
      }
      setHistory(Array.isArray(response?.history) ? response.history : []);
      setHistoryTotal(nextTotal);
      setHistoryCounts(response?.counts || { all: 0, sync: 0, http_action: 0 });
    };

    const openHistoryDetail = async (id) => {
      setModalLoading({
        title: "Cargando detalle",
        message: `Abriendo el historial #${id}...`,
      });
      try {
        const response = await apiFetch(`/deploy/test-sync/history/${id}`);
        if (response?.error) {
          toast(response.error, "err");
          return;
        }
        setHistoryReplay({ running: false, progress: null, result: null });
        setHistoryDetail(response);
      } finally {
        setModalLoading(null);
      }
    };

    const exportHistoryDetailPdf = async () => {
      if (!historyDetail) return;
      await openPrintReport({
        title: `Detalle historial test #${historyDetail.id}`,
        subtitle: `${historyDetail.type === "http_action" ? "Acción HTTP" : "Sync"} · ${historyDetail.status || "—"}`,
        bodyHtml: buildTestSyncHistoryDetailHtml(historyDetail),
        filename: `historial-test-${historyDetail.id}`,
      });
    };

    const exportHistoryDetailTxt = () => {
      if (!historyDetail) return;
      const content = buildTestSyncHistoryDetailText(historyDetail);
      const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = `historial-test-${historyDetail.id}.txt`;
      document.body.appendChild(anchor);
      anchor.click();
      document.body.removeChild(anchor);
      URL.revokeObjectURL(url);
    };

    const replayHistoryHttpActions = async () => {
      if (!historyDetail || historyDetail.type !== "http_action") return;
      const actions = Array.isArray(historyDetail.details?.actions) ? historyDetail.details.actions : [];
      if (!actions.length) {
        toast("No hay acciones para re-ejecutar", "err");
        return;
      }

      setHistoryReplay({ running: true, progress: { current: 0, total: actions.length, label: "" }, result: null });
      const aggregated = [];
      let baseUrl = historyDetail.details?.base_url || config?.base_url || "";

      for (let index = 0; index < actions.length; index += 1) {
        const action = actions[index];
        setHistoryReplay((prev) => ({
          ...prev,
          progress: {
            current: index + 1,
            total: actions.length,
            label: action.label || `${action.method || "GET"} ${action.route_path || ""}`,
          },
        }));

        const response = await apiFetch("/deploy/test-sync/http-actions/run", {
          method: "POST",
          body: {
            mode: "single",
            action_label: action.label || action.action_label || "",
            route_path: action.route_path,
            method: action.method || "GET",
            request_body: ["GET", "HEAD"].includes(String(action.method || "GET").toUpperCase()) ? "" : (action.request_body || ""),
            timeout_ms: action.timeout_ms || 15000,
          },
        });

        if (response?.error) {
          aggregated.push({
            label: action.label || `${action.method || "GET"} ${action.route_path || ""}`,
            method: action.method || "GET",
            route_path: action.route_path || "",
            request_body: action.request_body || "",
            timeout_ms: action.timeout_ms || 15000,
            target_url: `${baseUrl}${action.route_path || ""}`,
            http_status: 0,
            status: "error",
            duration_ms: 0,
            response_excerpt: response.error,
          });
        } else {
          baseUrl = response.base_url || baseUrl;
          aggregated.push(...(response.actions || []));
        }
      }

      const okCount = aggregated.filter((item) => item.status === "ok").length;
      const replayResult = {
        ok: aggregated.every((item) => item.status === "ok"),
        status: aggregated.every((item) => item.status === "ok") ? "ok" : "error",
        base_url: baseUrl,
        total_actions: aggregated.length,
        ok_count: okCount,
        actions: aggregated,
      };

      setHistoryReplay({ running: false, progress: null, result: replayResult });
      loadHistory();
      toast(replayResult.ok ? "Acciones re-ejecutadas en test" : "Re-ejecución con errores en test", replayResult.ok ? "ok" : "err");
    };

    useEffect(() => {
      loadConfig();
    }, []);

    useEffect(() => {
      loadHistory();
    }, [historyPage, historyFilter, historySearch]);

    const actions = Array.isArray(config?.actions) ? config.actions : [];
    const advancedActions = actions.filter((item) => ["git_status", "sync_target_dir"].includes(item.key));
    const activeKeys = [
      "git_pull",
      ...advancedActions.filter((item) => selectedActions[item.key]).map((item) => item.key),
    ];

    const toggleAction = (key) => {
      setSelectedActions((prev) => ({ ...prev, [key]: !prev[key] }));
    };

    const run = async () => {
      if (forceRemote) {
        const ok = window.confirm(
          "Vas a ejecutar el pase con \"Forzar a estado remoto\" activo. " +
          "Si hay commits locales en el server de test que no estén en GitHub, se DESCARTARÁN sin recuperación. " +
          "¿Continuar?"
        );
        if (!ok) return;
      }
      setRunning(true);
      setResult(null);
      const response = await apiFetch("/deploy/test-sync/run", {
        method: "POST",
        body: { actions: activeKeys, force_remote: forceRemote },
      });
      if (response?.error) {
        toast(response.error, "err");
      } else {
        setResult(response);
        toast(response.ok ? "Sincronización a test completada" : "Sincronización a test con errores", response.ok ? "ok" : "err");
        loadHistory();
      }
      setRunning(false);
      loadConfig();
    };

    const fetchPreview = async () => {
      setPreviewing(true);
      setModalLoading({
        title: "Preparando modal",
        message: "Generando el preview del pase a test...",
      });
      try {
        const response = await apiFetch("/deploy/test-sync/preview", {
          method: "POST",
          body: { actions: activeKeys },
        });
        if (response?.error) {
          toast(response.error, "err");
        } else {
          setPreview(response);
        }
      } finally {
        setPreviewing(false);
        setModalLoading(null);
      }
    };

    const previewTone = preview?.recommended_mode === "needs_stash_or_commit"
      ? { border: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15", label: "habrá fallback con stash" }
      : preview?.recommended_mode === "no_changes"
        ? { border: "#94a3b844", color: "#94a3b8", background: "#94a3b815", label: "sin cambios remotos" }
        : { border: "#22d3a544", color: "#22d3a5", background: "#22d3a515", label: "listo para ejecutar" };
    const previewRunBlockedReason = preview?.recommended_mode === "no_changes"
      ? "No hay cambios remotos nuevos para aplicar en test."
      : preview?.recommended_mode === "only_local_changes"
        ? "Solo hay cambios locales en test; no hay archivos nuevos desde Git para ejecutar el pase."
        : "";
    const totalPages = Math.max(1, Math.ceil(historyTotal / HISTORY_PAGE_SIZE));
    const pageNumbers = Array.from({ length: totalPages }, (_, index) => index + 1)
      .filter((pageNumber) => Math.abs(pageNumber - historyPage) <= 1 || pageNumber === 1 || pageNumber === totalPages);

    return (
      <div className="card" style={{ marginBottom: 16 }}>
        <div className="card-head">🧪 Pase Git al ambiente de test</div>
        <div className="card-body">
          {!config ? (
            <div className="alert alert-info">Cargando configuración del ambiente de test...</div>
          ) : (
            <>
              <div className="alert alert-info" style={{ marginBottom: 14, fontSize: 11 }}>
                Flujo principal: hace <strong style={{ color: "#e2e8f0" }}>git pull</strong> en test y, si detecta conflicto por cambios locales, reintenta automáticamente con <strong style={{ color: "#e2e8f0" }}>stash preventivo</strong>.
              </div>

              <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 12 }}>
                <span className="tag" style={{ borderColor: config.enabled ? "#22d3a544" : "#64748b44", color: config.enabled ? "#22d3a5" : "#94a3b8", background: config.enabled ? "#22d3a515" : "#64748b15" }}>
                  {config.enabled ? "habilitado" : "deshabilitado"}
                </span>
                <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                  {config.label || "Test"}
                </span>
                <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                  {config.username || "root"}@{config.host || "sin-host"}:{config.port || 22}
                </span>
                <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                  {config.branch || "main"}
                </span>
                {config.target_dir && (
                  <span className="tag" style={{ borderColor: "#22d3ee44", color: "#22d3ee", background: "#22d3ee15" }}>
                    destino {config.target_dir}
                  </span>
                )}
                {config.base_url && (
                  <span className="tag" style={{ borderColor: "#a78bfa44", color: "#a78bfa", background: "#a78bfa15" }}>
                    {config.base_url}
                  </span>
                )}
              </div>

              <div style={{ fontSize: 11, color: "#64748b", marginBottom: 12 }}>
                Ruta remota: <span style={{ color: "#e2e8f0" }}>{config.repo_path || "sin configurar"}</span>
                {" · "}
                Credencial: <span style={{ color: config.password_configured ? "#22d3a5" : "#f43f5e" }}>{config.password_configured ? "lista" : "faltante"}</span>
                {" · "}
                Acciones HTTP: <span style={{ color: config.http_actions_ready ? "#22d3a5" : "#f43f5e" }}>{config.http_actions_ready ? "listas" : "falta URL base"}</span>
              </div>

              {!config.ready ? (
                <div className="alert alert-danger" style={{ marginBottom: 12 }}>
                  Falta completar la configuración del ambiente de test en `Configuración` antes de ejecutar la actualización.
                </div>
              ) : (
                <>
                  <div className="btn-row" style={{ marginBottom: 12 }}>
                    <button onClick={fetchPreview} disabled={running || previewing} className="btn btn-primary" style={{ opacity: running || previewing ? 0.7 : 1 }}>
                      {previewing ? "Obteniendo preview..." : "Actualizar ambiente test"}
                    </button>
                    <button onClick={() => setHttpActionOpen(true)} disabled={!config.http_actions_ready} className="btn btn-secondary" style={{ opacity: config.http_actions_ready ? 1 : 0.5 }}>
                      Acciones HTTP en test
                    </button>
                    <button onClick={() => setShowAdvanced((prev) => !prev)} className="btn btn-secondary">
                      {showAdvanced ? "Ocultar avanzadas" : "Opciones avanzadas"}
                    </button>
                  </div>

                  {showAdvanced && (
                    <div style={{ marginBottom: 14, border: "1px solid #1e2a3a", borderRadius: 12, padding: 12, background: "#0b1016" }}>
                      <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0", marginBottom: 8 }}>Opciones avanzadas</div>
                      <div className="alert alert-info" style={{ marginBottom: 10, fontSize: 11 }}>
                        `Git pull` siempre va incluido. Si encuentra el conflicto típico por archivos locales, aplicará stash automático sin que lo selecciones aparte.
                      </div>
                      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                        {advancedActions
                          .filter((action) => action.key !== "sync_target_dir" || config.target_dir)
                          .map((action) => (
                            <label
                              key={action.key}
                              style={{
                                display: "flex",
                                gap: 10,
                                alignItems: "flex-start",
                                padding: "10px 12px",
                                borderRadius: 10,
                                border: `1px solid ${selectedActions[action.key] ? "#22d3a544" : "#1e2a3a"}`,
                                background: selectedActions[action.key] ? "#22d3a512" : "#131820",
                                cursor: "pointer",
                              }}
                            >
                              <input
                                type="checkbox"
                                checked={!!selectedActions[action.key]}
                                onChange={() => toggleAction(action.key)}
                                style={{ marginTop: 3 }}
                              />
                              <div>
                                <div style={{ fontWeight: 700, fontSize: 12 }}>{action.label}</div>
                                <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>{action.description}</div>
                              </div>
                            </label>
                          ))}
                        <label
                          style={{
                            display: "flex",
                            gap: 10,
                            alignItems: "flex-start",
                            padding: "10px 12px",
                            borderRadius: 10,
                            border: `1px solid ${forceRemote ? "#f43f5e66" : "#1e2a3a"}`,
                            background: forceRemote ? "#f43f5e12" : "#131820",
                            cursor: "pointer",
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={forceRemote}
                            onChange={(event) => setForceRemote(event.target.checked)}
                            style={{ marginTop: 3 }}
                          />
                          <div>
                            <div style={{ fontWeight: 700, fontSize: 12, color: forceRemote ? "#f43f5e" : "#e2e8f0" }}>
                              Forzar a estado remoto (descarta commits locales)
                            </div>
                            <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>
                              Si el `git pull` falla porque el `main` del server de test divergió de `origin/main`, reintenta con `git fetch && git reset --hard origin/&lt;branch&gt;`. Pierde sin recuperación cualquier commit local que no esté en GitHub. Útil cuando el ambiente de test debe ser un espejo exacto del remoto.
                            </div>
                          </div>
                        </label>
                      </div>
                    </div>
                  )}
                </>
              )}

              {result && (
                <div style={{ marginTop: 14, background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 12 }}>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
                    <span className="tag" style={{ borderColor: result.ok ? "#22d3a544" : "#f43f5e44", color: result.ok ? "#22d3a5" : "#f43f5e", background: result.ok ? "#22d3a515" : "#f43f5e15" }}>
                      {result.status}
                    </span>
                    <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                      {result.environment?.username}@{result.environment?.host}:{result.environment?.port}
                    </span>
                  </div>
                  <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                    {(result.actions || []).map((action) => (
                      <div key={action.key} style={{ border: "1px solid #1e2a3a", borderRadius: 8, padding: 10, background: "#131820" }}>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                          <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                            <div style={{ fontWeight: 700, fontSize: 12 }}>{action.label}</div>
                            {action.fallback_used && (
                              <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                                fallback aplicado
                              </span>
                            )}
                          </div>
                          <div style={{ fontSize: 10, color: action.status === "ok" ? "#22d3a5" : "#f43f5e" }}>
                            {action.status} · código {action.code} · {action.duration_ms}ms
                          </div>
                        </div>
                        <pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, color: "#cbd5e1", fontFamily: "JetBrains Mono, monospace" }}>
                          {action.output || "(sin salida)"}
                        </pre>
                      </div>
                    ))}
                  </div>
                </div>
              )}

              <div style={{ marginTop: 14, border: "1px solid #1e2a3a", borderRadius: 12, overflow: "hidden" }}>
                <div style={{ padding: "12px 14px", background: "#0b1016", borderBottom: "1px solid #1e2a3a", display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
                  <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
                    <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>Mini historial test</div>
                    <button
                      type="button"
                      onClick={() => {
                        setHistoryFilter("all");
                        setHistoryPage(1);
                      }}
                      className="tag"
                      style={{
                        borderColor: historyFilter === "all" ? "#00e5ff" : "#64748b44",
                        color: historyFilter === "all" ? "#00e5ff" : "#94a3b8",
                        background: historyFilter === "all" ? "#00e5ff18" : "#64748b15",
                        cursor: "pointer",
                      }}
                    >
                      Todos {historyCounts.all || 0}
                    </button>
                    <button
                      type="button"
                      onClick={() => {
                        setHistoryFilter("sync");
                        setHistoryPage(1);
                      }}
                      className="tag"
                      style={{
                        borderColor: historyFilter === "sync" ? "#22d3a5" : "#64748b44",
                        color: historyFilter === "sync" ? "#22d3a5" : "#94a3b8",
                        background: historyFilter === "sync" ? "#22d3a518" : "#64748b15",
                        cursor: "pointer",
                      }}
                    >
                      Sync {historyCounts.sync || 0}
                    </button>
                    <button
                      type="button"
                      onClick={() => {
                        setHistoryFilter("http_action");
                        setHistoryPage(1);
                      }}
                      className="tag"
                      style={{
                        borderColor: historyFilter === "http_action" ? "#a78bfa" : "#64748b44",
                        color: historyFilter === "http_action" ? "#a78bfa" : "#94a3b8",
                        background: historyFilter === "http_action" ? "#a78bfa18" : "#64748b15",
                        cursor: "pointer",
                      }}
                    >
                      HTTP {historyCounts.http_action || 0}
                    </button>
                  </div>
                  <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
                    <input
                      value={historySearch}
                      onChange={(event) => {
                        setHistorySearch(event.target.value);
                        setHistoryPage(1);
                      }}
                      className="input"
                      placeholder="Buscar en historial"
                      style={{ width: 220, minWidth: 180 }}
                    />
                    <button onClick={() => {
                      setHistorySearch("");
                      setHistoryPage(1);
                    }} className="btn btn-ghost btn-sm" disabled={!historySearch.trim()}>
                      Limpiar
                    </button>
                    <button onClick={() => loadHistory()} className="btn btn-ghost btn-sm">Actualizar</button>
                  </div>
                </div>
                {!history.length ? (
                  <div style={{ padding: "16px", fontSize: 12, color: "#64748b" }}>
                    {historySearch.trim() ? "No hay resultados para esa búsqueda." : "Sin ejecuciones registradas todavía."}
                  </div>
                ) : (
                  <div style={{ display: "flex", flexDirection: "column" }}>
                    {history.map((entry) => (
                      <div key={entry.id} style={{ padding: "12px 14px", borderTop: "1px solid #1e2a3a15", background: "#131820" }}>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                          <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
                            <span className="tag" style={{ borderColor: entry.status === "ok" ? "#22d3a544" : "#f43f5e44", color: entry.status === "ok" ? "#22d3a5" : "#f43f5e", background: entry.status === "ok" ? "#22d3a515" : "#f43f5e15" }}>
                              {entry.type === "http_action" ? "acción http" : "sync"}
                            </span>
                            {entry.type === "sync" && entry.fallback_used && (
                              <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                                fallback aplicado
                              </span>
                            )}
                            <span style={{ fontSize: 12, color: "#e2e8f0", fontWeight: 700 }}>{entry.summary}</span>
                          </div>
                          <div style={{ fontSize: 10, color: "#64748b" }}>{fmtDate(entry.created_at)}</div>
                        </div>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
                          <div style={{ fontSize: 11, color: "#94a3b8" }}>
                            Ejecutado por <strong style={{ color: "#e2e8f0" }}>{entry.actor_username}</strong>
                            {entry.details?.base_url ? ` · ${entry.details.base_url}` : ""}
                          </div>
                          <button onClick={() => openHistoryDetail(entry.id)} className="btn btn-ghost btn-sm">
                            Detalle
                          </button>
                        </div>
                      </div>
                    ))}
                  </div>
                )}
                {historyTotal > 0 && (
                  <div style={{ padding: "12px 14px", borderTop: "1px solid #1e2a3a", background: "#0b1016", display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
                    <div style={{ fontSize: 11, color: "#64748b" }}>
                      Página {Math.min(historyPage, totalPages)} de {totalPages} · {historyTotal} registro{historyTotal !== 1 ? "s" : ""}
                    </div>
                    <div className="btn-row" style={{ justifyContent: "flex-end", flexWrap: "wrap" }}>
                      <button
                        type="button"
                        onClick={() => setHistoryPage((prev) => Math.max(prev - 1, 1))}
                        className="btn btn-ghost btn-sm"
                        disabled={historyPage <= 1}
                      >
                        Anterior
                      </button>
                      {pageNumbers.map((pageNumber, index) => {
                        const prevPage = pageNumbers[index - 1];
                        const showGap = index > 0 && pageNumber - prevPage > 1;
                        return (
                          <div key={pageNumber} style={{ display: "contents" }}>
                            {showGap && (
                              <span style={{ alignSelf: "center", fontSize: 11, color: "#64748b", padding: "0 2px" }}>…</span>
                            )}
                            <button
                              type="button"
                              onClick={() => setHistoryPage(pageNumber)}
                              className={historyPage === pageNumber ? "btn btn-primary btn-sm" : "btn btn-ghost btn-sm"}
                            >
                              {pageNumber}
                            </button>
                          </div>
                        );
                      })}
                      <button
                        type="button"
                        onClick={() => setHistoryPage((prev) => Math.min(prev + 1, totalPages))}
                        className="btn btn-ghost btn-sm"
                        disabled={historyPage >= totalPages}
                      >
                        Siguiente
                      </button>
                    </div>
                  </div>
                )}
              </div>

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

              {preview && (
                <div className="modal-overlay">
                  <div className="modal modal-lg">
                    <div className="modal-head">
                      <span>📋 Confirmar pase a test</span>
                      <button onClick={() => setPreview(null)} className="btn btn-ghost btn-sm">
                        ✕
                      </button>
                    </div>
                    <div className="modal-body">
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 12 }}>
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          {preview.current_branch || config?.branch || "main"}
                        </span>
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          actual {preview.current_commit || "—"}
                        </span>
                        <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                          remoto {preview.remote_commit || "—"}
                        </span>
                        <span className="tag" style={{ borderColor: previewTone.border, color: previewTone.color, background: previewTone.background }}>
                          {previewTone.label}
                        </span>
                      </div>

                      <div className="alert alert-info" style={{ marginBottom: 14, fontSize: 11 }}>
                        {preview.summary_message}
                      </div>

                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
                        <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                          entran {preview.incoming_count}
                        </span>
                        <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                          locales {preview.local_change_count}
                        </span>
                        <span className="tag" style={{ borderColor: preview.conflict_count ? "#f43f5e44" : "#64748b44", color: preview.conflict_count ? "#f43f5e" : "#94a3b8", background: preview.conflict_count ? "#f43f5e15" : "#64748b15" }}>
                          conflicto {preview.conflict_count}
                        </span>
                      </div>

                      <div style={{ marginBottom: 14 }}>
                        <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#00e5ff", marginBottom: 8 }}>
                          Acciones seleccionadas
                        </div>
                        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                          {(preview.actions || []).map((action) => (
                            <div key={action.key} style={{ background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: "10px 12px" }}>
                              <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>{action.label}</div>
                              <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>{action.description}</div>
                            </div>
                          ))}
                        </div>
                      </div>

                      <div style={{ marginBottom: 14 }}>
                        <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#22d3a5", marginBottom: 8 }}>
                          Archivos remotos que entrarían
                        </div>
                        {preview.incoming_count === 0 ? (
                          <div className="alert alert-success">✔ No hay archivos nuevos por traer desde Git.</div>
                        ) : (
                          <FilterableFileList
                            files={preview.incoming_files || []}
                            placeholder="Buscar archivo entrante…"
                            maxHeight={220}
                            renderItem={(file, index) => (
                              <div key={`${file.path}-${index}`} style={{ padding: "6px 12px", fontSize: 11, display: "flex", gap: 8, borderBottom: "1px solid #1e2a3a15" }}>
                                <span style={{ color: file.status.startsWith("A") ? "#22d3a5" : "#fbbf24", flexShrink: 0, fontSize: 10 }}>
                                  [{file.status}]
                                </span>
                                <span style={{ wordBreak: "break-all" }}>{file.path}</span>
                              </div>
                            )}
                          />
                        )}
                      </div>

                      <div style={{ marginBottom: preview.conflict_count ? 14 : 0 }}>
                        <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#fbbf24", marginBottom: 8 }}>
                          Cambios locales detectados en test
                        </div>
                        {preview.local_change_count === 0 ? (
                          <div className="alert alert-success">✔ No se detectaron cambios locales en el repo remoto.</div>
                        ) : (
                          <FilterableFileList
                            files={preview.local_changes || []}
                            placeholder="Buscar cambio local…"
                            maxHeight={180}
                            renderItem={(file, index) => (
                              <div key={`${file.path}-${index}`} style={{ padding: "6px 12px", fontSize: 11, display: "flex", gap: 8, borderBottom: "1px solid #1e2a3a15" }}>
                                <span style={{ color: "#fbbf24", flexShrink: 0, fontSize: 10 }}>
                                  [{file.raw.slice(0, 2)}]
                                </span>
                                <span style={{ wordBreak: "break-all" }}>{file.path}</span>
                              </div>
                            )}
                          />
                        )}
                      </div>
                    </div>
                    {previewRunBlockedReason && (
                      <div className="alert alert-warn" style={{ margin: "0 16px 12px", fontSize: 11 }}>
                        {previewRunBlockedReason}
                      </div>
                    )}
                    <div className="modal-foot btn-row">
                      <button onClick={() => setPreview(null)} className="btn btn-secondary">
                        Cancelar
                      </button>
                      <button
                        onClick={async () => {
                          setPreview(null);
                          await run();
                        }}
                        disabled={running || !!previewRunBlockedReason}
                        className="btn"
                        style={{
                          flex: 1,
                          background: "#00e5ff",
                          color: "#000",
                          borderRadius: 8,
                          border: "none",
                          cursor: running || previewRunBlockedReason ? "not-allowed" : "pointer",
                          fontWeight: 700,
                          opacity: running || previewRunBlockedReason ? 0.55 : 1,
                        }}
                      >
                        Confirmar y ejecutar
                      </button>
                    </div>
                  </div>
                </div>
              )}

              {httpActionOpen && (
                <TestHttpActionModal
                  presets={presets}
                  baseUrl={config?.base_url || ""}
                  toast={toast}
                  onClose={() => setHttpActionOpen(false)}
                  onExecuted={() => loadHistory()}
                />
              )}

              {historyDetail && (
                <div className="modal-overlay">
                  <div className="modal modal-lg">
                    <div className="modal-head">
                      <span>📜 Detalle historial test</span>
                      <button onClick={() => setHistoryDetail(null)} className="btn btn-ghost btn-sm">✕</button>
                    </div>
                    <div className="modal-body">
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 12 }}>
                        <span className="tag" style={{ borderColor: historyDetail.status === "ok" ? "#22d3a544" : "#f43f5e44", color: historyDetail.status === "ok" ? "#22d3a5" : "#f43f5e", background: historyDetail.status === "ok" ? "#22d3a515" : "#f43f5e15" }}>
                          {historyDetail.type === "http_action" ? "acción http" : "sync"}
                        </span>
                        {historyDetail.type === "sync" && historyDetail.fallback_used && (
                          <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                            fallback aplicado
                          </span>
                        )}
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                          {fmtDate(historyDetail.created_at)}
                        </span>
                      </div>
                      <div style={{ fontSize: 11, color: "#94a3b8", marginBottom: 14 }}>
                        Ejecutado por <strong style={{ color: "#e2e8f0" }}>{historyDetail.actor_username}</strong>
                      </div>

                      {historyDetail.type === "sync" ? (
                        <>
                          <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
                            <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                              actual {historyDetail.details?.current_commit || "—"}
                            </span>
                            <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                              remoto {historyDetail.details?.remote_commit || "—"}
                            </span>
                            <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                              archivos {Array.isArray(historyDetail.details?.incoming_files) ? historyDetail.details.incoming_files.length : 0}
                            </span>
                          </div>

                          <div style={{ marginBottom: 14 }}>
                            <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#22d3a5", marginBottom: 8 }}>
                              Archivos que se pasaron
                            </div>
                            {!historyDetail.details?.incoming_files?.length ? (
                              <div className="alert alert-info">No quedaron archivos registrados para esta ejecución.</div>
                            ) : (
                              <FilterableFileList
                                files={historyDetail.details.incoming_files}
                                placeholder="Buscar archivo en este pase…"
                                maxHeight={220}
                                renderItem={(file, index) => (
                                  <div key={`${file.path}-${index}`} style={{ padding: "6px 12px", fontSize: 11, display: "flex", gap: 8, borderBottom: "1px solid #1e2a3a15" }}>
                                    <span style={{ color: file.status?.startsWith("A") ? "#22d3a5" : "#fbbf24", flexShrink: 0, fontSize: 10 }}>
                                      [{file.status}]
                                    </span>
                                    <span style={{ wordBreak: "break-all" }}>{file.path}</span>
                                  </div>
                                )}
                              />
                            )}
                          </div>

                          <div>
                            <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#00e5ff", marginBottom: 8 }}>
                              Pasos ejecutados
                            </div>
                            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                              {(historyDetail.details?.actions || []).map((action, index) => (
                                <div key={`${action.key || index}`} style={{ border: "1px solid #1e2a3a", borderRadius: 8, padding: 10, background: "#131820" }}>
                                  <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                                    <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
                                      <div style={{ fontWeight: 700, fontSize: 12 }}>{action.key || action.label || `Paso ${index + 1}`}</div>
                                      {action.fallback_used && (
                                        <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                                          fallback aplicado
                                        </span>
                                      )}
                                    </div>
                                    <div style={{ fontSize: 10, color: action.status === "ok" ? "#22d3a5" : "#f43f5e" }}>
                                      {action.status} · código {action.code ?? "—"} · {action.duration_ms || 0}ms
                                    </div>
                                  </div>
                                </div>
                              ))}
                            </div>
                          </div>
                        </>
                      ) : (
                        <>
                          <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
                            <span className="tag" style={{ borderColor: "#a78bfa44", color: "#a78bfa", background: "#a78bfa15" }}>
                              {historyDetail.details?.base_url || "sin url"}
                            </span>
                            <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                              {historyDetail.details?.ok_count || 0}/{historyDetail.details?.total_actions || 0} ok
                            </span>
                          </div>
                          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                            {(historyDetail.details?.actions || []).map((action, index) => (
                              <div key={`${action.route_path || index}`} style={{ border: "1px solid #1e2a3a", borderRadius: 8, padding: 10, background: "#131820" }}>
                                <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                                  <div style={{ fontWeight: 700, fontSize: 12 }}>{action.label || `${action.method} ${action.route_path}`}</div>
                                  <div style={{ fontSize: 10, color: action.status === "ok" ? "#22d3a5" : "#f43f5e" }}>
                                    {action.status} · http {action.http_status || 0} · {action.duration_ms || 0}ms
                                  </div>
                                </div>
                                <div style={{ fontSize: 10, color: "#64748b" }}>
                                  {(action.method || "GET")} {action.route_path || ""}
                                </div>
                              </div>
                            ))}
                          </div>

                          {historyReplay.progress && (
                            <div className="alert alert-info" style={{ marginTop: 14 }}>
                              Re-ejecutando acción {historyReplay.progress.current} de {historyReplay.progress.total}
                              {historyReplay.progress.label ? ` · ${historyReplay.progress.label}` : ""}
                            </div>
                          )}

                          {historyReplay.result && (
                            <div style={{ marginTop: 14, background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 12 }}>
                              <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
                                <span className="tag" style={{ borderColor: historyReplay.result.ok ? "#22d3a544" : "#f43f5e44", color: historyReplay.result.ok ? "#22d3a5" : "#f43f5e", background: historyReplay.result.ok ? "#22d3a515" : "#f43f5e15" }}>
                                  {historyReplay.result.status}
                                </span>
                                <span className="tag" style={{ borderColor: "#a78bfa44", color: "#a78bfa", background: "#a78bfa15" }}>
                                  {historyReplay.result.base_url}
                                </span>
                                <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                                  {historyReplay.result.ok_count}/{historyReplay.result.total_actions} ok
                                </span>
                              </div>
                              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                                {(historyReplay.result.actions || []).map((action, index) => (
                                  <div key={`${action.route_path || index}-replay`} style={{ border: "1px solid #1e2a3a", borderRadius: 8, padding: 10, background: "#131820" }}>
                                    <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
                                      <div style={{ fontWeight: 700, fontSize: 12 }}>{action.label || `${action.method} ${action.route_path}`}</div>
                                      <div style={{ fontSize: 10, color: action.status === "ok" ? "#22d3a5" : "#f43f5e" }}>
                                        {action.status} · http {action.http_status || 0} · {action.duration_ms || 0}ms
                                      </div>
                                    </div>
                                    <div style={{ fontSize: 10, color: "#64748b", marginBottom: 6 }}>{action.target_url}</div>
                                    <pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, color: "#cbd5e1", fontFamily: "JetBrains Mono, monospace" }}>
                                      {action.response_excerpt || "(sin salida)"}
                                    </pre>
                                  </div>
                                ))}
                              </div>
                            </div>
                          )}
                        </>
                      )}
                    </div>
                    <div className="modal-foot btn-row">
                      <button onClick={exportHistoryDetailTxt} className="btn btn-secondary">Exportar TXT</button>
                      <button onClick={exportHistoryDetailPdf} className="btn btn-secondary">Exportar PDF</button>
                      {historyDetail.type === "http_action" && (
                        <button onClick={replayHistoryHttpActions} className="btn btn-primary" disabled={historyReplay.running}>
                          {historyReplay.running ? "Re-ejecutando..." : "Re-ejecutar en test"}
                        </button>
                      )}
                      <button onClick={() => setHistoryDetail(null)} className="btn btn-secondary">Cerrar</button>
                    </div>
                  </div>
                </div>
              )}
            </>
          )}
        </div>
      </div>
    );
  }

  function DeployView({ servers, toast, reloadHistory }) {
    const [branch, setBranch] = useState("main");
    const [dryRun, setDryRun] = useState(false);
    const [selected, setSelected] = useState({});
    const [passwords, setPasswords] = useState({});
    const [logs, setLogs] = useState([{ text: "Sistema listo.", type: "muted" }]);
    const [running, setRunning] = useState(false);
    const [progress, setProgress] = useState(0);
    const [preview, setPreview] = useState(null);
    const [previewPrecheck, setPreviewPrecheck] = useState(null);
    const [previewing, setPreviewing] = useState(false);
    const [confirmOpen, setConfirm] = useState(false);
    const [changeValidation, setChangeValidation] = useState(null);
    const [validatingChanges, setValidatingChanges] = useState(false);
    const [withPostAction, setWithPostAction] = useState(false);
    const [actionPresets, setActionPresets] = useState([]);
    const [actionPresetId, setActionPresetId] = useState("");
    const [postActionMode, setPostActionMode] = useState("single");
    const [postActionQueuedPresetIds, setPostActionQueuedPresetIds] = useState([]);
    const [postActionForm, setPostActionForm] = useState({
      action_label: "",
      route_path: "/intranet/",
      method: "GET",
      request_body: "",
      timeout: 15,
    });
    const [validateAfterDeploy, setValidateAfterDeploy] = useState(false);
    const [autoRollback, setAutoRollback] = useState(false);
    const [validationDelay, setValidationDelay] = useState(8);
    const [requiresApproval, setRequiresApproval] = useState(false);
    const [pendingApprovals, setPendingApprovals] = useState([]);
    const [lastDeployId, setLastDeployId] = useState(null);
    const [postAction, setPostAction] = useState(null);
    const [activeTab, setActiveTab] = useState(() => {
      try {
        return window.localStorage.getItem("dc:tabs:deploy") || "deploy";
      } catch (_error) {
        return "deploy";
      }
    });
    const logRef = useRef(null);
    const allServersSelected = servers.length > 0 && servers.every((server) => selected[server.id]);
    const selectedPostActionPresets = actionPresets.filter((preset) => postActionQueuedPresetIds.includes(String(preset.id)));
    const deployTabs = [
      { key: "deploy", label: "Deploy principal", icon: "🚀" },
      { key: "test_sync", label: "Pase a test", icon: "🧪" },
      ...(pendingApprovals.length > 0 ? [{ key: "approvals", label: `Aprobaciones (${pendingApprovals.length})`, icon: "✋" }] : []),
    ];

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

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

    const currentProtectionPreset = (() => {
      if (!validateAfterDeploy && !autoRollback) return "fast";
      if (validateAfterDeploy && autoRollback) return "safe";
      return "custom";
    })();

    const applyProtectionPreset = (preset) => {
      if (preset === "fast") {
        setValidateAfterDeploy(false);
        setAutoRollback(false);
        setValidationDelay(8);
        return;
      }
      if (preset === "safe") {
        setValidateAfterDeploy(true);
        setAutoRollback(true);
        setValidationDelay(12);
      }
    };

    useEffect(() => {
      if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
    }, [logs]);

    useEffect(() => {
      apiFetch("/http-actions/presets").then((response) => {
        setActionPresets(Array.isArray(response) ? response : []);
      });
      apiFetch("/deploy/approvals").then((response) => {
        setPendingApprovals(Array.isArray(response) ? response.filter((a) => a.status === "pending") : []);
      });
    }, []);

    useEffect(() => {
      if (activeTab === "approvals" && pendingApprovals.length === 0) {
        setActiveTab("deploy");
      }
    }, [activeTab, pendingApprovals.length]);

    const applyTemplate = (tmpl) => {
      if (tmpl.branch) setBranch(tmpl.branch);
      if (typeof tmpl.dry_run !== "undefined") setDryRun(!!tmpl.dry_run);
      if (typeof tmpl.validate_after_deploy !== "undefined") setValidateAfterDeploy(!!tmpl.validate_after_deploy);
      if (typeof tmpl.auto_rollback !== "undefined") setAutoRollback(!!tmpl.auto_rollback);
      if (typeof tmpl.validation_delay_ms !== "undefined") setValidationDelay(Math.round(tmpl.validation_delay_ms / 1000));
      if (typeof tmpl.requires_approval !== "undefined") setRequiresApproval(!!tmpl.requires_approval);
      if (tmpl.server_ids_json) {
        try {
          const ids = JSON.parse(tmpl.server_ids_json);
          const next = {};
          ids.forEach((id) => { next[id] = true; });
          setSelected(next);
        } catch (_) {}
      }
      toast(`Template "${tmpl.name}" cargado`, "ok");
    };

    const approveRequest = async (approvalId) => {
      const res = await apiFetch(`/deploy/approvals/${approvalId}/approve`, { method: "POST" });
      if (res?.error) { toast(res.error, "err"); return; }
      toast("Deploy aprobado", "ok");
      const updated = await apiFetch("/deploy/approvals");
      setPendingApprovals(Array.isArray(updated) ? updated.filter((a) => a.status === "pending") : []);
    };

    const rejectRequest = async (approvalId) => {
      const reason = window.prompt("Motivo del rechazo (opcional):");
      if (reason === null) return;
      const res = await apiFetch(`/deploy/approvals/${approvalId}/reject`, { method: "POST", body: { reason } });
      if (res?.error) { toast(res.error, "err"); return; }
      toast("Deploy rechazado", "ok");
      const updated = await apiFetch("/deploy/approvals");
      setPendingApprovals(Array.isArray(updated) ? updated.filter((a) => a.status === "pending") : []);
    };

    const addLog = (text, type = "muted") =>
      setLogs((prev) => [
        ...prev,
        { text: `[${new Date().toLocaleTimeString("es", { hour12: false })}] ${text}`, type },
      ]);

    const applyActionPreset = (nextPresetId) => {
      setActionPresetId(nextPresetId);
      const preset = actionPresets.find((item) => String(item.id) === String(nextPresetId));
      if (!preset) return;
      setPostActionForm({
        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)),
      });
    };

    const togglePostActionPreset = (nextPresetId) => {
      const normalizedId = String(nextPresetId);
      setPostActionQueuedPresetIds((prev) => (
        prev.includes(normalizedId)
          ? prev.filter((item) => item !== normalizedId)
          : [...prev, normalizedId]
      ));
    };

    const selectAllServers = () => {
      const next = {};
      servers.forEach((server) => {
        next[server.id] = true;
      });
      setSelected(next);
    };

    const clearSelectedServers = () => {
      setSelected({});
    };

    const validatePostActionConfig = () => {
      if (!withPostAction) return true;
      if (postActionMode === "multiple") {
        if (!selectedPostActionPresets.length) {
          toast("Selecciona al menos un preset para la cola post-deploy", "err");
          return false;
        }
        return true;
      }
      if (!String(postActionForm.route_path || "").trim()) {
        toast("Ingresa la ruta de la acción post-deploy", "err");
        return false;
      }
      if (!String(postActionForm.route_path || "").trim().startsWith("/")) {
        toast("La ruta post-deploy debe empezar con /", "err");
        return false;
      }
      if (postActionForm.method !== "GET" && String(postActionForm.request_body || "").trim()) {
        try {
          JSON.parse(postActionForm.request_body);
        } catch (_error) {
          toast("El body JSON de la acción post-deploy no es válido", "err");
          return false;
        }
      }
      return true;
    };

    const selectedServers = servers.filter((server) => selected[server.id]);

    const exportPrecheckPdf = () => {
      if (!previewPrecheck) return;
      const precheck = previewPrecheck;
      const title = "Precheck post-deploy";
      const subtitle = `${branch || "sin branch"} · ${precheck.ready_count || 0} listos · ${precheck.skipped_count || 0} omitidos`;

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

      y = pdfAddMetrics({ doc, pageW, startY: y, metrics: [
        { label: "Branch",    value: branch || "—" },
        { label: "Servidores",value: precheck.server_count || 0 },
        { label: "Listos",    value: precheck.ready_count || 0, color: PDF_C.ok },
        { label: "Omitidos",  value: precheck.skipped_count || 0, color: PDF_C.skip },
        { label: "Total",     value: precheck.total || 0 },
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Resumen por servidor" });
      doc.autoTable({
        ...pdfAutoTableDefaults(doc),
        startY: y,
        columns: [
          {header:"Servidor",dataKey:"server_label"},
          {header:"Listos",  dataKey:"ready_count"},
          {header:"Omitidos",dataKey:"skipped_count"},
          {header:"Total",   dataKey:"total"},
        ],
        body: (precheck.servers || []).map(s => ({
          server_label:  s.server_label || "—",
          ready_count:   s.ready_count || 0,
          skipped_count: s.skipped_count || 0,
          total:         s.total || 0,
        })),
        didParseCell(data) {
          if (data.section === "body") {
            if (data.column.dataKey === "ready_count") data.cell.styles.textColor = PDF_C.ok;
            if (data.column.dataKey === "skipped_count" && Number(data.cell.raw) > 0) data.cell.styles.textColor = PDF_C.skip;
          }
        },
        columnStyles:{0:{cellWidth:80},1:{cellWidth:20},2:{cellWidth:20},3:{cellWidth:"auto"}},
      });
      y = doc.lastAutoTable.finalY + 5;

      const inactive = precheck.inactive_rows || [];
      if (inactive.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Dominios omitibles por precheck (${inactive.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          headStyles: { fillColor: PDF_C.skip, textColor: [255,255,255], fontStyle: "bold", fontSize: 7 },
          columns: [
            {header:"Servidor",  dataKey:"server_label"},
            {header:"Usuario",   dataKey:"virt_user"},
            {header:"Dominio",   dataKey:"domain"},
            {header:"Estado",    dataKey:"status"},
            {header:"Detalle",   dataKey:"detail"},
          ],
          body: inactive.map(r => ({
            server_label: r.server_label || "—",
            virt_user:    r.virt_user || "—",
            domain:       r.domain || "—",
            status:       r.status || "—",
            detail:       `dns=${r.dns_ok?"ok":"fail"} · https=${r.https_ok?"ok":"fail"} · http=${r.http_status||0} · ${r.response_ms??0}ms`,
          })),
          columnStyles:{0:{cellWidth:30},1:{cellWidth:22},2:{cellWidth:50},3:{cellWidth:18},4:{cellWidth:"auto"}},
        });
      }

      pdfAddFooters({ doc, title });
      doc.save(`precheck-post-deploy-${branch || "sin-branch"}.pdf`);
    };

    const loadPostActionPrecheck = async () => {
      if (!withPostAction || dryRun) {
        setPreviewPrecheck(null);
        return null;
      }

      const serverSummaries = [];

      for (const server of selectedServers) {
        const users = await apiFetch(`/servers/${server.id}/users`);
        const eligibleUsers = (Array.isArray(users) ? users : [])
          .filter((user) => !user.excluded && user.include_deploy && user.domain);

        if (!eligibleUsers.length) continue;

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

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

        serverSummaries.push(response);
      }

      const summary = {
        checked_at: new Date().toISOString(),
        server_count: serverSummaries.length,
        total: serverSummaries.reduce((acc, item) => acc + (item.total || 0), 0),
        ready_count: serverSummaries.reduce((acc, item) => acc + (item.ready_count || 0), 0),
        skipped_count: serverSummaries.reduce((acc, item) => acc + (item.skipped_count || 0), 0),
        servers: serverSummaries,
        inactive_rows: serverSummaries.flatMap((item) =>
          (item.rows || [])
            .filter((row) => row.action_status === "skipped")
            .map((row) => ({ ...row, server_label: item.server_label }))
        ),
      };

      setPreviewPrecheck(summary);
      return summary;
    };

    const fetchPreview = async () => {
      if (!validatePostActionConfig()) return;
      if (!selectedServers.length) {
        toast("Selecciona un servidor", "err");
        return;
      }

      setPreviewing(true);
      setPreview(null);
      setPreviewPrecheck(null);

      try {
        // Un preview por servidor hace SSH; en serie tardaba N×latencia. En paralelo
        // tarda lo del más lento. Promise.all preserva el orden de selectedServers.
        const results = await Promise.all(selectedServers.map(async (server) => {
          const response = await apiFetch(`/servers/${server.id}/preview`, {
            method: "POST",
            body: { branch, password: passwords[server.id] || "" },
          });
          return { server, data: response };
        }));
        setPreview(results);
        if (withPostAction && !dryRun) {
          await loadPostActionPrecheck();
        }
      } catch {
        toast("Error al obtener preview", "err");
      }

      setPreviewing(false);
    };

    // Pre-chequeo READ-ONLY de los cambios del branch (php -l + migraciones + revisión
    // IA) antes de desplegar. No toca clientes. Devuelve el veredicto.
    const validateChanges = async () => {
      if (!selectedServers.length) {
        toast("Seleccioná al menos un servidor para validar.", "err");
        return null;
      }
      setValidatingChanges(true);
      try {
        const response = await apiFetch("/deploy/validate", {
          method: "POST",
          body: { branch, server_ids: selectedServers.map((s) => s.id) },
        });
        setChangeValidation(response);
        if (response?.error) toast(response.error, "err");
        else if (response?.passed) toast(`✓ Validación OK — ${response.changedCount} cambio(s), sin hallazgos`);
        else toast(`🔴 Validación: ${(response.blocking || []).length} hallazgo(s) bloqueante(s)`, "err");
        return response;
      } catch (e) {
        const errObj = { ok: false, error: e.message };
        setChangeValidation(errObj);
        toast(`Error al validar: ${e.message}`, "err");
        return errObj;
      } finally {
        setValidatingChanges(false);
      }
    };

    const renderValidationPanel = () => {
      const v = changeValidation;
      if (validatingChanges && !v) {
        return <div className="alert alert-info" style={{ marginTop: 10, fontSize: 12 }}>⟳ Validando cambios con Claude + checks (puede tardar unos segundos)...</div>;
      }
      if (!v) return null;
      if (v.error || v.ok === false) {
        return <div className="alert alert-warn" style={{ marginTop: 10, fontSize: 12 }}>⚠ No se pudo validar: {v.error || "error desconocido"}</div>;
      }
      const blocked = !v.passed;
      return (
        <div className="alert" style={{ marginTop: 10, fontSize: 12, background: blocked ? "#f43f5e15" : "#22d3a515", border: `1px solid ${blocked ? "#f43f5e44" : "#22d3a544"}`, borderRadius: 8, padding: 12 }}>
          <div style={{ fontWeight: 700, color: blocked ? "#f43f5e" : "#22d3a5" }}>
            {blocked ? "🔴 Cambios bloqueados" : "🟢 Cambios validados — sin hallazgos bloqueantes"}
          </div>
          <div style={{ fontSize: 10, color: "#94a3b8", marginTop: 3 }}>
            branch {v.branch} · {v.changedCount} archivo(s) · {(v.baseline || "").slice(0, 8)} → {(v.target || "").slice(0, 8)}{v.server_label ? ` · ref: ${v.server_label}` : ""}
          </div>
          {v.note && <div style={{ fontSize: 11, color: "#94a3b8", marginTop: 6 }}>{v.note}</div>}
          {v.aiVerdict && (
            <div style={{ fontSize: 11, color: v.aiVerdict.block ? "#f43f5e" : "#22d3a5", marginTop: 6 }}>
              🤖 Revisión IA: {v.aiVerdict.block ? `bloqueó (severidad ${v.aiVerdict.severity})` : `OK (severidad ${v.aiVerdict.severity})`}
            </div>
          )}
          {(v.blocking || []).map((b, i) => (
            <div key={`b${i}`} style={{ fontSize: 11, color: "#fca5a5", marginTop: 5 }}>• [{b.check}] {b.file ? `${b.file}: ` : ""}{b.message}</div>
          ))}
          {(v.warnings || []).map((w, i) => (
            <div key={`w${i}`} style={{ fontSize: 11, color: "#fcd34d", marginTop: 5 }}>• [{w.check}] {w.message}</div>
          ))}
        </div>
      );
    };

    // Auto-validación al abrir el modal de confirmación (deploy real, no dry-run):
    // muestra el veredicto antes de ejecutar.
    useEffect(() => {
      if (confirmOpen && !dryRun) {
        setChangeValidation(null);
        validateChanges();
      }
    }, [confirmOpen]);

    const startDeploy = async () => {
      if (!validatePostActionConfig()) return;
      setConfirm(false);
      setPreview(null);
      setRunning(true);
      setProgress(0);
      addLog("═══════════════════════", "header");
      addLog(`Deploy${dryRun ? " [DRY RUN]" : ""} — branch: ${branch}`, "header");

      const passwordMap = {};
      selectedServers.forEach((server) => {
        passwordMap[server.id] = passwords[server.id] || "";
      });

      try {
        const response = await apiFetch("/deploy", {
          method: "POST",
          body: {
            branch,
            dry_run: dryRun,
            server_ids: selectedServers.map((server) => server.id),
            password_map: passwordMap,
            validate_after_deploy: validateAfterDeploy,
            auto_rollback: autoRollback,
            validation_delay_ms: Number(validationDelay || 8) * 1000,
            requires_approval: requiresApproval,
          },
        });

        if (response.error) {
          addLog(`Error: ${response.error}`, "error");
          setRunning(false);
          return;
        }

        if (response.status === "pending_approval") {
          addLog(`Deploy #${response.deploy_id} esperando aprobación (aprobación #${response.approval_id})`, "warn");
          addLog("Un administrador debe aprobar el deploy antes de que se ejecute.", "muted");
          setRunning(false);
          const updated = await apiFetch("/deploy/approvals");
          setPendingApprovals(Array.isArray(updated) ? updated.filter((a) => a.status === "pending") : []);
          return;
        }

        setLastDeployId(response.deploy_id);
        addLog(`Deploy #${response.deploy_id} iniciado...`, "info");

        const seen = new Set();
        let lastValidationStatus = "";
        let lastRollbackStatus = "";
        let done = false;

        while (!done) {
          await sleep(1500);
          const status = await apiFetch(`/deploy/${response.deploy_id}/status`);
          (status.results || []).forEach((result) => {
            if (!seen.has(result.id)) {
              seen.add(result.id);
              const icon = { ok: "✔", error: "✗", skipped: "⚙", excluded: "⊘", no_git: "⚠" }[result.status] || "·";
              const type = { ok: "ok", error: "error", skipped: "warn", excluded: "muted", no_git: "warn" }[result.status] || "muted";
              addLog(
                `  ${icon} [${result.server_label || "srv"}] ${result.virt_user} — ${result.status}${result.git_commit ? ` (${result.git_commit})` : ""}`,
                type
              );
            }
          });
          if (status.deploy?.validation_status && status.deploy.validation_status !== lastValidationStatus) {
            lastValidationStatus = status.deploy.validation_status;
            addLog(`  ◉ Validación post-deploy: ${status.deploy.validation_status}`, status.deploy.validation_status === "failed" ? "error" : status.deploy.validation_status === "success" ? "ok" : "warn");
          }
          if (status.deploy?.rollback_status && status.deploy.rollback_status !== lastRollbackStatus) {
            lastRollbackStatus = status.deploy.rollback_status;
            addLog(`  ↩ Rollback: ${status.deploy.rollback_status}`, status.deploy.rollback_status === "success" ? "ok" : status.deploy.rollback_status === "partial" ? "warn" : "error");
          }
          setProgress(Math.min(90, seen.size * 8));
          if (status.deploy?.status !== "running") done = true;
        }

        const finalStatus = await apiFetch(`/deploy/${response.deploy_id}/status`);
        const okCount = finalStatus.results?.filter((result) => result.status === "ok").length || 0;
        const errorCount = finalStatus.results?.filter((result) => result.status === "error").length || 0;
        setProgress(100);

        const finalType =
          finalStatus.deploy?.status === "success"
            ? "ok"
            : finalStatus.deploy?.status === "partial"
              ? "warn"
              : "error";

        addLog(
          `✔ Completado | Estado: ${finalStatus.deploy?.status || "desconocido"} | Validación: ${finalStatus.deploy?.validation_status || "—"} | Rollback: ${finalStatus.deploy?.rollback_status || "—"} | OK: ${okCount} | Errores: ${errorCount}`,
          finalType
        );
        addLog("═══════════════════════", "header");
        reloadHistory();

        if (withPostAction && !dryRun && finalStatus.deploy?.status !== "failed") {
          const okUsers = new Set(
            (finalStatus.results || [])
              .filter((result) => result.status === "ok" && !String(result.virt_user || "").startsWith("_"))
              .map((result) => `${result.server_id}:${result.virt_user}`)
          );

          const actionUsers = [];
          for (const server of selectedServers) {
            const users = await apiFetch(`/servers/${server.id}/users`);
            (Array.isArray(users) ? users : [])
              .filter((user) => !user.excluded && user.include_deploy && user.domain && okUsers.has(`${server.id}:${user.username}`))
              .forEach((user) => actionUsers.push({ serverId: server.id, serverLabel: server.label, username: user.username, domain: user.domain }));
          }

          if (actionUsers.length) {
            addLog(`⚡ Acción post-deploy lista para ${actionUsers.length} usuario(s)`, "info");
            setPostAction({
              users: actionUsers,
              presets: actionPresets,
              initialForm: postActionMode === "multiple"
                ? {
                    mode: "multiple",
                    queued_preset_ids: postActionQueuedPresetIds,
                  }
                : {
                    mode: "single",
                    preset_id: actionPresetId || "",
                    action_label: postActionForm.action_label,
                    route_path: postActionForm.route_path,
                    method: postActionForm.method,
                    request_body: postActionForm.method === "GET" ? "" : postActionForm.request_body,
                    timeout: Number(postActionForm.timeout || 15),
                  },
            });
          } else {
            addLog("⚠ Sin usuarios exitosos con dominio para ejecutar la acción post-deploy", "warn");
          }
        }
      } catch (error) {
        addLog(`Error: ${error.message}`, "error");
      }

      setRunning(false);
    };

    const logColors = {
      info: "#00e5ff",
      ok: "#22d3a5",
      warn: "#fbbf24",
      error: "#f43f5e",
      muted: "#64748b",
      header: "#e2e8f0",
    };
    const previewHasDeployableChanges = Array.isArray(preview) &&
      preview.some((item) => !item.data?.error && Number(item.data?.total_changes || 0) > 0);
    const previewDeployBlockedReason = preview && !previewHasDeployableChanges
      ? "El preview no detectó cambios nuevos para deployar en los servidores seleccionados."
      : "";

    return (
      <div style={{ animation: "fadeIn .3s ease" }}>
        {postAction && (
          <HttpActionModal
            users={postAction.users}
            presets={postAction.presets}
            initialForm={postAction.initialForm}
            deployId={lastDeployId}
            onClose={() => setPostAction(null)}
          />
        )}

        {preview && (
          <div className="modal-overlay">
            <div className="modal modal-lg">
              <div className="modal-head">
                <span>📋 Preview — {branch}</span>
                <button onClick={() => {
                  setPreview(null);
                  setPreviewPrecheck(null);
                }} className="btn btn-ghost btn-sm">
                  ✕
                </button>
              </div>
              <div className="modal-body">
                {preview.map((item, index) => (
                  <div key={index} style={{ marginBottom: 16 }}>
                    <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, marginBottom: 8, color: "#00e5ff" }}>
                      {item.server.label}
                  {item.data.error && (
                        <span style={{ color: "#f43f5e", fontSize: 12, marginLeft: 8 }}>— {item.data.error}</span>
                      )}
                    </div>
                    {!item.data.error && (
                      <>
                        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#64748b", background: "#64748b15" }}>
                            actual: {item.data.current_commit || "—"}
                          </span>
                          <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                            nuevo: {item.data.new_commit || "—"}
                          </span>
                          <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                            {item.data.total_changes} cambios
                          </span>
                          <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                            {item.data.total_users} usuarios
                          </span>
                        </div>
                        {item.data.total_changes === 0 ? (
                          <div className="alert alert-success">✔ Sin cambios pendientes</div>
                        ) : (
                          <div style={{ background: "#080b10", borderRadius: 8, maxHeight: 180, overflowY: "auto" }}>
                            {item.data.changed_files?.map((file, fileIndex) => (
                              <div
                                key={fileIndex}
                                style={{
                                  padding: "4px 12px",
                                  fontSize: 11,
                                  display: "flex",
                                  gap: 8,
                                  borderBottom: "1px solid #1e2a3a15",
                                }}
                              >
                                <span
                                  style={{
                                    color: item.data.new_files?.includes(file) ? "#22d3a5" : "#fbbf24",
                                    flexShrink: 0,
                                    fontSize: 10,
                                  }}
                                >
                                  {item.data.new_files?.includes(file) ? "[nuevo]" : "[modif]"}
                                </span>
                                <span style={{ wordBreak: "break-all" }}>{file}</span>
                              </div>
                            ))}
                          </div>
                        )}
                      </>
                    )}
                  </div>
                ))}

                {withPostAction && !dryRun && (
                  <div style={{ marginTop: 18, paddingTop: 14, borderTop: "1px solid #1e2a3a33" }}>
                    <div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 10 }}>
                      <div>
                        <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 14, color: "#22d3ee" }}>
                          🔎 Precheck previo de la acción post-deploy
                        </div>
                        <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                          Candidatos actuales con dominio e inclusión de deploy. Los omitidos se saltarán automáticamente si siguen caídos.
                        </div>
                      </div>
                      {previewPrecheck && (
                        <button onClick={exportPrecheckPdf} className="btn btn-ghost btn-sm">
                          🧾 Exportar PDF
                        </button>
                      )}
                    </div>

                    {!previewPrecheck ? (
                      <div className="alert alert-info">Calculando precheck previo...</div>
                    ) : (
                      <>
                        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
                          <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                            listos {previewPrecheck.ready_count}
                          </span>
                          <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                            omitidos {previewPrecheck.skipped_count}
                          </span>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                            revisados {previewPrecheck.total}
                          </span>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                            {fmtDate(previewPrecheck.checked_at)}
                          </span>
                        </div>

                        {previewPrecheck.inactive_rows.length > 0 ? (
                          <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 220, overflowY: "auto", paddingRight: 4 }}>
                            {previewPrecheck.inactive_rows.map((row, index) => (
                              <div key={`${row.server_label}-${row.virt_user}-${index}`} style={{ background: "#080b10", 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} · {row.status} · dns {row.dns_ok ? "ok" : "fail"} · https {row.https_ok ? "ok" : "fail"} · http {row.http_status || 0}
                                    </div>
                                  </div>
                                  <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                                    se omitirá
                                  </span>
                                </div>
                              </div>
                            ))}
                          </div>
                        ) : (
                          <div className="alert alert-success">✔ No se detectaron dominios omitibles para la acción post-deploy.</div>
                        )}
                      </>
                    )}
                  </div>
                )}
              </div>
              <div className="modal-foot btn-row">
                {withPostAction && !dryRun && previewPrecheck && (
                  <button onClick={exportPrecheckPdf} className="btn btn-secondary">
                    🧾 PDF previo
                  </button>
                )}
                <button onClick={() => {
                  setPreview(null);
                  setPreviewPrecheck(null);
                }} className="btn btn-secondary">
                  Cancelar
                </button>
                <button
                  onClick={() => {
                    setPreview(null);
                    setConfirm(true);
                  }}
                  disabled={!!previewDeployBlockedReason}
                  className="btn"
                  style={{
                    flex: 1,
                    background: dryRun ? "#fbbf24" : "#f43f5e",
                    color: "#000",
                    borderRadius: 8,
                    border: "none",
                    cursor: previewDeployBlockedReason ? "not-allowed" : "pointer",
                    fontWeight: 700,
                    fontSize: 13,
                    opacity: previewDeployBlockedReason ? 0.55 : 1,
                  }}
                >
                  {dryRun ? "⚙ Simular" : "🚀 Deploy"}
                </button>
              </div>
              {previewDeployBlockedReason && (
                <div className="alert alert-warn" style={{ margin: "0 16px 16px", fontSize: 11 }}>
                  {previewDeployBlockedReason}
                </div>
              )}
            </div>
          </div>
        )}

        {confirmOpen && (
          <div className="modal-overlay">
            <div className="modal">
              <div className="modal-head">{dryRun ? "⚙ Dry Run" : "🚀 Confirmar Deploy"}</div>
              <div className="modal-body">
                <p style={{ fontSize: 12, color: "#64748b", lineHeight: 1.7 }}>
                  {dryRun
                  ? `Simulación en ${selectedServers.length} servidor(es).`
                    : `Deploy en ${selectedServers.length} servidor(es) desde "${branch}". Validación: ${validateAfterDeploy ? "sí" : "no"}${autoRollback ? " · rollback automático: sí" : ""}${withPostAction ? " · acción HTTP post-deploy: sí" : ""}.`}
                </p>
                {withPostAction && !dryRun && previewPrecheck && (
                  <div className="alert alert-info" style={{ marginTop: 12, fontSize: 11 }}>
                    Acción post-deploy candidata: <strong style={{ color: "#e2e8f0" }}>{previewPrecheck.ready_count}</strong> dominios listos y
                    <strong style={{ color: "#e2e8f0" }}> {previewPrecheck.skipped_count}</strong> que se omitirán automáticamente si siguen caídos al ejecutar la acción.
                  </div>
                )}
                {!dryRun && renderValidationPanel()}
              </div>
              <div className="modal-foot btn-row">
                <button onClick={() => setConfirm(false)} className="btn btn-secondary">
                  Cancelar
                </button>
                <button
                  onClick={startDeploy}
                  disabled={!dryRun && validatingChanges}
                  className="btn"
                  style={{
                    flex: 1,
                    background: dryRun ? "#fbbf24" : (!dryRun && changeValidation && changeValidation.passed === false ? "#fbbf24" : "#f43f5e"),
                    color: "#000",
                    borderRadius: 8,
                    border: "none",
                    cursor: !dryRun && validatingChanges ? "not-allowed" : "pointer",
                    fontWeight: 700,
                    opacity: !dryRun && validatingChanges ? 0.6 : 1,
                  }}
                >
                  {dryRun
                    ? "Simular"
                    : validatingChanges
                      ? "⟳ Validando..."
                      : changeValidation && changeValidation.passed === false
                        ? "⚠ Ejecutar igual (bloqueado)"
                        : "Ejecutar"}
                </button>
              </div>
            </div>
          </div>
        )}

        <DeployTabs tabs={deployTabs} activeTab={activeTab} onChange={setActiveTab} />

        {activeTab === "approvals" && pendingApprovals.length > 0 && (
          <div className="card" style={{ marginBottom: 16 }}>
            <div className="card-head" style={{ color: "#a78bfa" }}>
              ✋ Aprobaciones Pendientes ({pendingApprovals.length})
            </div>
            <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {pendingApprovals.map((approval) => (
                <div
                  key={approval.id}
                  style={{
                    background: "#131820",
                    border: "1px solid #a78bfa33",
                    borderRadius: 10,
                    padding: "12px 14px",
                    display: "flex",
                    justifyContent: "space-between",
                    alignItems: "center",
                    gap: 10,
                    flexWrap: "wrap",
                  }}
                >
                  <div>
                    <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>
                      Deploy #{approval.deploy_id}
                      {approval.branch && <span style={{ color: "#64748b", marginLeft: 8, fontWeight: 400 }}>· {approval.branch}</span>}
                    </div>
                    <div style={{ fontSize: 11, color: "#64748b", marginTop: 3 }}>
                      Solicitado por <strong style={{ color: "#94a3b8" }}>{approval.requested_by}</strong>
                      {approval.expires_at && <span> · expira {fmtDate(approval.expires_at)}</span>}
                    </div>
                  </div>
                  <div className="btn-row">
                    <button
                      onClick={() => approveRequest(approval.id)}
                      className="btn btn-ghost btn-sm"
                      style={{ color: "#22d3a5", borderColor: "#22d3a544" }}
                    >
                      ✔ Aprobar
                    </button>
                    <button
                      onClick={() => rejectRequest(approval.id)}
                      className="btn btn-ghost btn-sm"
                      style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}
                    >
                      ✗ Rechazar
                    </button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {activeTab === "test_sync" && (
          <TestSyncCard toast={toast} presets={actionPresets} />
        )}

        {activeTab === "deploy" && (
          <>
            {pendingApprovals.length > 0 && (
              <div className="card" style={{ marginBottom: 16 }}>
                <div className="card-head" style={{ color: "#a78bfa" }}>
                  ✋ Aprobaciones pendientes ({pendingApprovals.length})
                </div>
                <div className="card-body" style={{ fontSize: 12, color: "#94a3b8" }}>
                  Tienes solicitudes pendientes. Si quieres revisarlas en detalle o aprobarlas, usa la pestaña <strong style={{ color: "#e2e8f0" }}>Aprobaciones</strong>.
                </div>
              </div>
            )}

            <div className="card">
              <div className="card-head">
                <span>⚙ Configuración</span>
                <TemplatesPicker onApply={applyTemplate} />
              </div>
              <div className="card-body">
            <div className="grid-2" style={{ marginBottom: 14 }}>
              <div className="field">
                <label>Branch</label>
                <select value={branch} onChange={(event) => setBranch(event.target.value)} className="input select">
                  <option value="main">main</option>
                  <option value="production">production</option>
                  <option value="release">release</option>
                </select>
              </div>
              <div className="field">
                <label>Modo</label>
                <div className="btn-row">
                  {[["false", "🚀 Real"], ["true", "⚙ Dry Run"]].map(([value, label]) => (
                    <button
                      key={value}
                      onClick={() => setDryRun(value === "true")}
                      className="btn btn-secondary"
                      style={{
                        color: String(dryRun) === value ? "#00e5ff" : "#64748b",
                        borderColor: String(dryRun) === value ? "#00e5ff" : "#1e2a3a",
                        fontSize: 12,
                      }}
                    >
                      {label}
                    </button>
                  ))}
                </div>
              </div>
            </div>

            <div className="field">
              <label>Servidores destino</label>
              {servers.length === 0 && <div style={{ fontSize: 12, color: "#64748b" }}>Sin servidores configurados.</div>}
              {servers.length > 0 && (
                <div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 10 }}>
                  <div style={{ fontSize: 11, color: "#64748b" }}>
                    {selectedServers.length} de {servers.length} servidor{servers.length !== 1 ? "es" : ""} seleccionado{selectedServers.length !== 1 ? "s" : ""}
                  </div>
                  <div className="btn-row" style={{ flex: "0 0 auto" }}>
                    <button
                      type="button"
                      onClick={selectAllServers}
                      className="btn btn-ghost btn-sm"
                      style={{ color: allServersSelected ? "#22d3a5" : "#94a3b8", borderColor: allServersSelected ? "#22d3a544" : "#1e2a3a" }}
                    >
                      Seleccionar todos
                    </button>
                    <button type="button" onClick={clearSelectedServers} className="btn btn-ghost btn-sm">
                      Limpiar
                    </button>
                  </div>
                </div>
              )}
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {servers.map((server) => (
                  <div
                    key={server.id}
                    onClick={() => setSelected((prev) => ({ ...prev, [server.id]: !prev[server.id] }))}
                    style={{
                      background: "#131820",
                      border: `1px solid ${selected[server.id] ? "#00e5ff" : "#1e2a3a"}`,
                      borderRadius: 10,
                      padding: "12px 14px",
                      cursor: "pointer",
                      display: "flex",
                      alignItems: "center",
                      gap: 10,
                      flexWrap: "wrap",
                    }}
                  >
                    <div
                      style={{
                        width: 18,
                        height: 18,
                        borderRadius: 5,
                        border: `2px solid ${selected[server.id] ? "#00e5ff" : "#1e2a3a"}`,
                        background: selected[server.id] ? "#00e5ff" : "transparent",
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center",
                        fontSize: 11,
                        color: "#000",
                        flexShrink: 0,
                      }}
                    >
                      {selected[server.id] ? "✓" : ""}
                    </div>
                    <div style={{ flex: 1, minWidth: 100 }}>
                      <div style={{ fontWeight: 700, fontSize: 13 }}>{server.label}</div>
                      <div style={{ fontSize: 11, color: "#64748b" }}>
                        {server.username}@{server.host}:{server.port}
                      </div>
                    </div>
                    {selected[server.id] && (
                      <div onClick={(event) => event.stopPropagation()} style={{ minWidth: 140, flex: "0 0 auto" }}>
                        <input
                          type="password"
                          placeholder="Vacío = usar guardada"
                          value={passwords[server.id] || ""}
                          onChange={(event) => setPasswords((prev) => ({ ...prev, [server.id]: event.target.value }))}
                          className="input input-sm"
                        />
                      </div>
                    )}
                  </div>
                ))}
              </div>
            </div>

            <div
              style={{
                background: "#131820",
                border: "1px solid #22d3ee30",
                borderRadius: 10,
                padding: "12px 14px",
                marginBottom: 14,
              }}
            >
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: withPostAction ? 12 : 0 }}>
                <div>
                  <div style={{ fontSize: 13, fontWeight: 700 }}>⚡ Acción HTTP después del deploy</div>
                  <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>Solo usuarios exitosos con dominio configurado</div>
                </div>
                <Toggle value={withPostAction} color="#22d3ee" onChange={setWithPostAction} />
              </div>
              {withPostAction && (
                <>
                  {actionPresets.length > 1 && (
                    <div className="field" style={{ marginBottom: 12 }}>
                      <label>Modo de ejecución post-deploy</label>
                      <div className="btn-row">
                        <button
                          type="button"
                          onClick={() => setPostActionMode("single")}
                          className={postActionMode === "single" ? "btn btn-primary" : "btn btn-secondary"}
                        >
                          Una acción
                        </button>
                        <button
                          type="button"
                          onClick={() => setPostActionMode("multiple")}
                          className={postActionMode === "multiple" ? "btn btn-primary" : "btn btn-secondary"}
                        >
                          Varias acciones
                        </button>
                      </div>
                    </div>
                  )}

                  {postActionMode === "multiple" ? (
                    <>
                      {actionPresets.length === 0 ? (
                        <div className="alert alert-warn" style={{ fontSize: 11 }}>
                          Primero crea presets en la pestaña de acciones para poder lanzar varias acciones post-deploy.
                        </div>
                      ) : (
                        <div className="field" style={{ marginBottom: 12 }}>
                          <div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 8 }}>
                            <label style={{ marginBottom: 0 }}>Cola de presets</label>
                            <div className="btn-row" style={{ flex: "0 0 auto" }}>
                              <button
                                type="button"
                                onClick={() => setPostActionQueuedPresetIds(actionPresets.map((preset) => String(preset.id)))}
                                className="btn btn-ghost btn-sm"
                              >
                                Todos
                              </button>
                              <button
                                type="button"
                                onClick={() => setPostActionQueuedPresetIds([])}
                                className="btn btn-ghost btn-sm"
                              >
                                Ninguno
                              </button>
                            </div>
                          </div>
                          <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 260, overflowY: "auto", paddingRight: 4 }}>
                            {actionPresets.map((preset) => {
                              const isSelected = postActionQueuedPresetIds.includes(String(preset.id));
                              return (
                                <label
                                  key={preset.id}
                                  style={{
                                    display: "flex",
                                    alignItems: "flex-start",
                                    gap: 10,
                                    padding: "10px 12px",
                                    borderRadius: 10,
                                    border: `1px solid ${isSelected ? "#22d3a544" : "#1e2a3a"}`,
                                    background: isSelected ? "#22d3a512" : "#131820",
                                    cursor: "pointer",
                                  }}
                                >
                                  <input
                                    type="checkbox"
                                    checked={isSelected}
                                    onChange={() => togglePostActionPreset(preset.id)}
                                    style={{ marginTop: 3 }}
                                  />
                                  <div style={{ minWidth: 0 }}>
                                    <div style={{ fontWeight: 700, fontSize: 12 }}>{preset.name}</div>
                                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>
                                      {(preset.method || "GET")} {preset.route_path} · timeout {Math.max(1, Math.round((preset.timeout_ms || 15000) / 1000))}s
                                    </div>
                                  </div>
                                </label>
                              );
                            })}
                          </div>
                        </div>
                      )}

                      <div className="alert alert-info" style={{ fontSize: 11 }}>
                        Tras el deploy, se abrirá una cola de <strong style={{ color: "#e2e8f0" }}>{selectedPostActionPresets.length || 0}</strong> acciones sobre los usuarios exitosos con dominio configurado.
                      </div>
                    </>
                  ) : (
                    <>
                      {actionPresets.length > 0 && (
                        <div className="field" style={{ marginBottom: 12 }}>
                          <label>Preset guardado</label>
                          <select
                            value={actionPresetId}
                            onChange={(event) => applyActionPreset(event.target.value)}
                            className="input select"
                          >
                            <option value="">Sin preset</option>
                            {actionPresets.map((preset) => (
                              <option key={preset.id} value={preset.id}>
                                {preset.name}
                              </option>
                            ))}
                          </select>
                        </div>
                      )}

                      <div className="grid-3" style={{ marginBottom: 10 }}>
                        <div>
                          <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Etiqueta</label>
                          <input
                            value={postActionForm.action_label}
                            onChange={(event) => setPostActionForm((prev) => ({ ...prev, action_label: event.target.value }))}
                            className="input input-sm"
                            placeholder="Reset cache"
                          />
                        </div>
                        <div>
                          <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Método</label>
                          <select
                            value={postActionForm.method}
                            onChange={(event) => setPostActionForm((prev) => ({ ...prev, method: event.target.value }))}
                            className="input input-sm"
                          >
                            {["GET", "POST", "PUT", "PATCH", "DELETE"].map((method) => (
                              <option key={method} value={method}>{method}</option>
                            ))}
                          </select>
                        </div>
                        <div>
                          <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Timeout (s)</label>
                          <input
                            type="number"
                            min="1"
                            max="120"
                            value={postActionForm.timeout}
                            onChange={(event) => setPostActionForm((prev) => ({ ...prev, timeout: event.target.value }))}
                            className="input input-sm"
                            placeholder="15"
                          />
                        </div>
                      </div>

                      <div style={{ marginBottom: 10 }}>
                        <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Ruta relativa</label>
                        <input
                          value={postActionForm.route_path}
                          onChange={(event) => setPostActionForm((prev) => ({ ...prev, route_path: event.target.value }))}
                          className="input input-sm"
                          placeholder="/intranet/Api_extention/control_total/reset_cache"
                        />
                      </div>

                      <div style={{ marginBottom: 10 }}>
                        <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Body JSON opcional</label>
                        <textarea
                          value={postActionForm.request_body}
                          onChange={(event) => setPostActionForm((prev) => ({ ...prev, request_body: event.target.value }))}
                          className="input input-sm"
                          rows={4}
                          disabled={postActionForm.method === "GET"}
                          style={{ resize: "vertical" }}
                          placeholder='{"reset":true}'
                        />
                      </div>

                      <div className="alert alert-info" style={{ fontSize: 11 }}>
                        Tras el deploy, se abrirá una sola acción HTTP para los usuarios exitosos. La URL final siempre será <strong style={{ color: "#e2e8f0" }}>https://dominio{postActionForm.route_path || "/ruta"}</strong>.
                      </div>
                    </>
                  )}
                </>
              )}
            </div>

            <div
              style={{
                background: "#131820",
                border: "1px solid #22d3ee30",
                borderRadius: 10,
                padding: "12px 14px",
                marginBottom: 14,
              }}
            >
              <div style={{ marginBottom: 12 }}>
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap", marginBottom: 8 }}>
                  <div style={{ fontSize: 10, color: "#64748b", textTransform: "uppercase", letterSpacing: "1px" }}>
                    Preset de protección
                  </div>
                  <span
                    className="tag"
                    style={{
                      borderColor: currentProtectionPreset === "safe" ? "#22d3a544" : currentProtectionPreset === "fast" ? "#64748b44" : "#fbbf2444",
                      color: currentProtectionPreset === "safe" ? "#22d3a5" : currentProtectionPreset === "fast" ? "#94a3b8" : "#fbbf24",
                      background: currentProtectionPreset === "safe" ? "#22d3a515" : currentProtectionPreset === "fast" ? "#64748b15" : "#fbbf2415",
                    }}
                  >
                    {currentProtectionPreset === "safe" ? "Seguro" : currentProtectionPreset === "fast" ? "Rápido" : "Personalizado"}
                  </span>
                </div>
                <div className="btn-row">
                  <button
                    onClick={() => applyProtectionPreset("fast")}
                    className="btn btn-secondary"
                    style={{
                      color: currentProtectionPreset === "fast" ? "#00e5ff" : "#94a3b8",
                      borderColor: currentProtectionPreset === "fast" ? "#00e5ff" : "#1e2a3a",
                      background: currentProtectionPreset === "fast" ? "#00e5ff12" : "transparent",
                      fontSize: 12,
                    }}
                  >
                    Rápido
                  </button>
                  <button
                    onClick={() => applyProtectionPreset("safe")}
                    className="btn btn-secondary"
                    style={{
                      color: currentProtectionPreset === "safe" ? "#22d3a5" : "#94a3b8",
                      borderColor: currentProtectionPreset === "safe" ? "#22d3a5" : "#1e2a3a",
                      background: currentProtectionPreset === "safe" ? "#22d3a512" : "transparent",
                      fontSize: 12,
                    }}
                  >
                    Seguro
                  </button>
                </div>
                <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 8, fontSize: 11, color: "#64748b" }}>
                  <span>Rápido: deploy sin validación ni rollback.</span>
                  <span>Seguro: validación activada con rollback automático.</span>
                </div>
              </div>

              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: validateAfterDeploy ? 12 : 0 }}>
                <div>
                  <div style={{ fontSize: 13, fontWeight: 700 }}>🛡 Validación post-deploy</div>
                  <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                    Revisa dominios desplegados antes y después. Solo dispara rollback si el sitio empeora de verdad.
                  </div>
                </div>
                <Toggle value={validateAfterDeploy} color="#22d3ee" onChange={(value) => {
                  setValidateAfterDeploy(value);
                  if (!value) setAutoRollback(false);
                }} />
              </div>

              <div className="grid-2" style={{ marginBottom: 12, opacity: validateAfterDeploy ? 1 : 0.65 }}>
                <div>
                  <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 4 }}>Espera antes de validar (seg)</label>
                  <input
                    type="number"
                    min="2"
                    max="60"
                    value={validationDelay}
                    onChange={(event) => setValidationDelay(event.target.value)}
                    className="input input-sm"
                    placeholder="8"
                    disabled={!validateAfterDeploy}
                  />
                </div>
                <div style={{ display: "flex", alignItems: "flex-end" }}>
                  <div style={{ width: "100%" }}>
                    <label style={{ display: "block", fontSize: 10, color: "#64748b", marginBottom: 6 }}>Rollback automático</label>
                    <div
                      style={{
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "space-between",
                        background: validateAfterDeploy ? "#0f141b" : "#10151d",
                        border: `1px solid ${validateAfterDeploy ? "#1e2a3a" : "#1e2a3a88"}`,
                        borderRadius: 10,
                        padding: "10px 12px",
                      }}
                    >
                      <div style={{ fontSize: 11, color: validateAfterDeploy ? "#cbd5e1" : "#64748b", maxWidth: "85%" }}>
                        Restaurar snapshot si el sitio queda peor tras el deploy
                      </div>
                      <Toggle value={autoRollback} color="#a78bfa" onChange={setAutoRollback} disabled={!validateAfterDeploy} />
                    </div>
                  </div>
                </div>
              </div>

              {validateAfterDeploy ? (
                <div className="alert alert-info" style={{ fontSize: 11 }}>
                  El rollback automático solo actúa en usuarios con dominio y snapshot previo correcto. Si un sitio ya estaba mal antes, no se considera regresión.
                </div>
              ) : (
                <div className="alert alert-warn" style={{ fontSize: 11 }}>
                  Activa la validación post-deploy para habilitar la espera de comprobación y el rollback automático.
                </div>
              )}
            </div>

            <div
              style={{
                background: "#131820",
                border: "1px solid #a78bfa30",
                borderRadius: 10,
                padding: "12px 14px",
                marginBottom: 14,
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
              }}
            >
              <div>
                <div style={{ fontSize: 13, fontWeight: 700 }}>✋ Requiere aprobación</div>
                <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                  El deploy quedará pendiente hasta que un administrador lo apruebe
                </div>
              </div>
              <Toggle value={requiresApproval} color="#a78bfa" onChange={setRequiresApproval} />
            </div>

            <button onClick={validateChanges} disabled={validatingChanges || running || !selectedServers.length} className="btn btn-secondary" style={{ width: "100%", marginBottom: 8, opacity: validatingChanges || !selectedServers.length ? 0.6 : 1 }}>
              {validatingChanges ? "⟳ Validando cambios..." : "🔍 Validar cambios antes de desplegar (Claude + checks)"}
            </button>
            {renderValidationPanel()}

            <button onClick={fetchPreview} disabled={running || previewing} className="btn btn-primary" style={{ opacity: running || previewing ? 0.5 : 1 }}>
              {running
                ? "⟳ Ejecutando..."
                : previewing
                  ? "⟳ Obteniendo preview..."
                  : dryRun
                    ? "⚙ Ver cambios y simular"
                    : withPostAction
                      ? "🚀 Ver cambios, precheck y deployar"
                      : "🚀 Ver cambios y deployar"}
            </button>
              </div>
            </div>

            <div className="card">
              <div className="card-head">
                <span>▶ Log en vivo</span>
                <button onClick={() => setLogs([{ text: "— limpiado —", type: "muted" }])} className="btn btn-ghost btn-sm">
                  limpiar
                </button>
              </div>
              <div className="card-body" style={{ padding: 12 }}>
                {running && (
                  <div className="progress-wrap">
                    <div className="progress-bar" style={{ width: `${progress}%` }} />
                  </div>
                )}
                <div className="log-box" ref={logRef}>
                  {logs.map((log, index) => (
                    <div key={index} style={{ color: logColors[log.type] || "#64748b" }}>
                      {log.text}
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </>
        )}
      </div>
    );
  }

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