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

  function buildDeployReportHtml(detail) {
    const deploy = detail?.deploy || {};
    const results = Array.isArray(detail?.results) ? detail.results : [];
    const rollbacks = Array.isArray(detail?.rollbacks) ? detail.rollbacks : [];
    const changeSummary = deploy.change_summary || {};
    const changedFiles = Array.isArray(changeSummary.files) ? changeSummary.files : [];
    const changedServers = Array.isArray(changeSummary.servers) ? changeSummary.servers : [];
    const validationServers = Array.isArray(deploy.validation_summary?.servers) ? deploy.validation_summary.servers : [];
    const degradedItems = validationServers.flatMap((server) =>
      (server.degraded_users || []).map((item) => ({
        serverLabel: server.server_label || server.server_id || "Servidor",
        domain: item.domain || item.virt_user || "sin dominio",
        beforeStatus: item.before_status || "—",
        afterStatus: item.after_status || "—",
      }))
    );
    const okCount = results.filter((result) => result.status === "ok").length;
    const errorCount = results.filter((result) => result.status === "error").length;

    return `
      <section class="report-section">
        <div class="metrics">
          <div class="metric">
            <div class="metric-label">Deploy</div>
            <div class="metric-value">#${escapeHtml(deploy.id || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Estado</div>
            <div class="metric-value status-${escapeHtml(deploy.status || "pending")}">${escapeHtml(deploy.status || "—")}</div>
          </div>
          <div class="metric">
            <div class="metric-label">OK</div>
            <div class="metric-value status-ok">${okCount}</div>
          </div>
          <div class="metric">
            <div class="metric-label">Errores</div>
            <div class="metric-value status-error">${errorCount}</div>
          </div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen</h2>
        <div class="meta-list">
          <div class="meta-item">Branch: <strong>${escapeHtml(deploy.branch || "—")}</strong></div>
          <div class="meta-item">Iniciado: <strong>${escapeHtml(fmtFullDate(deploy.started_at))}</strong></div>
          <div class="meta-item">Disparado por: <strong>${escapeHtml(deploy.triggered_user || "—")}</strong></div>
          <div class="meta-item">Dry run: <strong>${deploy.dry_run ? "Sí" : "No"}</strong></div>
          <div class="meta-item">Validación: <strong>${escapeHtml(deploy.validation_status || "—")}</strong></div>
          <div class="meta-item">Rollback: <strong>${escapeHtml(deploy.rollback_status || "—")}</strong></div>
          <div class="meta-item">Commit previo: <strong class="mono">${escapeHtml(deploy.previous_commit || "—")}</strong></div>
          <div class="meta-item">Commit desplegado: <strong class="mono">${escapeHtml(deploy.deployed_commit || "—")}</strong></div>
          <div class="meta-item">Archivos cambiados: <strong>${escapeHtml(changeSummary.file_count || 0)}</strong></div>
        </div>
      </section>

      <section class="report-section">
        <h2>Cambios desplegados</h2>
        ${changedServers.length ? `
          <div class="callout">
            Servidores con resumen Git: <strong>${escapeHtml(changedServers.length)}</strong>
            · Archivos detectados: <strong>${escapeHtml(changeSummary.file_count || 0)}</strong>
          </div>
          <table style="margin-top:12px">
            <thead>
              <tr>
                <th>Servidor</th>
                <th>Antes</th>
                <th>Después</th>
                <th>Archivos</th>
              </tr>
            </thead>
            <tbody>
              ${changedServers.map((server) => `
                <tr>
                  <td>${escapeHtml(server.server_label || "—")}</td>
                  <td class="mono">${escapeHtml(server.previous_commit_short || server.previous_commit || "—")}</td>
                  <td class="mono">${escapeHtml(server.deployed_commit_short || server.deployed_commit || "—")}</td>
                  <td>${escapeHtml(server.file_count || 0)}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted">No hay rango de commits registrado para este deploy.</div>'}
        ${changedFiles.length ? `
          <table style="margin-top:12px">
            <thead>
              <tr>
                <th>Tipo</th>
                <th>Archivo</th>
              </tr>
            </thead>
            <tbody>
              ${changedFiles.map((file) => `
                <tr>
                  <td class="mono">${escapeHtml(file.status || "M")}</td>
                  <td class="mono">${escapeHtml(file.path || "—")}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted" style="margin-top:10px">No se detectaron archivos modificados o el repo ya estaba al día.</div>'}
      </section>

      <section class="report-section">
        <h2>Resultados por usuario</h2>
        <table>
          <thead>
            <tr>
              <th>Usuario</th>
              <th>Servidor</th>
              <th>Estado</th>
              <th>Commit</th>
              <th>Salida</th>
            </tr>
          </thead>
          <tbody>
            ${results.map((result) => `
              <tr>
                <td>${escapeHtml(result.virt_user || "—")}</td>
                <td>${escapeHtml(result.server_label || "—")}</td>
                <td class="status-${escapeHtml(result.status || "pending")}">${escapeHtml(result.status || "—")}</td>
                <td class="mono">${escapeHtml(result.git_commit || "—")}</td>
                <td class="mono">${escapeHtml(result.output || "—").slice(0, 320)}</td>
              </tr>
            `).join("") || `<tr><td colspan="5" class="muted">Sin resultados registrados.</td></tr>`}
          </tbody>
        </table>
      </section>

      <section class="report-section">
        <h2>Validación post-deploy</h2>
        ${deploy.validation_summary ? `
          <div class="callout">
            Servidores revisados: <strong>${escapeHtml(deploy.validation_summary.checked_servers || (deploy.validation_summary.server_id ? 1 : 0))}</strong>
            · Degradados: <strong>${escapeHtml(deploy.validation_summary.degraded_count || 0)}</strong>
          </div>
          ${degradedItems.length ? `
            <table style="margin-top:12px">
              <thead>
                <tr>
                  <th>Servidor</th>
                  <th>Dominio</th>
                  <th>Antes</th>
                  <th>Después</th>
                </tr>
              </thead>
              <tbody>
                ${degradedItems.map((item) => `
                  <tr>
                    <td>${escapeHtml(item.serverLabel)}</td>
                    <td>${escapeHtml(item.domain)}</td>
                    <td>${escapeHtml(item.beforeStatus)}</td>
                    <td class="status-warn">${escapeHtml(item.afterStatus)}</td>
                  </tr>
                `).join("")}
              </tbody>
            </table>
          ` : '<div class="muted" style="margin-top:10px">No se detectaron degradaciones registradas.</div>'}
        ` : '<div class="muted">Sin validación post-deploy registrada.</div>'}
      </section>

      <section class="report-section">
        <h2>Snapshots y rollback</h2>
        ${rollbacks.length ? `
          <table>
            <thead>
              <tr>
                <th>Usuario</th>
                <th>Dominio</th>
                <th>Servidor</th>
                <th>Snapshot</th>
                <th>Restore</th>
              </tr>
            </thead>
            <tbody>
              ${rollbacks.map((row) => `
                <tr>
                  <td>${escapeHtml(row.virt_user || "—")}</td>
                  <td>${escapeHtml(row.domain || "sin dominio")}</td>
                  <td>${escapeHtml(row.server_label || "—")}</td>
                  <td>${escapeHtml(row.snapshot_status || "—")}</td>
                  <td>${escapeHtml(row.restore_status || "—")}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        ` : '<div class="muted">No hubo snapshots o rollback registrados para este deploy.</div>'}
      </section>
    `;
  }

  function buildHttpActionsReportHtml(actions) {
    const batches = Array.isArray(actions?.batches) ? actions.batches : [];
    const results = Array.isArray(actions?.results) ? actions.results : [];
    if (!batches.length) return "";

    const totalOk = batches.reduce((acc, b) => acc + (b.ok_count || 0), 0);
    const totalErr = batches.reduce((acc, b) => acc + (b.error_count || 0), 0);
    const totalSkip = batches.reduce((acc, b) => acc + (b.skipped_count || 0), 0);

    return `
      <section class="report-section">
        <h2>Acciones HTTP post-deploy</h2>
        <div class="metrics">
          <div class="metric"><div class="metric-label">Lotes</div><div class="metric-value">${escapeHtml(batches.length)}</div></div>
          <div class="metric"><div class="metric-label">OK</div><div class="metric-value status-ok">${escapeHtml(totalOk)}</div></div>
          <div class="metric"><div class="metric-label">Errores</div><div class="metric-value status-error">${escapeHtml(totalErr)}</div></div>
          <div class="metric"><div class="metric-label">Omitidos</div><div class="metric-value">${escapeHtml(totalSkip)}</div></div>
        </div>
      </section>

      <section class="report-section">
        <h2>Resumen por lote</h2>
        <table>
          <thead>
            <tr><th>Servidor</th><th>Acción</th><th>Estado</th><th>OK</th><th>Errores</th><th>Omitidos</th><th>Inicio</th></tr>
          </thead>
          <tbody>
            ${batches.map((b) => `
              <tr>
                <td>${escapeHtml(b.server_label || "—")}</td>
                <td class="mono">${escapeHtml(b.action_label || `${b.method} ${b.route_path}`)}</td>
                <td class="status-${escapeHtml(b.status || "pending")}">${escapeHtml(b.status || "—")}</td>
                <td class="status-ok">${escapeHtml(b.ok_count || 0)}</td>
                <td class="status-error">${escapeHtml(b.error_count || 0)}</td>
                <td>${escapeHtml(b.skipped_count || 0)}</td>
                <td>${escapeHtml(fmtFullDate(b.started_at))}</td>
              </tr>
            `).join("")}
          </tbody>
        </table>
      </section>

      <section class="report-section">
        <h2>Resultados por usuario</h2>
        <table>
          <thead>
            <tr><th>Usuario</th><th>Servidor</th><th>Dominio</th><th>Estado</th><th>HTTP</th><th>ms</th></tr>
          </thead>
          <tbody>
            ${results.map((r) => `
              <tr>
                <td>${escapeHtml(r.virt_user || "—")}</td>
                <td>${escapeHtml(r.server_label || "—")}</td>
                <td>${escapeHtml(r.domain || "—")}</td>
                <td class="status-${escapeHtml(r.status || "pending")}">${escapeHtml(r.status || "—")}</td>
                <td>${escapeHtml(r.http_status || "—")}</td>
                <td>${escapeHtml(r.duration_ms != null ? r.duration_ms : "—")}</td>
              </tr>
            `).join("") || `<tr><td colspan="6" class="muted">Sin resultados registrados.</td></tr>`}
          </tbody>
        </table>
      </section>
    `;
  }

  const HIST_PAGE_SIZE = 25;

  function shortenCommit(commit) {
    return String(commit || "").trim().slice(0, 12);
  }

  function slugifyFilePart(value, fallback = "sin-branch") {
    const normalized = String(value || "")
      .toLowerCase()
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "");
    return normalized || fallback;
  }

  function pdfAddParagraph(doc, text, startY, options = {}) {
    const marginX = options.marginX || 14;
    const maxWidth = options.maxWidth || 180;
    const fontSize = options.fontSize || 9;
    const color = options.color || [71, 85, 105];
    doc.setFontSize(fontSize);
    doc.setTextColor(...color);
    const lines = doc.splitTextToSize(String(text || ""), maxWidth);
    doc.text(lines, marginX, startY);
    return startY + (lines.length * (fontSize * 0.5 + 1.8));
  }

  function HistoryView({ currentUser }) {
    const [deploys, setDeploys] = useState([]);
    const [histTotal, setHistTotal] = useState(0);
    const [histPage, setHistPage] = useState(0);
    const [histLoading, setHistLoading] = useState(false);
    const [detail, setDetail] = useState(null);
    const [actions, setActions] = useState(null);
    const [loadingActions, setLoadingActions] = useState(false);
    const [modalLoading, setModalLoading] = useState(null);
    const [refreshingDetail, setRefreshingDetail] = useState(false);

    const loadDeploys = async (page = 0) => {
      setHistLoading(true);
      const offset = page * HIST_PAGE_SIZE;
      const data = await apiFetch(`/history?limit=${HIST_PAGE_SIZE}&offset=${offset}`);
      setHistLoading(false);
      if (data?.deploys) {
        setDeploys(data.deploys);
        setHistTotal(data.total || 0);
        setHistPage(page);
      }
    };

    const clearHistory = async () => {
      if (!window.confirm("¿Eliminar todo el historial de deploys? Esta acción no se puede deshacer.")) return;
      const res = await apiFetch("/history", { method: "DELETE" });
      if (res?.error) { window.alert(res.error); return; }
      setDeploys([]);
      setHistTotal(0);
      setHistPage(0);
      setDetail(null);
    };

    useEffect(() => { loadDeploys(0); }, []);

    const isSuperAdmin = currentUser?.role === "superadmin";

    const openDetail = async (id) => {
      setModalLoading({
        title: "Cargando detalle",
        message: `Preparando el deploy #${id}...`,
      });
      try {
        setActions(null);
        const data = await apiFetch(`/history/${id}`);
        setDetail(data);
        // cargar acciones HTTP vinculadas en paralelo
        setLoadingActions(true);
        apiFetch(`/history/${id}/actions`).then((res) => {
          setActions(res?.error ? null : res);
          setLoadingActions(false);
        });
      } finally {
        setModalLoading(null);
      }
    };

    // Refresca el detalle (y sus acciones HTTP) en el lugar, sin cerrar el modal ni
    // mostrar el overlay de carga completo. Útil para deploys 'running' que cambian.
    const refreshDetail = async () => {
      const id = detail?.deploy?.id;
      if (!id || refreshingDetail) return;
      setRefreshingDetail(true);
      try {
        const data = await apiFetch(`/history/${id}`);
        if (data && !data.error) setDetail(data);
        const res = await apiFetch(`/history/${id}/actions`);
        setActions(res?.error ? null : res);
      } finally {
        setRefreshingDetail(false);
      }
    };

    const hasActions = actions?.batches?.length > 0;
    const hasChangeData = Boolean(
      detail?.deploy?.change_summary?.available
      && (detail?.deploy?.change_summary?.file_count || 0) > 0
    );

    const exportChangesPdf = () => {
      if (!detail?.deploy) return;
      const deploy = detail.deploy;
      const changeSummary = deploy.change_summary || {};
      const changedFiles = Array.isArray(changeSummary.files) ? changeSummary.files : [];
      const commits = Array.isArray(changeSummary.commits) ? changeSummary.commits : [];
      const impacts = Array.isArray(changeSummary.impacts) ? changeSummary.impacts : [];
      const validations = Array.isArray(changeSummary.validations) ? changeSummary.validations : [];
      if (!changedFiles.length) {
        window.alert("Este deploy no tiene información suficiente para generar un PDF solo de cambios.");
        return;
      }

      const title = `Cambios del deploy #${deploy.id} — ${deploy.branch || "sin branch"}`;
      const subtitle = `${deploy.status || "—"} · ${changeSummary.file_count || 0} archivo(s) · ${commits.length} commit(s)`;
      const ctx = pdfCreateDoc({ title, subtitle });
      if (!ctx) { window.alert("Librería PDF no disponible"); return; }
      const { doc, pageW } = ctx;
      let y = ctx.startY;

      y = pdfAddMetrics({ doc, pageW, startY: y, metrics: [
        { label: "Deploy", value: `#${deploy.id}` },
        { label: "Branch", value: deploy.branch || "—" },
        { label: "Archivos", value: changeSummary.file_count || 0, color: PDF_C.info },
        { label: "Commits", value: commits.length, color: PDF_C.ok },
        { label: "Estado", value: deploy.status || "—", color: pdfStatusColor(deploy.status) },
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Resumen Ejecutivo" });
      y = pdfAddInfo({ doc, pageW, startY: y, rows: [
        ["Deploy", `#${deploy.id}`],
        ["Branch", deploy.branch || "—"],
        ["Disparado por", deploy.triggered_user || "—"],
        ["Fecha", fmtFullDate(deploy.started_at)],
        ["Commit previo", changeSummary.previous_commit_short || shortenCommit(changeSummary.previous_commit) || "—"],
        ["Commit nuevo", changeSummary.deployed_commit_short || shortenCommit(changeSummary.deployed_commit) || "—"],
        ["Origen", changeSummary.recovered ? "Recuperado desde historial/GitHub" : "Registrado en el deploy"],
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Alcance Del Cambio" });
      y = pdfAddParagraph(
        doc,
        `Este documento resume los cambios incluidos en el deploy #${deploy.id}. Se listan los archivos afectados, los commits identificados y una guía de impacto potencial para facilitar la revisión funcional posterior al despliegue.`,
        y
      );

      if (commits.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y + 2, title: `Commits Incluidos (${commits.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            { header: "SHA", dataKey: "short_sha" },
            { header: "Autor", dataKey: "author" },
            { header: "Mensaje", dataKey: "message" },
          ],
          body: commits.map((commit) => ({
            short_sha: commit.short_sha || shortenCommit(commit.sha) || "—",
            author: commit.author || "—",
            message: commit.message || "Sin mensaje",
          })),
          columnStyles: { 0:{cellWidth:22},1:{cellWidth:35},2:{cellWidth:"auto"} },
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      y = pdfSectionTitle({ doc, pageW, startY: y, title: `Archivos Afectados (${changedFiles.length})` });
      doc.autoTable({
        ...pdfAutoTableDefaults(doc),
        startY: y,
        columns: [
          { header: "Tipo", dataKey: "status" },
          { header: "Archivo", dataKey: "path" },
          { header: "+", dataKey: "additions" },
          { header: "-", dataKey: "deletions" },
          { header: "Cambios", dataKey: "changes" },
        ],
        body: changedFiles.map((file) => ({
          status: file.status || "M",
          path: file.path || "—",
          additions: file.additions ?? "—",
          deletions: file.deletions ?? "—",
          changes: file.changes ?? "—",
        })),
        columnStyles: { 0:{cellWidth:16},1:{cellWidth:"auto"},2:{cellWidth:12},3:{cellWidth:12},4:{cellWidth:16} },
      });
      y = doc.lastAutoTable.finalY + 5;

      if (impacts.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: "Impacto Potencial" });
        impacts.forEach((impact) => {
          y = pdfAddParagraph(doc, `• ${impact}`, y, { color: [15, 23, 42] });
        });
      }

      if (validations.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y + 2, title: "Validaciones Sugeridas" });
        validations.forEach((validation) => {
          y = pdfAddParagraph(doc, `• ${validation}`, y, { color: [15, 23, 42] });
        });
      }

      y = pdfSectionTitle({ doc, pageW, startY: y + 2, title: "Observación Operativa" });
      y = pdfAddParagraph(
        doc,
        "Este PDF debe usarse como respaldo de qué cambios entraron en el deploy. Se recomienda contrastarlo con una validación funcional breve en los módulos o pantallas impactadas antes de comunicar cierre definitivo.",
        y
      );

      pdfAddFooters({ doc, title });
      doc.save(`deploy-${deploy.id}-${slugifyFilePart(deploy.branch)}-cambios.pdf`);
    };

    const exportDeployPdf = () => {
      if (!detail?.deploy) return;
      const deploy = detail.deploy;
      const results = detail.results || [];
      const rollbacks = detail.rollbacks || [];
      const changeSummary = deploy.change_summary || {};
      const changedFiles = Array.isArray(changeSummary.files) ? changeSummary.files : [];
      const changedServers = Array.isArray(changeSummary.servers) ? changeSummary.servers : [];
      const title = `Deploy #${deploy.id} — ${deploy.branch || "sin branch"}`;
      const subtitle = `${deploy.status || "—"} · ${deploy.triggered_user || ""}`;

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

      const okCount  = results.filter(r => r.status === "ok").length;
      const errCount = results.filter(r => r.status === "error").length;
      y = pdfAddMetrics({ doc, pageW, startY: y, metrics: [
        { label: "Deploy",   value: `#${deploy.id}` },
        { label: "Estado",   value: deploy.status || "—", color: pdfStatusColor(deploy.status) },
        { label: "OK",       value: okCount,   color: PDF_C.ok },
        { label: "Errores",  value: errCount,  color: errCount > 0 ? PDF_C.err : PDF_C.muted },
        { label: "Servers",  value: results.length > 0 ? [...new Set(results.map(r => r.server_label))].length : "—" },
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Resumen" });
      y = pdfAddInfo({ doc, pageW, startY: y, rows: [
        ["Branch",       deploy.branch],
        ["Iniciado",     fmtFullDate(deploy.started_at)],
        ["Disparado por",deploy.triggered_user],
        ["Dry run",      deploy.dry_run ? "Sí" : "No"],
        ["Validación",   deploy.validation_status],
        ["Rollback",     deploy.rollback_status],
        ["Commit previo", deploy.previous_commit ? shortenCommit(deploy.previous_commit) : "—"],
        ["Commit nuevo", deploy.deployed_commit ? shortenCommit(deploy.deployed_commit) : "—"],
        ["Archivos cambiados", changeSummary.file_count || 0],
      ]});

      if (changedServers.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Cambios desplegados (${changeSummary.file_count || 0} archivos)` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            { header: "Servidor", dataKey: "server_label" },
            { header: "Antes", dataKey: "previous_commit" },
            { header: "Después", dataKey: "deployed_commit" },
            { header: "Archivos", dataKey: "file_count" },
          ],
          body: changedServers.map((server) => ({
            server_label: server.server_label || "—",
            previous_commit: server.previous_commit_short || "—",
            deployed_commit: server.deployed_commit_short || "—",
            file_count: server.file_count || 0,
          })),
          columnStyles: { 0:{cellWidth:42},1:{cellWidth:28},2:{cellWidth:28},3:{cellWidth:"auto"} },
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      if (changedFiles.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Archivos modificados (${changedFiles.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            { header: "Tipo", dataKey: "status" },
            { header: "Archivo", dataKey: "path" },
          ],
          body: changedFiles.map((file) => ({
            status: file.status || "M",
            path: file.path || "—",
          })),
          columnStyles: { 0:{cellWidth:18},1:{cellWidth:"auto"} },
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      if (results.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Resultados por usuario (${results.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            { header: "Usuario",  dataKey: "virt_user" },
            { header: "Servidor", dataKey: "server_label" },
            { header: "Estado",   dataKey: "__status__" },
            { header: "Commit",   dataKey: "git_commit" },
            { header: "Salida",   dataKey: "output" },
          ],
          body: results.map(r => ({
            virt_user:    r.virt_user || "—",
            server_label: r.server_label || "—",
            __status__:   r.status || "—",
            git_commit:   (r.git_commit || "—").slice(0, 12),
            output:       (r.output || "—").slice(0, 200),
          })),
          columnStyles: { 0:{cellWidth:22},1:{cellWidth:30},2:{cellWidth:18},3:{cellWidth:20},4:{cellWidth:"auto"} },
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      // Validation degraded
      const validationServers = deploy.validation_summary?.servers || [];
      const degraded = validationServers.flatMap(s =>
        (s.degraded_users || []).map(u => ({
          server: s.server_label || s.server_id || "—",
          domain: u.domain || u.virt_user || "—",
          before: u.before_status || "—",
          after:  u.after_status || "—",
        }))
      );
      if (degraded.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Degradados post-deploy (${degraded.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          headStyles: { fillColor: PDF_C.warn, textColor: [255,255,255], fontStyle: "bold", fontSize: 7 },
          columns: [
            {header:"Servidor",dataKey:"server"},{header:"Dominio",dataKey:"domain"},
            {header:"Antes",dataKey:"before"},{header:"Después",dataKey:"after"},
          ],
          body: degraded,
          columnStyles: {0:{cellWidth:35},1:{cellWidth:55},2:{cellWidth:25},3:{cellWidth:"auto"}},
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      // Rollbacks
      if (rollbacks.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Snapshots y rollback (${rollbacks.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Usuario",dataKey:"virt_user"},{header:"Dominio",dataKey:"domain"},
            {header:"Servidor",dataKey:"server_label"},{header:"Snapshot",dataKey:"snapshot_status"},
            {header:"Restore",dataKey:"restore_status"},
          ],
          body: rollbacks.map(r => ({
            virt_user:       r.virt_user||"—", domain:r.domain||"—",
            server_label:    r.server_label||"—", snapshot_status:r.snapshot_status||"—",
            restore_status:  r.restore_status||"—",
          })),
        });
      }

      pdfAddFooters({ doc, title });
      doc.save(`deploy-${deploy.id}-${slugifyFilePart(deploy.branch)}-resumen-tecnico.pdf`);
    };

    const exportCombinedPdf = () => {
      if (!detail?.deploy) return;
      const deploy = detail.deploy;
      const results = detail.results || [];
      const rollbacks = detail.rollbacks || [];
      const changeSummary = deploy.change_summary || {};
      const changedFiles = Array.isArray(changeSummary.files) ? changeSummary.files : [];
      const changedServers = Array.isArray(changeSummary.servers) ? changeSummary.servers : [];
      const batches = actions?.batches || [];
      const httpResults = actions?.results || [];
      const title = `Reporte completo — Deploy #${deploy.id}`;
      const subtitle = `${deploy.branch || "sin branch"} · ${deploy.status || "—"} · ${batches.length} lotes HTTP`;

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

      // ── Deploy metrics ──
      const okCount  = results.filter(r => r.status === "ok").length;
      const errCount = results.filter(r => r.status === "error").length;
      y = pdfAddMetrics({ doc, pageW, startY: y, metrics: [
        { label: "Deploy",    value: `#${deploy.id}` },
        { label: "Estado",    value: deploy.status || "—", color: pdfStatusColor(deploy.status) },
        { label: "Deploy OK", value: okCount,   color: PDF_C.ok },
        { label: "Errores",   value: errCount,  color: errCount > 0 ? PDF_C.err : PDF_C.muted },
        { label: "HTTP lotes",value: batches.length },
        { label: "HTTP OK",   value: batches.reduce((a,b)=>a+(b.ok_count||0),0), color: PDF_C.ok },
      ]});

      y = pdfSectionTitle({ doc, pageW, startY: y, title: "Datos del deploy" });
      y = pdfAddInfo({ doc, pageW, startY: y, rows: [
        ["Branch",       deploy.branch],
        ["Iniciado",     fmtFullDate(deploy.started_at)],
        ["Disparado por",deploy.triggered_user],
        ["Dry run",      deploy.dry_run ? "Sí" : "No"],
        ["Validación",   deploy.validation_status],
        ["Rollback",     deploy.rollback_status],
        ["Commit previo", deploy.previous_commit ? shortenCommit(deploy.previous_commit) : "—"],
        ["Commit nuevo", deploy.deployed_commit ? shortenCommit(deploy.deployed_commit) : "—"],
        ["Archivos cambiados", changeSummary.file_count || 0],
      ]});

      if (changedServers.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Cambios desplegados (${changeSummary.file_count || 0} archivos)` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Servidor",dataKey:"server_label"},
            {header:"Antes",dataKey:"previous_commit"},
            {header:"Después",dataKey:"deployed_commit"},
            {header:"Archivos",dataKey:"file_count"},
          ],
          body: changedServers.map((server) => ({
            server_label: server.server_label || "—",
            previous_commit: server.previous_commit_short || "—",
            deployed_commit: server.deployed_commit_short || "—",
            file_count: server.file_count || 0,
          })),
          columnStyles:{0:{cellWidth:38},1:{cellWidth:25},2:{cellWidth:25},3:{cellWidth:"auto"}},
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      if (changedFiles.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Archivos modificados (${changedFiles.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Tipo",dataKey:"status"},
            {header:"Archivo",dataKey:"path"},
          ],
          body: changedFiles.map((file) => ({
            status: file.status || "M",
            path: file.path || "—",
          })),
          columnStyles:{0:{cellWidth:18},1:{cellWidth:"auto"}},
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      // ── Deploy results ──
      if (results.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Resultados deploy (${results.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Usuario",dataKey:"virt_user"},{header:"Servidor",dataKey:"server_label"},
            {header:"Estado",dataKey:"__status__"},{header:"Commit",dataKey:"git_commit"},{header:"Salida",dataKey:"output"},
          ],
          body: results.map(r => ({
            virt_user:r.virt_user||"—",server_label:r.server_label||"—",
            __status__:r.status||"—",git_commit:(r.git_commit||"—").slice(0,12),output:(r.output||"—").slice(0,150),
          })),
          columnStyles:{0:{cellWidth:22},1:{cellWidth:30},2:{cellWidth:18},3:{cellWidth:20},4:{cellWidth:"auto"}},
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      // ── HTTP batches summary ──
      if (batches.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Acciones HTTP — resumen por lote (${batches.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Servidor",dataKey:"server_label"},{header:"Acción",dataKey:"action"},
            {header:"Estado",dataKey:"__status__"},{header:"OK",dataKey:"ok"},
            {header:"Errores",dataKey:"err"},{header:"Omitidos",dataKey:"skip"},
          ],
          body: batches.map(b => ({
            server_label:b.server_label||"—",
            action:(b.action_label||`${b.method} ${b.route_path}`).slice(0,40),
            __status__:b.status||"—",
            ok:b.ok_count||0, err:(b.error_count||0)+(b.timeout_count||0), skip:b.skipped_count||0,
          })),
          columnStyles:{0:{cellWidth:35},1:{cellWidth:55},2:{cellWidth:18},3:{cellWidth:12},4:{cellWidth:14},5:{cellWidth:"auto"}},
        });
        y = doc.lastAutoTable.finalY + 5;
      }

      // ── HTTP results ──
      if (httpResults.length) {
        y = pdfSectionTitle({ doc, pageW, startY: y, title: `Resultados HTTP por usuario (${httpResults.length})` });
        doc.autoTable({
          ...pdfAutoTableDefaults(doc),
          startY: y,
          columns: [
            {header:"Usuario",dataKey:"virt_user"},{header:"Servidor",dataKey:"server_label"},
            {header:"Dominio",dataKey:"domain"},{header:"Estado",dataKey:"__status__"},
            {header:"HTTP",dataKey:"http_status"},{header:"ms",dataKey:"duration_ms"},
          ],
          body: httpResults.map(r => ({
            virt_user:r.virt_user||"—",server_label:r.server_label||"—",
            domain:r.domain||"—",__status__:r.status||"—",
            http_status:r.http_status>0?r.http_status:"—",duration_ms:r.duration_ms??"-",
          })),
          columnStyles:{0:{cellWidth:22},1:{cellWidth:30},2:{cellWidth:45},3:{cellWidth:18},4:{cellWidth:14},5:{cellWidth:"auto"}},
        });
      }

      pdfAddFooters({ doc, title });
      doc.save(`deploy-${deploy.id}-${slugifyFilePart(deploy.branch)}-reporte-areas.pdf`);
    };

    const exportDetailPdf = exportDeployPdf;

    return (
      <div style={{ animation: "fadeIn .3s ease" }}>
        <div className="history-hero">
          <div className="history-hero-main">
            <div className="history-hero-title">Historial</div>
            <div className="history-hero-count">
              {histTotal > 0 ? `${histTotal} deploy${histTotal !== 1 ? "s" : ""}` : "Sin historial"}
            </div>
            <div className="history-hero-help">
              <span className="history-hero-help-kicker">Compartir con otras áreas</span>
              <span>
                Abre el detalle del deploy y usa <strong>Descargar PDF completo</strong>. Ese archivo incluye el resumen del despliegue y los cambios registrados.
              </span>
            </div>
          </div>
          <div className="history-hero-actions">
            {isSuperAdmin && histTotal > 0 && (
              <button
                onClick={clearHistory}
                className="btn btn-ghost history-hero-btn history-hero-btn-danger"
              >
                🗑 Limpiar historial
              </button>
            )}
            <button onClick={() => loadDeploys(histPage)} className="btn btn-ghost history-hero-btn" disabled={histLoading}>
              {histLoading ? "…" : "↻ Actualizar"}
            </button>
          </div>
        </div>

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

        {detail && (
          <div className="modal-overlay">
            <div className="modal modal-lg">
              <div className="modal-head">
                <span>
                  Deploy #{detail.deploy?.id} — {detail.deploy?.branch}{" "}
                  {detail.deploy?.triggered_user && `(por ${detail.deploy.triggered_user})`}
                </span>
                <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                  <button
                    onClick={refreshDetail}
                    disabled={refreshingDetail}
                    className="btn btn-ghost btn-sm"
                    title="Actualizar la información de este deploy sin cerrar"
                  >
                    {refreshingDetail ? "⏳ Actualizando…" : "🔄 Actualizar"}
                  </button>
                  <button onClick={() => setDetail(null)} className="btn btn-ghost btn-sm">
                    ✕
                  </button>
                </div>
              </div>
              <div className="modal-body">
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 14 }}>
                  <span
                    className="tag"
                    style={{
                      borderColor: `${SC[detail.deploy?.status] || "#64748b"}44`,
                      color: SC[detail.deploy?.status] || "#64748b",
                      background: `${SC[detail.deploy?.status] || "#64748b"}15`,
                    }}
                  >
                    {detail.deploy?.status}
                  </span>
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#64748b", background: "#64748b15" }}>
                    {fmtDate(detail.deploy?.started_at)}
                  </span>
                  {detail.deploy?.dry_run ? (
                    <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                      DRY RUN
                    </span>
                  ) : null}
                  {detail.deploy?.validation_status ? (
                    <span className="tag" style={{ borderColor: `${SC[detail.deploy.validation_status] || "#64748b"}44`, color: SC[detail.deploy.validation_status] || "#64748b", background: `${SC[detail.deploy.validation_status] || "#64748b"}15` }}>
                      validación: {detail.deploy.validation_status}
                    </span>
                  ) : null}
                  {detail.deploy?.rollback_status ? (
                    <span className="tag" style={{ borderColor: `${SC[detail.deploy.rollback_status] || "#a78bfa"}44`, color: SC[detail.deploy.rollback_status] || "#a78bfa", background: `${SC[detail.deploy.rollback_status] || "#a78bfa"}15` }}>
                      rollback: {detail.deploy.rollback_status}
                    </span>
                  ) : null}
                  <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                    ✔ {detail.results?.filter((result) => result.status === "ok").length || 0}
                  </span>
                  <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                    ✗ {detail.results?.filter((result) => result.status === "error").length || 0}
                  </span>
                  {detail.deploy?.change_summary?.file_count >= 0 && (
                    <span className="tag" style={{ borderColor: "#38bdf844", color: "#38bdf8", background: "#38bdf815" }}>
                      cambios: {detail.deploy?.change_summary?.file_count || 0}
                    </span>
                  )}
                </div>

                <div style={{ marginBottom: 14, padding: 10, borderRadius: 10, background: "#101722", border: "1px solid #1e2a3a55" }}>
                  <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 12, marginBottom: 8, color: "#64748b", letterSpacing: "1px" }}>
                    CAMBIOS DESPLEGADOS
                  </div>
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 8, fontSize: 11, color: "#94a3b8" }}>
                    <span>Commit previo: <strong style={{ color: "#e2e8f0" }}>{shortenCommit(detail.deploy?.previous_commit) || "—"}</strong></span>
                    <span>Commit nuevo: <strong style={{ color: "#e2e8f0" }}>{shortenCommit(detail.deploy?.deployed_commit) || "—"}</strong></span>
                    <span>Archivos: <strong style={{ color: "#38bdf8" }}>{detail.deploy?.change_summary?.file_count || 0}</strong></span>
                  </div>
                  {(detail.deploy?.change_summary?.servers || []).length > 0 && (
                    <div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: (detail.deploy?.change_summary?.files || []).length ? 10 : 0 }}>
                      {detail.deploy.change_summary.servers.map((server) => (
                        <div key={`${server.server_id || server.server_label}-${server.deployed_commit || "none"}`} style={{ fontSize: 11, color: "#94a3b8" }}>
                          [{server.server_label || "Servidor"}] <strong style={{ color: "#e2e8f0" }}>{server.previous_commit_short || "—"}</strong> → <strong style={{ color: "#22d3a5" }}>{server.deployed_commit_short || "—"}</strong> · {server.file_count || 0} archivo(s)
                        </div>
                      ))}
                    </div>
                  )}
                  {(detail.deploy?.change_summary?.files || []).length > 0 ? (
                    <FilterableFileList
                      files={detail.deploy.change_summary.files}
                      placeholder="Buscar archivo en el deploy…"
                      maxHeight={260}
                      renderItem={(file, index) => (
                        <div key={`${file.status}-${file.path}-${index}`} style={{ padding: "4px 12px", fontSize: 11, color: "#cbd5e1", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", borderBottom: "1px solid #1e2a3a15" }}>
                          <span style={{ color: file.status === "A" ? "#22d3a5" : file.status === "D" ? "#f43f5e" : file.status === "R" ? "#fbbf24" : "#38bdf8", marginRight: 8 }}>{file.status || "M"}</span>
                          {file.path || "—"}
                        </div>
                      )}
                    />
                  ) : (
                    <div style={{ fontSize: 11, color: "#64748b" }}>
                      No se detectaron archivos modificados o el repositorio ya estaba actualizado al momento del deploy.
                    </div>
                  )}
                </div>
                {(detail.results || []).map((result, index) => (
                  <div
                    key={index}
                    style={{
                      padding: "10px 0",
                      borderBottom: "1px solid #1e2a3a22",
                      display: "grid",
                      gridTemplateColumns: "20px 1fr auto auto",
                      gap: 10,
                      alignItems: "start",
                      fontSize: 12,
                    }}
                  >
                    <span style={{ color: SC[result.status] || "#64748b" }}>{SI[result.status] || "·"}</span>
                    <div>
                      <div style={{ fontWeight: 600 }}>{vuLabel(result.virt_user)}</div>
                      <div style={{ fontSize: 10, color: "#64748b" }}>{result.server_label}</div>
                      {result.output && result.status === "error" && (
                        <div
                          style={{
                            fontSize: 10,
                            color: "#f43f5e",
                            marginTop: 3,
                            background: "#1a0a0a",
                            padding: "4px 8px",
                            borderRadius: 5,
                            wordBreak: "break-all",
                          }}
                        >
                          {result.output.slice(0, 200)}
                        </div>
                      )}
                      {result.migration_status && (
                        <div
                          style={{
                            fontSize: 10,
                            marginTop: 3,
                            color: result.migration_status === "ok"
                              ? "#22d3a5"
                              : result.migration_status === "failed"
                                ? "#f59e0b"
                                : result.migration_status === "pending"
                                  ? "#38bdf8"
                                  : "#64748b",
                          }}
                          title={result.migration_url || ""}
                        >
                          Migraciones:{" "}
                          {result.migration_status === "ok"
                            ? "✅ OK"
                            : result.migration_status === "failed"
                              ? `⚠️ Falló${result.migration_http_status ? ` (HTTP ${result.migration_http_status})` : ""}`
                              : result.migration_status === "pending"
                                ? "⏳ En progreso"
                                : "⏭ Omitido"}
                          {(result.migration_status === "failed" || result.migration_status === "pending") && result.migration_message && (
                            <span style={{ color: "#94a3b8" }}> · {String(result.migration_message).slice(0, 120)}</span>
                          )}
                        </div>
                      )}
                    </div>
                    <span
                      className="tag"
                      style={{
                        borderColor: `${SC[result.status] || "#64748b"}44`,
                        color: SC[result.status] || "#64748b",
                        background: `${SC[result.status] || "#64748b"}15`,
                      }}
                    >
                      {result.status}
                    </span>
                    <span style={{ fontSize: 10, color: "#64748b" }}>{result.git_commit || "—"}</span>
                  </div>
                ))}

                {detail.deploy?.validation_summary && (
                  <div style={{ marginTop: 16, paddingTop: 12, borderTop: "1px solid #1e2a3a33" }}>
                    <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 12, marginBottom: 8, color: "#64748b", letterSpacing: "1px" }}>
                      VALIDACIÓN POST-DEPLOY
                    </div>
                    <div style={{ fontSize: 12, color: "#cbd5e1", marginBottom: 8 }}>
                      Servidores: <strong>{detail.deploy.validation_summary.checked_servers || (detail.deploy.validation_summary.server_id ? 1 : 0)}</strong> ·
                      Degradados: <strong style={{ color: (detail.deploy.validation_summary.degraded_count || 0) > 0 ? "#fbbf24" : "#22d3a5" }}> {detail.deploy.validation_summary.degraded_count || 0}</strong>
                    </div>
                    {(detail.deploy.validation_summary.servers || []).length > 0 && (
                      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
                        {detail.deploy.validation_summary.servers.flatMap((server) =>
                          (server.degraded_users || []).map((item) => (
                            <div key={`${server.server_id}-${item.virt_user}-${item.domain || "nd"}`} style={{ fontSize: 11, color: "#94a3b8" }}>
                              [{server.server_label || server.server_id}] {item.domain || item.virt_user}: <strong style={{ color: "#e2e8f0" }}>{item.before_status}</strong> → <strong style={{ color: "#fbbf24" }}>{item.after_status}</strong>
                            </div>
                          ))
                        )}
                      </div>
                    )}
                  </div>
                )}

                {detail.rollbacks?.length > 0 && (
                  <div style={{ marginTop: 16, paddingTop: 12, borderTop: "1px solid #1e2a3a33" }}>
                    <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 12, marginBottom: 8, color: "#64748b", letterSpacing: "1px" }}>
                      SNAPSHOTS Y ROLLBACK
                    </div>
                    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                      {detail.rollbacks.map((row) => (
                        <div key={row.id} style={{ background: "#131820", border: "1px solid #1e2a3a33", borderRadius: 8, padding: 10 }}>
                          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", marginBottom: 4 }}>
                            <strong style={{ fontSize: 12, color: "#e2e8f0" }}>{row.virt_user}</strong>
                            <span className="tag" style={{ borderColor: `${SC[row.snapshot_status] || "#64748b"}44`, color: SC[row.snapshot_status] || "#64748b", background: `${SC[row.snapshot_status] || "#64748b"}15` }}>
                              snapshot: {row.snapshot_status}
                            </span>
                            {row.restore_status ? (
                              <span className="tag" style={{ borderColor: `${SC[row.restore_status] || "#a78bfa"}44`, color: SC[row.restore_status] || "#a78bfa", background: `${SC[row.restore_status] || "#a78bfa"}15` }}>
                                restore: {row.restore_status}
                              </span>
                            ) : null}
                          </div>
                          <div style={{ fontSize: 10, color: "#64748b" }}>
                            {row.domain || "sin dominio"} · {row.server_label}
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                )}

                {/* Sección de acciones HTTP vinculadas */}
                <div style={{ marginTop: 16, paddingTop: 12, borderTop: "1px solid #1e2a3a33" }}>
                  <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 12, marginBottom: 8, color: "#64748b", letterSpacing: "1px", display: "flex", alignItems: "center", gap: 8 }}>
                    ⚡ ACCIONES HTTP VINCULADAS
                    {loadingActions && <span style={{ fontSize: 10, color: "#00e5ff" }}>cargando...</span>}
                  </div>
                  {!loadingActions && !hasActions && (
                    <div style={{ fontSize: 11, color: "#64748b" }}>
                      Sin acciones HTTP registradas para este deploy.
                      <div style={{ fontSize: 10, marginTop: 3, color: "#475569" }}>
                        Las acciones HTTP se vinculan automáticamente cuando se ejecutan desde el modal post-deploy.
                      </div>
                    </div>
                  )}
                  {hasActions && (
                    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                      {actions.batches.map((batch) => (
                        <div key={batch.id} style={{ background: "#131820", border: "1px solid #22d3ee22", borderRadius: 8, padding: "10px 12px" }}>
                          <div style={{ display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
                            <div>
                              <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>
                                {batch.action_label || `${batch.method} ${batch.route_path}`}
                              </div>
                              <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                                {batch.server_label} · {fmtDate(batch.started_at)}
                              </div>
                            </div>
                            <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
                              <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515", fontSize: 10 }}>✔ {batch.ok_count || 0}</span>
                              <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15", fontSize: 10 }}>✗ {batch.error_count || 0}</span>
                              {(batch.skipped_count || 0) > 0 && (
                                <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815", fontSize: 10 }}>⏭ {batch.skipped_count}</span>
                              )}
                              <span className="tag" style={{ borderColor: `${SC[batch.status] || "#64748b"}44`, color: SC[batch.status] || "#64748b", background: `${SC[batch.status] || "#64748b"}15`, fontSize: 10 }}>
                                {batch.status}
                              </span>
                            </div>
                          </div>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              </div>
              <div className="modal-foot">
                <div style={{ fontSize: 11, color: "#64748b", marginBottom: 10, lineHeight: 1.45 }}>
                  PDF de cambios: muestra los archivos, commits, impacto potencial y validaciones sugeridas del deploy.
                  <br />
                  PDF técnico: incluye el resultado del deploy y los cambios registrados en ese despliegue.
                  <br />
                  PDF completo: documento para compartir con las áreas involucradas.
                </div>
                <div
                  style={{
                    display: "grid",
                    gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
                    gap: 8,
                  }}
                >
                  <button
                    onClick={() => setDetail(null)}
                    className="btn btn-secondary"
                    style={{ minWidth: 0, whiteSpace: "normal", lineHeight: 1.2 }}
                  >
                    Cerrar
                  </button>
                  <button
                    onClick={exportChangesPdf}
                    className="btn btn-ghost"
                    disabled={!hasChangeData}
                    title={hasChangeData ? "Descarga el PDF solo de cambios" : "No hay información suficiente para reconstruir los cambios de este deploy"}
                    style={{
                      minWidth: 0,
                      whiteSpace: "normal",
                      lineHeight: 1.2,
                      opacity: hasChangeData ? 1 : 0.45,
                      cursor: hasChangeData ? "pointer" : "not-allowed",
                      filter: hasChangeData ? "none" : "grayscale(0.35)",
                    }}
                  >
                    {hasChangeData ? "🧩 Descargar cambios del deploy" : "🧩 Cambios del deploy no disponibles"}
                  </button>
                  <button
                    onClick={exportDeployPdf}
                    className="btn btn-ghost"
                    title="Descarga el PDF técnico con los cambios de este deploy"
                    style={{ minWidth: 0, whiteSpace: "normal", lineHeight: 1.2 }}
                  >
                    🧾 Descargar PDF técnico
                  </button>
                  <button
                    onClick={exportCombinedPdf}
                    className="btn btn-primary"
                    style={{ minWidth: 0, whiteSpace: "normal", lineHeight: 1.2 }}
                    title={!hasActions ? "Descarga el reporte en PDF con deploy y cambios registrados" : `Descarga el PDF del deploy + ${actions.batches.length} lote(s) HTTP`}
                  >
                    📊 Descargar PDF completo {hasActions ? `(+${actions.batches.length} HTTP)` : ""}
                  </button>
                </div>
              </div>
            </div>
          </div>
        )}

        {!histLoading && deploys.length === 0 && (
          <div style={{ textAlign: "center", padding: "40px 20px", color: "#64748b" }}>
            <div style={{ fontSize: 32, marginBottom: 10 }}>📋</div>
            <div>Sin historial de deploys</div>
          </div>
        )}
        {histLoading && deploys.length === 0 && (
          <div style={{ textAlign: "center", padding: "40px 20px", color: "#64748b", fontSize: 13 }}>
            Cargando…
          </div>
        )}

        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {deploys.map((deploy) => (
            <div key={deploy.id} className="card" style={{ cursor: "pointer", margin: 0 }} onClick={() => openDetail(deploy.id)}>
              <div style={{ padding: "14px 16px", display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
                <div
                  style={{
                    width: 9,
                    height: 9,
                    borderRadius: "50%",
                    background: SC[deploy.status] || "#64748b",
                    boxShadow: `0 0 6px ${SC[deploy.status] || "#64748b"}`,
                    flexShrink: 0,
                  }}
                />
                <div style={{ flex: 1, minWidth: 100 }}>
                  <div style={{ fontWeight: 700, fontSize: 13 }}>
                    Deploy #{deploy.id} — {deploy.branch}{" "}
                    {deploy.dry_run ? <span style={{ fontSize: 10, color: "#fbbf24" }}>[DRY RUN]</span> : null}
                  </div>
                  <div style={{ fontSize: 11, color: "#64748b", marginTop: 2 }}>
                    {fmtDate(deploy.started_at)} {deploy.triggered_user && `· por ${deploy.triggered_user}`}
                  </div>
                  {String(deploy.triggered_user || "").startsWith("scheduler:") && (
                    <div style={{ fontSize: 11, color: "#9fb4c8", marginTop: 4 }}>
                      Origen programado: {String(deploy.triggered_user).replace(/^scheduler:/, "")}
                    </div>
                  )}
                </div>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {String(deploy.triggered_user || "").startsWith("scheduler:") && (
                    <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                      programado
                    </span>
                  )}
                  <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                    ✔ {deploy.ok_count || 0}
                  </span>
                  {deploy.error_count > 0 && (
                    <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                      ✗ {deploy.error_count}
                    </span>
                  )}
                  <span
                    className="tag"
                    style={{
                      borderColor: `${SC[deploy.status] || "#64748b"}44`,
                      color: SC[deploy.status] || "#64748b",
                      background: `${SC[deploy.status] || "#64748b"}15`,
                    }}
                  >
                    {deploy.status}
                  </span>
                  {deploy.validation_status && (
                    <span className="tag" style={{ borderColor: `${SC[deploy.validation_status] || "#64748b"}44`, color: SC[deploy.validation_status] || "#64748b", background: `${SC[deploy.validation_status] || "#64748b"}15` }}>
                      val: {deploy.validation_status}
                    </span>
                  )}
                  {deploy.rollback_status && (
                    <span className="tag" style={{ borderColor: `${SC[deploy.rollback_status] || "#a78bfa"}44`, color: SC[deploy.rollback_status] || "#a78bfa", background: `${SC[deploy.rollback_status] || "#a78bfa"}15` }}>
                      rb: {deploy.rollback_status}
                    </span>
                  )}
                </div>
              </div>
            </div>
          ))}
        </div>

        {histTotal > HIST_PAGE_SIZE && (
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 16, padding: "10px 0", borderTop: "1px solid #1e2a3a33" }}>
            <button
              onClick={() => loadDeploys(histPage - 1)}
              disabled={histPage === 0 || histLoading}
              className="btn btn-ghost btn-sm"
              style={{ opacity: histPage === 0 ? 0.3 : 1 }}
            >
              ← Anterior
            </button>
            <span style={{ fontSize: 12, color: "#64748b" }}>
              Página {histPage + 1} / {Math.ceil(histTotal / HIST_PAGE_SIZE)}
              <span style={{ marginLeft: 8, color: "#475569" }}>
                ({histPage * HIST_PAGE_SIZE + 1}–{Math.min((histPage + 1) * HIST_PAGE_SIZE, histTotal)} de {histTotal})
              </span>
            </span>
            <button
              onClick={() => loadDeploys(histPage + 1)}
              disabled={(histPage + 1) * HIST_PAGE_SIZE >= histTotal || histLoading}
              className="btn btn-ghost btn-sm"
              style={{ opacity: (histPage + 1) * HIST_PAGE_SIZE >= histTotal ? 0.3 : 1 }}
            >
              Siguiente →
            </button>
          </div>
        )}
      </div>
    );
  }

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