(() => {
  const { useState, useEffect, apiFetch, fmtDate, sleep } = window.DC;
  const { Toggle, DomainCell, HttpActionModal, ModalLoadingOverlay, FilterableFileList } = window.DCComponents;

  function parseUserMetadata(user) {
    try {
      return user.metadata_json ? JSON.parse(user.metadata_json) : (user.metadata || {});
    } catch (_error) {
      return {};
    }
  }

  // Config opt-in por virt_user del hook de migraciones post-deploy.
  // Persiste en metadata_json (merge, preservando otras claves) vía el endpoint
  // PATCH .../metadata (superadmin). El token va aparte, global, en Ajustes.
  function MigrationHookControls({ serverId, user, onSaved, toast }) {
    const initial = parseUserMetadata(user);
    const deployEnabled = user.include_deploy === 1 && user.excluded === 0;
    const explicit = initial.migration_hook_enabled;
    const explicitlyOn = explicit === true || explicit === 1 || explicit === "1";
    const explicitlyOff = explicit === false || explicit === 0 || explicit === "0";
    // Opt-out: si no hay override explícito, sigue al toggle de Deploy.
    const initialEnabled = explicitlyOn ? true : explicitlyOff ? false : deployEnabled;
    // Default acorde al subdirectorio web del deploy: la app se sirve desde
    // /intranet/, no desde la raíz del dominio.
    const DEFAULT_MIGRATION_PATH = "/intranet/migrations";
    const initialPath = typeof initial.migration_path === "string" && initial.migration_path.trim()
      ? initial.migration_path.trim()
      : DEFAULT_MIGRATION_PATH;
    const [enabled, setEnabled] = useState(initialEnabled);
    const [path, setPath] = useState(initialPath);
    const [saving, setSaving] = useState(false);

    const persist = async (nextEnabled, nextPath) => {
      setSaving(true);
      const meta = parseUserMetadata(user);
      const next = {
        ...meta,
        migration_hook_enabled: nextEnabled,
        migration_path: (nextPath || DEFAULT_MIGRATION_PATH).trim() || DEFAULT_MIGRATION_PATH,
      };
      const res = await apiFetch(
        `/servers/${serverId}/users/${encodeURIComponent(user.username)}/metadata`,
        { method: "PATCH", body: { metadata: next } }
      );
      setSaving(false);
      if (res && res.error) {
        if (toast) toast(res.error, "err");
        return;
      }
      if (toast) toast(nextEnabled ? "Migraciones post-deploy activadas" : "Migraciones post-deploy desactivadas");
      if (onSaved) onSaved(next);
    };

    return (
      <div className="user-toggle-group" style={{ gap: 6, alignItems: "center" }}>
        <span title="Migraciones tras un deploy exitoso. Activado por defecto en usuarios con deploy; apágalo aquí para excluir a este usuario.">Migraciones</span>{" "}
        <Toggle
          value={deployEnabled ? enabled : false}
          color="#f59e0b"
          disabled={saving || !deployEnabled}
          onChange={(value) => { setEnabled(value); persist(value, path); }}
        />
        {deployEnabled && enabled && (
          <input
            value={path}
            onChange={(event) => setPath(event.target.value)}
            onBlur={() => { if (path.trim() !== initialPath) persist(true, path); }}
            disabled={saving}
            className="input"
            style={{ width: 150, height: 24, fontSize: 11, padding: "2px 6px" }}
            placeholder="/intranet/migrations"
            title="Ruta del endpoint de migraciones (ej. /intranet/migrations)"
          />
        )}
      </div>
    );
  }

  function ServersView({ servers, reload, toast, currentUser }) {
    const [showAdd, setShowAdd] = useState(false);
    const [editing, setEditing] = useState(null);
    const [expanded, setExpanded] = useState(null);
    const [users, setUsers] = useState({});
    const [loadingUsers, setLoadingUsers] = useState({});
    const [savingUser, setSavingUser] = useState({});
    const [scanning, setScanning] = useState(null);
    const [scanModal, setScanModal] = useState(null);
    const [scanPass, setScanPass] = useState("");
    const [autoInit, setAutoInit] = useState(true);
    const [serverSearch, setServerSearch] = useState("");
    const [searchMatches, setSearchMatches] = useState({});
    const [searchLoading, setSearchLoading] = useState(false);
    const [userSearch, setUserSearch] = useState({});
    const [userStatusFilter, setUserStatusFilter] = useState({});
    const [actionModal, setActionModal] = useState(null);
    const [actionPresets, setActionPresets] = useState([]);
    const [repoStatus, setRepoStatus] = useState({});
    const [cloning, setCloning] = useState(null);
    const [form, setForm] = useState({
      label: "",
      host: "",
      port: 22,
      username: "root",
      base_path: "/home",
      notes: "",
    });
    const [singleModal, setSingleModal] = useState(null);
    const [singlePreview, setSinglePreview] = useState(null);
    const [singleHistory, setSingleHistory] = useState([]);
    const [singleModalLoading, setSingleModalLoading] = useState(false);
    const [singleModalError, setSingleModalError] = useState("");
    const [singleDeployRun, setSingleDeployRun] = useState(null);
    // Identifica la corrida de deploy en curso. Cerrar el modal o iniciar otra
    // corrida incrementa el ref; el loop de polling chequea su id antes de cada
    // setState y se detiene si dejó de ser la corrida activa (evita setState sobre
    // un modal cerrado y loops de polling huérfanos).
    const singleDeployRunIdRef = React.useRef(0);
    const [userDeployDetail, setUserDeployDetail] = useState(null);
    const [userHistoryModal, setUserHistoryModal] = useState(null);
    const [historySearch, setHistorySearch] = useState("");
    const [fileDiagnostic, setFileDiagnostic] = useState(null);
    const [fileDiagnosticLoading, setFileDiagnosticLoading] = useState(false);
    const [fileDiagnosticSearch, setFileDiagnosticSearch] = useState("");
    const [diagExpanded, setDiagExpanded] = useState({});
    const [diagVisible, setDiagVisible] = useState({});
    const [diagElapsed, setDiagElapsed] = useState(0);
    const deferredDiagSearch = React.useDeferredValue(fileDiagnosticSearch);

    useEffect(() => {
      if (!fileDiagnosticLoading) {
        setDiagElapsed(0);
        return;
      }
      setDiagElapsed(0);
      const start = Date.now();
      const interval = setInterval(() => {
        setDiagElapsed(Math.floor((Date.now() - start) / 1000));
      }, 500);
      return () => clearInterval(interval);
    }, [fileDiagnosticLoading]);

    const diagFilteredMappings = React.useMemo(() => {
      if (!fileDiagnostic?.data?.mappings) return null;
      const term = deferredDiagSearch.trim().toLowerCase();
      const filterFiles = (list) => term ? list.filter((f) => f.path.toLowerCase().includes(term)) : list;
      return fileDiagnostic.data.mappings.map((mapping) => ({
        ...mapping,
        only_in_repo: filterFiles(mapping.only_in_repo),
        only_in_deploy: filterFiles(mapping.only_in_deploy),
        size_mismatches: filterFiles(mapping.size_mismatches),
      }));
    }, [fileDiagnostic?.data?.mappings, deferredDiagSearch]);
    const [deleteModal, setDeleteModal] = useState(null);
    const [deletingId, setDeletingId] = useState(null);
    const [modalLoading, setModalLoading] = useState(null);

    const isViewer = currentUser?.role === "viewer";
    const isSuperAdmin = currentUser?.role === "superadmin";
    const canWrite = !isViewer;

    const withModalLoader = async (loadingState, task) => {
      setModalLoading(loadingState);
      try {
        return await task();
      } finally {
        setModalLoading(null);
      }
    };

    useEffect(() => {
      if (servers.length > 0) {
        const nextStatus = {};
        servers.forEach((server) => {
          if (server.repo_exists !== undefined) {
            nextStatus[server.id] = {
              exists: server.repo_exists === 1,
              commit: server.repo_commit || "",
            };
          }
        });
        if (Object.keys(nextStatus).length > 0) setRepoStatus(nextStatus);
      }
    }, [servers]);

    useEffect(() => {
      if (!canWrite) return;
      apiFetch("/http-actions/presets").then((response) => {
        setActionPresets(Array.isArray(response) ? response : []);
      });
    }, [canWrite]);

    useEffect(() => {
      const term = serverSearch.trim();
      if (!term) {
        setSearchMatches({});
        setSearchLoading(false);
        return;
      }

      let cancelled = false;
      const timer = setTimeout(async () => {
        setSearchLoading(true);
        const response = await apiFetch(`/servers/search?q=${encodeURIComponent(term)}`);
        if (cancelled) return;

        const next = {};
        (Array.isArray(response) ? response : []).forEach((item) => {
          next[item.server_id] = item;
        });
        setSearchMatches(next);
        setSearchLoading(false);
      }, 220);

      return () => {
        cancelled = true;
        clearTimeout(timer);
      };
    }, [serverSearch]);

    const resetForm = () =>
      setForm({ label: "", host: "", port: 22, username: "root", base_path: "/home", notes: "" });

    const saveServer = async () => {
      if (!form.label || !form.host) {
        toast("label y host requeridos", "err");
        return;
      }

      if (editing) {
        await apiFetch(`/servers/${editing}`, { method: "PUT", body: form });
        toast("Servidor actualizado");
        setEditing(null);
      } else {
        await apiFetch("/servers", { method: "POST", body: form });
        toast("Servidor agregado");
        setShowAdd(false);
      }

      resetForm();
      reload();
    };

    const deleteServer = async (server) => {
      setDeleteModal(server);
    };

    const confirmDeleteServer = async () => {
      if (!deleteModal) return;

      const counters = getServerCounters(deleteModal);
      setDeletingId(deleteModal.id);

      const response = await apiFetch(`/servers/${deleteModal.id}`, { method: "DELETE" });
      if (response.error) {
        toast(response.error, "err");
        setDeletingId(null);
        return;
      }

      toast(`${deleteModal.label} eliminado · ${counters.total} usuarios (${counters.active} activos, ${counters.excluded} excluidos)`);
      setDeletingId(null);
      setDeleteModal(null);
      if (expanded === deleteModal.id) setExpanded(null);
      reload();
    };

    const loadUsers = async (id, options = {}) => {
      if (options.showLoader) {
        setLoadingUsers((prev) => ({ ...prev, [id]: true }));
      }

      try {
        const data = await apiFetch(`/servers/${id}/users`);
        setUsers((prev) => ({ ...prev, [id]: Array.isArray(data) ? data : [] }));
      } finally {
        if (options.showLoader) {
          setLoadingUsers((prev) => ({ ...prev, [id]: false }));
        }
      }
    };

    // Actualiza SOLO la fila de un usuario en el estado local, sin recargar
    // toda la lista del servidor (la query de /users es pesada). Se usa tras
    // cambios rápidos por-usuario: toggles Deploy/Excluir, dominio, migraciones.
    const patchUserInState = (serverId, username, fields) => {
      setUsers((prev) => {
        const list = prev[serverId];
        if (!Array.isArray(list)) return prev;
        return {
          ...prev,
          [serverId]: list.map((item) => (item.username === username ? { ...item, ...fields } : item)),
        };
      });
    };

    const toggleExpand = async (id) => {
      if (expanded === id) {
        setExpanded(null);
        return;
      }
      setExpanded(id);
      await loadUsers(id, { showLoader: true });
    };

    const confirmScan = async () => {
      const server = scanModal;
      setScanModal(null);
      setScanning(server.id);

      try {
        const response = await apiFetch(`/servers/${server.id}/scan`, {
          method: "POST",
          body: { password: scanPass, auto_init_git: autoInit },
        });

        if (response.error) {
          toast(response.error, "err");
        } else {
          const summary = [`${response.total} usuarios`];
          if (response.added_count > 0) summary.push(`+${response.added_count} nuevos`);
          if (response.removed_count > 0) summary.push(`-${response.removed_count} eliminados`);
          if (response.initialized > 0) summary.push(`${response.initialized} git init`);
          toast(summary.join(" · "));
          await loadUsers(server.id);
          setExpanded(server.id);
          setRepoStatus((prev) => ({
            ...prev,
            [server.id]: { exists: response.repo_exists, commit: response.repo_commit },
          }));
        }
      } catch {
        toast("Error SSH", "err");
      }

      setScanning(null);
    };

    const cloneRepo = async (server) => {
      setCloning(server.id);
      try {
        const response = await apiFetch(`/servers/${server.id}/clone-repo`, {
          method: "POST",
          body: { password: "" },
        });

        if (response.error) {
          toast(response.error, "err");
        } else {
          toast(response.message || "Repo clonado ✅");
          setRepoStatus((prev) => ({
            ...prev,
            [server.id]: { exists: true, commit: response.commit },
          }));
        }
      } catch {
        toast("Error al clonar", "err");
      }
      setCloning(null);
    };

    const patchUser = async (serverId, username, patch) => {
      const key = `${serverId}:${username}`;
      setSavingUser((prev) => ({ ...prev, [key]: true }));
      try {
        const res = await apiFetch(`/servers/${serverId}/users/${username}`, { method: "PATCH", body: patch });
        if (res && res.error) {
          toast(res.error, "err");
          return;
        }
        patchUserInState(serverId, username, patch);
      } catch (_error) {
        toast("No se pudo actualizar el usuario", "err");
      } finally {
        setSavingUser((prev) => {
          const next = { ...prev };
          delete next[key];
          return next;
        });
      }
    };

    const loadSingleUserHistory = async (serverId, username) => {
      const response = await apiFetch(`/servers/${serverId}/users/${encodeURIComponent(username)}/history?limit=6`);
      const history = Array.isArray(response?.history) ? response.history : [];
      setSingleHistory(history);
      return history;
    };

    const closeSingleDeployModal = () => {
      singleDeployRunIdRef.current += 1; // invalida cualquier loop de polling en vuelo
      setSingleModal(null);
      setSinglePreview(null);
      setSingleHistory([]);
      setSingleDeployRun(null);
      setSingleModalLoading(false);
      setSingleModalError("");
    };

    const loadSingleDeployModalData = async (serverId, username) => {
      setSingleModalLoading(true);
      setSingleModalError("");
      const previewController = typeof AbortController === "function" ? new AbortController() : null;
      const previewTimeout = previewController
        ? setTimeout(() => previewController.abort(), 25000)
        : null;

      const [previewResponse, historyResponse] = await Promise.allSettled([
        apiFetch(`/servers/${serverId}/preview`, {
          method: "POST",
          body: { username },
          ...(previewController ? { signal: previewController.signal } : {}),
        }),
        apiFetch(`/servers/${serverId}/users/${encodeURIComponent(username)}/history?limit=6`),
      ]);
      if (previewTimeout) clearTimeout(previewTimeout);

      if (historyResponse.status === "fulfilled") {
        const history = Array.isArray(historyResponse.value?.history) ? historyResponse.value.history : [];
        setSingleHistory(history);
      } else {
        setSingleHistory([]);
      }

      if (previewResponse.status !== "fulfilled") {
        setSinglePreview(null);
        const timedOut = previewResponse.reason?.name === "AbortError";
        const message = timedOut
          ? "El preview tardó demasiado. Revisa el diagnóstico del servidor o intenta nuevamente."
          : "No se pudo generar el preview del deploy.";
        setSingleModalError(message);
        toast(message, "err");
        setSingleModalLoading(false);
        return;
      }

      if (previewResponse.value?.error) {
        setSinglePreview(null);
        setSingleModalError(previewResponse.value.error);
        toast(previewResponse.value.error, "err");
        setSingleModalLoading(false);
        return;
      }

      setSinglePreview(previewResponse.value);
      setSingleModalLoading(false);
    };

    const openUserHistoryModal = async (serverId, username) => {
      await withModalLoader(
        {
          title: "Cargando historial",
          message: `Buscando deploys recientes de ${username}...`,
        },
        async () => {
          const response = await apiFetch(`/servers/${serverId}/users/${encodeURIComponent(username)}/history?limit=10`);
          if (response?.error) {
            toast(response.error, "err");
            return;
          }
          setUserHistoryModal({
            serverId,
            username,
            history: Array.isArray(response?.history) ? response.history : [],
          });
        }
      );
    };

    const openLastDeployDetail = async (serverId, username, deployId) => {
      if (!deployId) return;
      await withModalLoader(
        {
          title: "Cargando detalle",
          message: `Abriendo el deploy #${deployId} de ${username}...`,
          front: true,
        },
        async () => {
          const response = await apiFetch(`/history/${deployId}`);
          if (response?.error) {
            toast(response.error, "err");
            return;
          }

          const results = Array.isArray(response?.results)
            ? response.results.filter((item) =>
              item.server_id === serverId
              && (item.virt_user === username || item.virt_user === "_git_pull_" || item.virt_user === "_server_" || item.virt_user === "_lock_"))
            : [];

          setUserDeployDetail({
            serverId,
            username,
            deployId,
            deploy: response?.deploy || null,
            results,
          });
        }
      );
    };

    const openActionModal = (server, targetUsers) => {
      const eligibleUsers = (targetUsers || [])
        .filter((user) => user.domain && !user.excluded)
        .map((user) => ({
          serverId: server.id,
          serverLabel: server.label,
          username: user.username,
          domain: user.domain,
        }));

      if (!eligibleUsers.length) {
        toast("No hay usuarios elegibles con dominio para ejecutar la acción", "err");
        return;
      }

      setActionModal({
        users: eligibleUsers,
        presets: actionPresets,
        initialForm: {},
      });
    };

    const deploySingleUser = async (serverId, username) => {
      toast("Abriendo modal de deploy...");
      setSingleDeployRun(null);
      setSinglePreview(null);
      setSingleHistory([]);
      setSingleModalError("");
      setSingleModal({ serverId, username });
      await loadSingleDeployModalData(serverId, username);
    };

    const runSingleDeploy = async (serverId, username, dryRun = false, options = {}) => {
      const mode = options.mode === "resync" ? "resync" : "deploy";
      const isResync = mode === "resync";
      // Marca esta corrida como la activa; si el modal se cierra o arranca otra, el
      // id cambia y el polling de abajo se detiene sin tocar el estado.
      const runId = ++singleDeployRunIdRef.current;
      const isActive = () => singleDeployRunIdRef.current === runId;
      setSingleDeployRun({
        deployId: null,
        dryRun,
        mode,
        status: "starting",
        results: [],
        okCount: 0,
        errorCount: 0,
        validationStatus: null,
        rollbackStatus: null,
        message: null,
      });
      if (isResync) {
        toast(`Re-sincronizando ${username} (sync completo)...`);
      } else {
        toast(`${dryRun ? "Simulando" : "Deployando"} ${username}...`);
      }

      const endpoint = isResync
        ? `/servers/${serverId}/users/${username}/resync`
        : `/servers/${serverId}/users/${username}/deploy`;
      const body = isResync ? {} : { dry_run: dryRun };
      const response = await apiFetch(endpoint, { method: "POST", body });
      if (!isActive()) return; // modal cerrado durante el arranque

      if (response?.error) {
        setSingleDeployRun((prev) => ({
          ...(prev || {}),
          status: "failed",
          errorCount: 1,
          message: response.error,
        }));
        toast(response.error, "err");
        return;
      }

      const deployId = response.deploy_id;
      setSingleDeployRun((prev) => ({
        ...(prev || {}),
        deployId,
        status: "running",
      }));
      toast(`Deploy #${deployId} iniciado para ${username}`);

      let done = false;
      while (!done) {
        await sleep(1500);
        if (!isActive()) return; // modal cerrado u otra corrida: cortar sin tocar estado
        const status = await apiFetch(`/deploy/${deployId}/status`);
        if (!isActive()) return;
        if (status?.error) {
          setSingleDeployRun((prev) => ({
            ...(prev || {}),
            deployId,
            status: "failed",
            message: status.error,
          }));
          toast(status.error, "err");
          return;
        }

        const currentResults = Array.isArray(status?.results)
          ? status.results.filter((item) =>
            item.server_id === serverId
            && (item.virt_user === username || item.virt_user === "_git_pull_" || item.virt_user === "_server_" || item.virt_user === "_lock_"))
          : [];
        const currentStatus = status?.deploy?.status || "running";

        setSingleDeployRun((prev) => ({
          ...(prev || {}),
          deployId,
          status: currentStatus,
          results: currentResults,
          okCount: currentResults.filter((item) => item.status === "ok").length,
          errorCount: currentResults.filter((item) => item.status === "error").length,
          validationStatus: status?.deploy?.validation_status || null,
          rollbackStatus: status?.deploy?.rollback_status || null,
        }));

        done = currentStatus !== "running";
      }

      const finalStatus = await apiFetch(`/deploy/${deployId}/status`);
      if (!isActive()) return;
      const finalResults = Array.isArray(finalStatus?.results)
        ? finalStatus.results.filter((item) =>
          item.server_id === serverId
          && (item.virt_user === username || item.virt_user === "_git_pull_" || item.virt_user === "_server_" || item.virt_user === "_lock_"))
        : [];
      const finalDeployStatus = finalStatus?.deploy?.status || "desconocido";
      const lockConflict = finalResults.find((item) => item.virt_user === "_lock_" && item.status === "error");
      const failureMessage = lockConflict
        ? `Bloqueado: ${String(lockConflict.output || "").split("\n")[0] || "otro deploy en curso"}`
        : null;

      setSingleDeployRun((prev) => ({
        ...(prev || {}),
        deployId,
        status: finalDeployStatus,
        results: finalResults,
        okCount: finalResults.filter((item) => item.status === "ok").length,
        errorCount: finalResults.filter((item) => item.status === "error").length,
        validationStatus: finalStatus?.deploy?.validation_status || null,
        rollbackStatus: finalStatus?.deploy?.rollback_status || null,
        message: failureMessage,
      }));

      const actionLabel = isResync ? "Resync" : dryRun ? "Dry run" : "Deploy";
      if (finalDeployStatus === "success") {
        toast(`${actionLabel} completado correctamente para ${username}`);
      } else if (finalDeployStatus === "partial") {
        toast(`${actionLabel} finalizado con alertas para ${username}`, "warn");
      } else if (lockConflict) {
        toast(`${actionLabel} bloqueado: hay otro deploy en curso sobre este servidor`, "err");
      } else {
        toast(`${actionLabel} falló para ${username}`, "err");
      }

      const postDeployTasks = [
        loadSingleUserHistory(serverId, username),
        loadUsers(serverId),
      ];

      if (!dryRun && (finalDeployStatus === "success" || finalDeployStatus === "partial")) {
        postDeployTasks.push(
          apiFetch(`/servers/${serverId}/preview`, { method: "POST", body: { username } }).then((preview) => {
            if (!preview?.error) setSinglePreview(preview);
          })
        );
      }

      await Promise.all(postDeployTasks);

      if (typeof options.onComplete === "function") {
        await options.onComplete({
          deployId,
          status: finalDeployStatus,
          results: finalResults,
        });
      }
    };

    const confirmSingleDeploy = async (dryRun = false) => {
      const { serverId, username } = singleModal;
      await runSingleDeploy(serverId, username, dryRun);
    };

    const closeFileDiagnostic = () => {
      setFileDiagnostic(null);
      setFileDiagnosticSearch("");
      setDiagExpanded({});
      setDiagVisible({});
    };

    const runFileDiagnostic = async (serverId, username) => {
      setFileDiagnosticLoading(true);
      setFileDiagnostic({ serverId, username, loading: true });
      setDiagExpanded({});
      setDiagVisible({});
      try {
        const response = await apiFetch(`/servers/${serverId}/users/${username}/file-diagnostic`, {
          method: "POST",
          body: {},
        });
        if (response?.error) {
          toast(response.error, "err");
          setFileDiagnostic(null);
          setFileDiagnosticSearch("");
          return;
        }
        setFileDiagnostic({ serverId, username, data: response });
      } catch (error) {
        toast(`Error de diagnóstico: ${error?.message || error}`, "err");
        setFileDiagnostic(null);
        setFileDiagnosticSearch("");
      } finally {
        setFileDiagnosticLoading(false);
      }
    };

    const releaseDeployLockAndRetry = async (serverId, username, mode = "deploy") => {
      const ok = typeof window !== "undefined" && window.confirm
        ? window.confirm(
            "Liberar el lock de deploy del servidor?\n\n" +
            "Solo hacelo si estas seguro que el deploy bloqueante ya no esta corriendo " +
            "(crashed, reinicio, etc.). Si liberas un lock de un deploy real podes generar " +
            "conflictos."
          )
        : true;
      if (!ok) return;

      const response = await apiFetch(`/deploy/locks/release`, {
        method: "POST",
        body: { server_id: serverId },
      });
      if (response?.error) {
        toast(response.error, "err");
        return;
      }
      toast("Lock liberado — reintentando deploy...");
      await runSingleDeploy(serverId, username, false, mode === "resync" ? { mode: "resync" } : {});
    };

    const confirmSingleResync = async () => {
      const { serverId, username } = singleModal;
      const ok = typeof window !== "undefined" && window.confirm
        ? window.confirm(
            `Reparar (sync completo) para ${username}?\n\n` +
            "Esta accion limpia el baseline de deploy del usuario y luego " +
            "re-sincroniza TODOS los archivos no protegidos desde el repo " +
            "(rsync no destructivo: no borra archivos del cliente, no toca " +
            "config.php, .htaccess ni la lista de protegidos).\n\n" +
            "Usalo cuando al cliente le faltan archivos despues de un deploy " +
            "o cuando la migracion falla por archivos ausentes."
          )
        : true;
      if (!ok) return;
      await runSingleDeploy(serverId, username, false, { mode: "resync" });
    };

    const singleDeployTone = (() => {
      const status = String(singleDeployRun?.status || "");
      if (status === "success") return { border: "#22d3a544", color: "#22d3a5", background: "#22d3a515", label: "completado" };
      if (status === "partial") return { border: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415", label: "parcial" };
      if (status === "failed" || status === "error") return { border: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15", label: "fallido" };
      if (status === "starting" || status === "running") return { border: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15", label: "en proceso" };
      return { border: "#64748b44", color: "#94a3b8", background: "#64748b15", label: "sin ejecutar" };
    })();

    const getDeployBadgeTone = (status) => {
      if (status === "ok" || status === "success") {
        return { borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" };
      }
      if (status === "partial" || status === "skipped") {
        return { borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" };
      }
      if (status === "error" || status === "failed") {
        return { borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" };
      }
      if (status === "running" || status === "starting") {
        return { borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" };
      }
      return { borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" };
    };

    const getUserDeployFilterState = (user) => {
      const resultStatus = String(user?.last_deploy_result_status || "").toLowerCase();
      const deployStatus = String(user?.last_deploy_status || "").toLowerCase();
      if (!user?.last_deploy_id && !resultStatus && !deployStatus) return "no_history";
      if (resultStatus === "error" || deployStatus === "failed") return "failed";
      if (resultStatus === "ok" && deployStatus === "success") return "success";
      if (deployStatus === "partial" || resultStatus === "skipped") return "warning";
      if (deployStatus === "running") return "running";
      return "other";
    };

    const summarizeDeployOutput = (entry) => {
      const output = String(entry?.output || "").trim();
      const deployStatus = String(entry?.deploy_status || "").toLowerCase();
      const resultStatus = String(entry?.result_status || entry?.status || "").toLowerCase();

      if (resultStatus === "ok" && deployStatus === "success") {
        return entry?.dry_run ? "Dry run completado correctamente." : "Deploy completado correctamente.";
      }
      if (deployStatus === "partial") return "Deploy finalizado con alertas.";
      if (resultStatus === "error" || deployStatus === "failed") {
        return output.split("\n").find(Boolean) || "Deploy fallido.";
      }
      if (resultStatus === "skipped") return "Dry run ejecutado.";
      return output.split("\n").find(Boolean) || "Sin detalle";
    };

    const normalizedSearch = serverSearch.trim().toLowerCase();
    const matchesServerDirectly = (server) =>
      !normalizedSearch
      || server.label.toLowerCase().includes(normalizedSearch)
      || server.host.toLowerCase().includes(normalizedSearch)
      || String(server.username || "").toLowerCase().includes(normalizedSearch)
      || String(server.base_path || "").toLowerCase().includes(normalizedSearch)
      || String(server.notes || "").toLowerCase().includes(normalizedSearch);

    const getUserMatchReasons = (user, term) => {
      const normalizedTerm = String(term || "").trim().toLowerCase();
      if (!normalizedTerm) return [];

      const reasons = [];
      if (String(user.username || "").toLowerCase().includes(normalizedTerm)) reasons.push("usuario");
      if (String(user.domain || "").toLowerCase().includes(normalizedTerm)) reasons.push("dominio");
      if (String(user.deploy_path || "").toLowerCase().includes(normalizedTerm)) reasons.push("ruta");
      return reasons;
    };

    const highlightMatch = (text, term) => {
      const value = String(text == null ? "" : text);
      const normalizedTerm = String(term || "").trim();
      if (!normalizedTerm) return value;
      const lowerValue = value.toLowerCase();
      const lowerTerm = normalizedTerm.toLowerCase();
      const parts = [];
      let lastIndex = 0;
      let matchIndex = lowerValue.indexOf(lowerTerm);
      let key = 0;
      while (matchIndex !== -1) {
        if (matchIndex > lastIndex) parts.push(value.slice(lastIndex, matchIndex));
        parts.push(
          <mark key={`hl-${key++}`} className="search-highlight">
            {value.slice(matchIndex, matchIndex + normalizedTerm.length)}
          </mark>
        );
        lastIndex = matchIndex + normalizedTerm.length;
        matchIndex = lowerValue.indexOf(lowerTerm, lastIndex);
      }
      if (lastIndex < value.length) parts.push(value.slice(lastIndex));
      return parts.length ? parts : value;
    };

    const getServerCounters = (server) => {
      const loadedUsers = users[server.id];
      if (Array.isArray(loadedUsers) && loadedUsers.length > 0) {
        return {
          total: loadedUsers.length,
          active: loadedUsers.filter((user) => user.excluded !== 1 && user.include_deploy === 1).length,
          excluded: loadedUsers.filter((user) => user.excluded === 1).length,
        };
      }

      return {
        total: Number(server.total_users || 0),
        active: Number(server.active_users || 0),
        excluded: Number(server.excluded_users || 0),
      };
    };

    const filteredServers = servers.filter((server) => {
      if (!normalizedSearch) return true;
      return matchesServerDirectly(server) || Boolean(searchMatches[server.id]);
    });

    const overallCounters = servers.reduce((acc, server) => {
      const counters = getServerCounters(server);
      acc.total += counters.total;
      acc.active += counters.active;
      acc.excluded += counters.excluded;
      return acc;
    }, { total: 0, active: 0, excluded: 0 });

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

        {deleteModal && (
          <div className="modal-overlay">
            <div className="modal">
              <div className="modal-head">
                <span>🗑 Eliminar servidor — {deleteModal.label}</span>
                {!deletingId && (
                  <button onClick={() => setDeleteModal(null)} className="btn btn-ghost btn-sm">
                    ✕
                  </button>
                )}
              </div>
              <div className="modal-body">
                <div className="alert alert-danger" style={{ marginBottom: 12 }}>
                  Vas a eliminar este servidor del sistema. Sus usuarios escaneados y monitoreo ligado al servidor se retirarán de la lista activa.
                </div>
                <div style={{ background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 14, marginBottom: 12 }}>
                  <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 800, fontSize: 16, marginBottom: 6 }}>
                    {deleteModal.label}
                  </div>
                  <div style={{ fontSize: 12, color: "#94a3b8", marginBottom: 10 }}>
                    {deleteModal.username}@{deleteModal.host}:{deleteModal.port} · {deleteModal.base_path}
                  </div>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                    {(() => {
                      const counters = getServerCounters(deleteModal);
                      return (
                        <>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                            usuarios: {counters.total}
                          </span>
                          <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                            activos: {counters.active}
                          </span>
                          <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                            excluidos: {counters.excluded}
                          </span>
                        </>
                      );
                    })()}
                  </div>
                </div>
                <div className="alert alert-info" style={{ fontSize: 11 }}>
                  El historial de deploys se conserva, pero quedará desacoplado del servidor eliminado.
                </div>
              </div>
              <div className="modal-foot btn-row">
                <button
                  onClick={() => setDeleteModal(null)}
                  className="btn btn-secondary"
                  disabled={Boolean(deletingId)}
                >
                  Cancelar
                </button>
                <button
                  onClick={confirmDeleteServer}
                  className="btn btn-danger"
                  disabled={Boolean(deletingId)}
                >
                  {deletingId ? "Eliminando..." : "🗑 Confirmar eliminación"}
                </button>
              </div>
            </div>
          </div>
        )}

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

        {userDeployDetail && (
          <div className="modal-overlay modal-overlay-front">
            <div className="modal">
              {(() => {
                const isRetryingSingleDeploy = ["starting", "running"].includes(singleDeployRun?.status);

                return (
                  <>
              <div className="modal-head">
                <span>📋 Último resultado — {userDeployDetail.username}</span>
                <button onClick={() => setUserDeployDetail(null)} className="btn btn-ghost btn-sm" disabled={isRetryingSingleDeploy}>
                  ✕
                </button>
              </div>
              <div className="modal-body">
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 12 }}>
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                    deploy #{userDeployDetail.deployId}
                  </span>
                  <span
                    className="tag"
                    style={getDeployBadgeTone(userDeployDetail.deploy?.status)}
                  >
                    deploy: {userDeployDetail.deploy?.status || "—"}
                  </span>
                  {userDeployDetail.deploy?.branch ? (
                    <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                      {userDeployDetail.deploy.branch}
                    </span>
                  ) : null}
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                    {fmtDate(userDeployDetail.deploy?.finished_at || userDeployDetail.deploy?.started_at)}
                  </span>
                </div>
                <div className="alert alert-info" style={{ fontSize: 11, marginBottom: 12 }}>
                  {userDeployDetail.deploy?.triggered_user
                    ? `Lanzado por ${userDeployDetail.deploy.triggered_user}.`
                    : "Detalle del último deploy del usuario."}
                </div>
                {!userDeployDetail.results.length ? (
                  <div className="alert alert-warn" style={{ fontSize: 11 }}>
                    No se encontraron resultados específicos para este usuario en ese deploy.
                  </div>
                ) : (
                  <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                    {userDeployDetail.results.map((result) => (
                      <div key={result.id} style={{ border: "1px solid #1e2a3a", borderRadius: 10, padding: 12, background: "#0a0e14" }}>
                        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                          <span
                            className="tag"
                            style={getDeployBadgeTone(result.status)}
                          >
                            {result.virt_user}
                          </span>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                            {result.server_label || "Servidor"}
                          </span>
                          {result.git_commit ? (
                            <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                              {result.git_commit}
                            </span>
                          ) : null}
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                            {fmtDate(result.created_at)}
                          </span>
                        </div>
                        <div style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, color: "#cbd5e1", lineHeight: 1.6 }}>
                          {result.output || "Sin salida registrada."}
                        </div>
                        {result.migration_status && (
                          <div
                            style={{
                              marginTop: 8,
                              paddingTop: 8,
                              borderTop: "1px solid #1e2a3a",
                              fontSize: 11,
                              color: result.migration_status === "ok"
                                ? "#22d3a5"
                                : result.migration_status === "failed"
                                  ? "#f59e0b"
                                  : result.migration_status === "pending"
                                    ? "#38bdf8"
                                    : "#94a3b8",
                            }}
                            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 && (
                              <div style={{ marginTop: 3, color: "#94a3b8", wordBreak: "break-word" }}>
                                {String(result.migration_message).slice(0, 200)}
                              </div>
                            )}
                            {result.migration_url && (
                              <div style={{ marginTop: 3, color: "#64748b", fontSize: 10, wordBreak: "break-all" }}>
                                {result.migration_url}
                              </div>
                            )}
                          </div>
                        )}
                      </div>
                    ))}
                  </div>
                )}
              </div>
              <div className="modal-foot btn-row">
                <button onClick={() => setUserDeployDetail(null)} className="btn btn-secondary" disabled={isRetryingSingleDeploy}>
                  Cerrar
                </button>
                <button
                  onClick={() => runSingleDeploy(
                    userDeployDetail.serverId,
                    userDeployDetail.username,
                    false,
                    {
                      onComplete: async ({ deployId }) => {
                        await openLastDeployDetail(userDeployDetail.serverId, userDeployDetail.username, deployId);
                      },
                    }
                  )}
                  className="btn btn-danger"
                  disabled={isRetryingSingleDeploy}
                >
                  {isRetryingSingleDeploy ? "⟳ Reintentando..." : "↻ Reintentar deploy"}
                </button>
              </div>
                  </>
                );
              })()}
            </div>
          </div>
        )}

        {fileDiagnostic && (
          <div className="modal-overlay modal-overlay-front">
            <div className="modal" style={{ maxWidth: 980 }}>
              <div className="modal-head">
                <span>🔍 Diagnóstico de archivos — {fileDiagnostic.username}</span>
                <button onClick={closeFileDiagnostic} className="btn btn-ghost btn-sm">✕</button>
              </div>
              <div className="modal-body">
                {fileDiagnostic.loading || !fileDiagnostic.data ? (
                  <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 4px" }}>
                    <div className="modal-loading-spinner" style={{ width: 22, height: 22, borderWidth: 2 }} />
                    <div style={{ fontSize: 12, color: "#94a3b8" }}>
                      Comparando repo vs deploy...
                      {diagElapsed > 0 && (
                        <span style={{ marginLeft: 8, color: "#64748b" }}>
                          ({diagElapsed}s {diagElapsed > 15 ? "— puede tomar hasta 25s" : ""})
                        </span>
                      )}
                    </div>
                  </div>
                ) : (
                  <>
                    <div className="alert alert-info" style={{ fontSize: 11, marginBottom: 14 }}>
                      Comparación filesystem entre el repo del servidor y el deploy de <strong>{fileDiagnostic.username}</strong>.
                      <br />
                      <strong>Solo en repo</strong> = archivos que el cliente no tiene (el resync los copiaría).
                      <strong> Solo en deploy</strong> = archivos del cliente que no están en el repo (manuales o legacy).
                      <strong> Tamaño difiere</strong> = el archivo existe en ambos lados pero con bytes distintos.
                    </div>

                    <div className="search-wrap" style={{ marginBottom: 14 }}>
                      <input
                        type="search"
                        className="input"
                        placeholder="🔍 Buscar archivo por nombre o ruta..."
                        value={fileDiagnosticSearch}
                        onChange={(event) => setFileDiagnosticSearch(event.target.value)}
                      />
                      {fileDiagnosticSearch && (
                        <button
                          type="button"
                          onClick={() => setFileDiagnosticSearch("")}
                          className="btn btn-ghost btn-sm"
                          style={{ position: "absolute", right: 8, top: 6, padding: "2px 8px" }}
                        >
                          ✕
                        </button>
                      )}
                    </div>

                    {(() => {
                      const term = deferredDiagSearch.trim().toLowerCase();
                      const CHUNK = 100;
                      const filteredMappings = diagFilteredMappings || [];
                      const totalMatches = term
                        ? filteredMappings.reduce(
                            (acc, m) => acc + m.only_in_repo.length + m.only_in_deploy.length + m.size_mismatches.length,
                            0,
                          )
                        : null;

                      const renderSection = ({ mapping, kind, color, label, items, truncated, totalCount, renderRow }) => {
                        if (totalCount === 0 && !term) return null;
                        const sectionKey = `${mapping.dest_dir}:${kind}`;
                        const isExpanded = !!diagExpanded[sectionKey];
                        const limit = diagVisible[sectionKey] || CHUNK;
                        const visibleItems = isExpanded ? items.slice(0, limit) : [];
                        const remaining = Math.max(0, items.length - visibleItems.length);
                        const summaryText = term
                          ? `${items.length} de ${totalCount}`
                          : `${totalCount}`;
                        return (
                          <div style={{ marginTop: 8 }}>
                            <button
                              type="button"
                              onClick={() => setDiagExpanded((prev) => ({ ...prev, [sectionKey]: !prev[sectionKey] }))}
                              className="btn btn-ghost btn-sm"
                              style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 6, color, fontSize: 12, fontWeight: 700, padding: "6px 8px" }}
                            >
                              <span style={{ width: 14, display: "inline-block" }}>{isExpanded ? "▼" : "▶"}</span>
                              <span>
                                {summaryText} {label} {truncated && !term ? "(server cap: 500)" : ""}
                              </span>
                            </button>
                            {isExpanded && items.length > 0 && (
                              <div style={{ maxHeight: 240, overflowY: "auto", marginTop: 6, fontFamily: "SFMono-Regular,ui-monospace,Menlo,Consolas,monospace", fontSize: 11, color: "#cbd5e1", background: "#06090f", border: "1px solid #1e2a3a", borderRadius: 8, padding: 8 }}>
                                {visibleItems.map(renderRow)}
                                {remaining > 0 && (
                                  <button
                                    type="button"
                                    onClick={() => setDiagVisible((prev) => ({ ...prev, [sectionKey]: (prev[sectionKey] || CHUNK) + CHUNK }))}
                                    className="btn btn-ghost btn-sm"
                                    style={{ marginTop: 8, fontSize: 11 }}
                                  >
                                    Ver {Math.min(CHUNK, remaining)} más ({remaining} restantes)
                                  </button>
                                )}
                              </div>
                            )}
                          </div>
                        );
                      };

                      return (
                        <>
                          {term && (
                            <div style={{ fontSize: 11, color: "#94a3b8", marginBottom: 10 }}>
                              {totalMatches === 0
                                ? `Sin coincidencias para "${fileDiagnosticSearch}".`
                                : `${totalMatches} coincidencia(s) para "${fileDiagnosticSearch}".`}
                            </div>
                          )}
                          {filteredMappings.map((mapping) => {
                            const tone =
                              mapping.only_in_repo_count > 0
                                ? "#f43f5e"
                                : mapping.size_mismatch_count > 0
                                ? "#fbbf24"
                                : "#22d3a5";
                            return (
                              <div key={mapping.dest_dir} style={{ border: `1px solid ${tone}44`, borderRadius: 12, padding: 14, marginBottom: 12, background: "#0a0e14" }}>
                                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8, marginBottom: 10 }}>
                                  <div>
                                    <div style={{ fontFamily: "Syne,sans-serif", fontSize: 14, fontWeight: 800, color: "#f8fafc" }}>
                                      {mapping.source_subdir} → {mapping.dest_dir}
                                    </div>
                                    <div style={{ fontSize: 11, color: "#94a3b8" }}>
                                      {mapping.repo_file_count} archivo(s) en repo · {mapping.deploy_file_count} en cliente
                                    </div>
                                  </div>
                                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                                    <span className="tag" style={{ borderColor: `${mapping.only_in_repo_count > 0 ? "#f43f5e" : "#64748b"}55`, color: mapping.only_in_repo_count > 0 ? "#f43f5e" : "#94a3b8", background: `${mapping.only_in_repo_count > 0 ? "#f43f5e" : "#64748b"}15` }}>
                                      Solo en repo: {mapping.only_in_repo_count}
                                    </span>
                                    <span className="tag" style={{ borderColor: "#64748b55", color: "#94a3b8", background: "#64748b15" }}>
                                      Solo en deploy: {mapping.only_in_deploy_count}
                                    </span>
                                    <span className="tag" style={{ borderColor: `${mapping.size_mismatch_count > 0 ? "#fbbf24" : "#64748b"}55`, color: mapping.size_mismatch_count > 0 ? "#fbbf24" : "#94a3b8", background: `${mapping.size_mismatch_count > 0 ? "#fbbf24" : "#64748b"}15` }}>
                                      Tamaño difiere: {mapping.size_mismatch_count}
                                    </span>
                                  </div>
                                </div>

                                {!mapping.repo_dir_exists && (
                                  <div className="alert alert-warn" style={{ fontSize: 11, marginBottom: 8 }}>
                                    ⚠️ {mapping.repo_dir} no existe en el servidor — verifica que el repo esté clonado.
                                  </div>
                                )}
                                {!mapping.deploy_dir_exists && (
                                  <div className="alert alert-warn" style={{ fontSize: 11, marginBottom: 8 }}>
                                    ⚠️ {mapping.deploy_dir} no existe — el cliente no tiene esta carpeta creada.
                                  </div>
                                )}

                                {renderSection({
                                  mapping,
                                  kind: "repo",
                                  color: "#f43f5e",
                                  label: "archivo(s) faltan en el cliente",
                                  items: mapping.only_in_repo,
                                  truncated: mapping.only_in_repo_truncated,
                                  totalCount: mapping.only_in_repo_count,
                                  renderRow: (file) => (
                                    <div key={file.path}>{file.path} <span style={{ color: "#64748b" }}>({file.size} B)</span></div>
                                  ),
                                })}

                                {renderSection({
                                  mapping,
                                  kind: "deploy",
                                  color: "#94a3b8",
                                  label: "archivo(s) solo en el cliente",
                                  items: mapping.only_in_deploy,
                                  truncated: mapping.only_in_deploy_truncated,
                                  totalCount: mapping.only_in_deploy_count,
                                  renderRow: (file) => (
                                    <div key={file.path}>{file.path} <span style={{ color: "#475569" }}>({file.size} B)</span></div>
                                  ),
                                })}

                                {renderSection({
                                  mapping,
                                  kind: "sizes",
                                  color: "#fbbf24",
                                  label: "archivo(s) con tamaño distinto",
                                  items: mapping.size_mismatches,
                                  truncated: mapping.size_mismatches_truncated,
                                  totalCount: mapping.size_mismatch_count,
                                  renderRow: (file) => (
                                    <div key={file.path}>{file.path} <span style={{ color: "#fbbf24" }}>(repo: {file.repo_size} B / deploy: {file.deploy_size} B)</span></div>
                                  ),
                                })}

                                {mapping.only_in_repo_count === 0 && mapping.size_mismatch_count === 0 && !term && (
                                  <div className="alert alert-success" style={{ fontSize: 11, marginTop: 4 }}>
                                    ✅ El cliente tiene todos los archivos del repo (para este mapping).
                                  </div>
                                )}
                              </div>
                            );
                          })}
                        </>
                      );
                    })()}

                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 10 }}>
                      Excludes aplicados: {fileDiagnostic.data.excludes_applied.join(", ")}<br />
                      Generado: {fmtDate(fileDiagnostic.data.generated_at)}
                    </div>
                  </>
                )}
              </div>
              <div className="modal-foot single-deploy-foot">
                <button onClick={closeFileDiagnostic} className="btn single-deploy-foot-cancel">Cerrar</button>
                {fileDiagnostic.data && (() => {
                  const needsSync = fileDiagnostic.data.mappings.some(
                    (m) => m.only_in_repo_count > 0 || m.size_mismatch_count > 0
                  );
                  return (
                    <div className="single-deploy-foot-actions">
                      <button
                        onClick={() => runFileDiagnostic(fileDiagnostic.serverId, fileDiagnostic.username)}
                        className="btn btn-ghost btn-sm"
                        disabled={fileDiagnosticLoading}
                      >
                        {fileDiagnosticLoading ? "⟳ Re-analizando..." : "↻ Refrescar"}
                      </button>
                      {needsSync && (
                        <button
                          onClick={async () => {
                            const ok = typeof window !== "undefined" && window.confirm
                              ? window.confirm(
                                  `Sincronizar archivos faltantes para ${fileDiagnostic.username}?\n\n` +
                                  "Va a ejecutar Reparar (sync completo): rsync no destructivo " +
                                  "desde el repo, copia los archivos faltantes y actualiza los " +
                                  "que tienen tamaño distinto. NO borra archivos del cliente."
                                )
                              : true;
                            if (!ok) return;
                            const { serverId, username } = fileDiagnostic;
                            closeFileDiagnostic();
                            await runSingleDeploy(serverId, username, false, { mode: "resync" });
                          }}
                          className="btn btn-sm single-deploy-action-deploy"
                          disabled={fileDiagnosticLoading}
                          title="Ejecuta Reparar (sync completo) inmediatamente, sin volver al modal principal"
                        >
                          🔧 Sincronizar ahora
                        </button>
                      )}
                    </div>
                  );
                })()}
              </div>
            </div>
          </div>
        )}

        {userHistoryModal && (
          <div className="modal-overlay">
            <div className="modal">
              <div className="modal-head">
                <span>🕘 Historial — {userHistoryModal.username}</span>
                <button onClick={() => { setUserHistoryModal(null); setHistorySearch(""); }} className="btn btn-ghost btn-sm">
                  ✕
                </button>
              </div>
              <div className="modal-body">
                {!userHistoryModal.history.length ? (
                  <div className="alert alert-info" style={{ fontSize: 11 }}>
                    No hay deploys registrados para este usuario.
                  </div>
                ) : (() => {
                  const term = historySearch.trim().toLowerCase();
                  const filtered = term
                    ? userHistoryModal.history.filter((entry) => {
                        const haystack = [
                          `#${entry.deploy_id}`,
                          entry.result_status,
                          entry.deploy_status,
                          entry.branch,
                          entry.triggered_user,
                          summarizeDeployOutput(entry),
                          fmtDate(entry.finished_at || entry.started_at || entry.result_created_at),
                        ].filter(Boolean).join(" ").toLowerCase();
                        return haystack.includes(term);
                      })
                    : userHistoryModal.history;
                  return (
                  <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                    <div className="search-wrap" style={{ marginBottom: 4 }}>
                      <input
                        type="search"
                        className="input"
                        placeholder="🔍 Buscar por estado, branch, fecha, usuario, mensaje..."
                        value={historySearch}
                        onChange={(event) => setHistorySearch(event.target.value)}
                      />
                      {historySearch && (
                        <button
                          type="button"
                          onClick={() => setHistorySearch("")}
                          className="btn btn-ghost btn-sm"
                          style={{ position: "absolute", right: 8, top: 6, padding: "2px 8px" }}
                        >
                          ✕
                        </button>
                      )}
                    </div>
                    {term && (
                      <div style={{ fontSize: 11, color: "#94a3b8" }}>
                        {filtered.length === 0
                          ? `Sin coincidencias para "${historySearch}".`
                          : `${filtered.length} de ${userHistoryModal.history.length} deploys.`}
                      </div>
                    )}
                    {filtered.map((entry) => (
                      <div key={entry.id} style={{ border: "1px solid #1e2a3a", borderRadius: 10, padding: 12, background: "#0a0e14" }}>
                        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                            deploy #{entry.deploy_id}
                          </span>
                          <span className="tag" style={getDeployBadgeTone(entry.result_status)}>
                            usuario: {entry.result_status || "—"}
                          </span>
                          <span className="tag" style={getDeployBadgeTone(entry.deploy_status)}>
                            deploy: {entry.deploy_status || "—"}
                          </span>
                          {entry.branch ? (
                            <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                              {entry.branch}
                            </span>
                          ) : null}
                          <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                            {fmtDate(entry.finished_at || entry.started_at || entry.result_created_at)}
                          </span>
                        </div>
                        <div style={{ fontSize: 11, color: "#94a3b8", marginBottom: 10 }}>
                          {entry.triggered_user ? `Lanzado por ${entry.triggered_user}` : "Sin usuario disparador registrado"}
                        </div>
                        <div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center" }}>
                          <div style={{ fontSize: 11, color: "#cbd5e1", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                            {summarizeDeployOutput(entry)}
                          </div>
                          <button
                            onClick={() => {
                              openLastDeployDetail(userHistoryModal.serverId, userHistoryModal.username, entry.deploy_id);
                            }}
                            className="btn btn-ghost btn-sm"
                            style={{ color: "#00e5ff", borderColor: "#00e5ff44", flexShrink: 0 }}
                          >
                            Ver detalle
                          </button>
                        </div>
                      </div>
                    ))}
                  </div>
                  );
                })()}
              </div>
              <div className="modal-foot btn-row">
                <button onClick={() => { setUserHistoryModal(null); setHistorySearch(""); }} className="btn btn-secondary">
                  Cerrar
                </button>
              </div>
            </div>
          </div>
        )}

        {singleModal && (
          <div className="modal-overlay">
            <div className="modal">
              {(() => {
                const syncPreviewTotal = Number(singlePreview?.sync_preview_total || 0);
                const syncPreviewNewTotal = Array.isArray(singlePreview?.sync_preview_new_files)
                  ? singlePreview.sync_preview_new_files.length
                  : 0;
                const syncPreviewModifiedTotal = Math.max(0, syncPreviewTotal - syncPreviewNewTotal);
                const syncPreviewIncomplete = Boolean(singlePreview?.sync_preview_incomplete);
                const repoPendingChanges = Number(singlePreview?.total_changes || 0);
                const repoAlreadyUpdated = repoPendingChanges === 0;
                const isTrackingSingleDeploy = ["starting", "running"].includes(singleDeployRun?.status);
                const previewAligned = !syncPreviewIncomplete && syncPreviewTotal === 0;

                return (
                  <>
              <div className="modal-head">
                <span>⚡ Deploy — {singleModal.username}</span>
                <button
                  onClick={closeSingleDeployModal}
                  className="btn btn-ghost btn-sm"
                  disabled={isTrackingSingleDeploy}
                >
                  ✕
                </button>
              </div>
              <div className="modal-body">
                {singleModalLoading && (
                  <div className="alert alert-info" style={{ marginBottom: 12 }}>
                    Preparando preview y cargando historial del usuario...
                  </div>
                )}

                {!singleModalLoading && singleModalError && (
                  <div className="alert alert-danger" style={{ marginBottom: 12 }}>
                    {singleModalError}
                  </div>
                )}

                {singleDeployRun && (
                  <div style={{ marginBottom: 12, border: `1px solid ${singleDeployTone.border}`, background: singleDeployTone.background, borderRadius: 10, padding: 12 }}>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                      {singleDeployRun.deployId && (
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                          deploy #{singleDeployRun.deployId}
                        </span>
                      )}
                      <span className="tag" style={{ borderColor: singleDeployTone.border, color: singleDeployTone.color, background: singleDeployTone.background }}>
                        estado: {singleDeployTone.label}
                      </span>
                      <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                        ok: {singleDeployRun.okCount || 0}
                      </span>
                      <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                        error: {singleDeployRun.errorCount || 0}
                      </span>
                      {singleDeployRun.validationStatus && (
                        <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                          validación: {singleDeployRun.validationStatus}
                        </span>
                      )}
                      {singleDeployRun.rollbackStatus && (
                        <span className="tag" style={{ borderColor: "#a78bfa44", color: "#c4b5fd", background: "#a78bfa15" }}>
                          rollback: {singleDeployRun.rollbackStatus}
                        </span>
                      )}
                    </div>
                    <div style={{ fontSize: 11, color: singleDeployTone.color }}>
                      {singleDeployRun.message
                        || (isTrackingSingleDeploy
                          ? "El deploy se está monitoreando automáticamente hasta que termine."
                          : "Resultado del último deploy ejecutado desde este modal.")}
                    </div>
                    {(() => {
                      const lockResult = Array.isArray(singleDeployRun.results)
                        ? singleDeployRun.results.find((r) => r.virt_user === "_lock_" && r.status === "error")
                        : null;
                      if (!lockResult || isTrackingSingleDeploy) return null;
                      return (
                        <div style={{ marginTop: 8, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
                          <button
                            onClick={() => releaseDeployLockAndRetry(
                              singleModal.serverId,
                              singleModal.username,
                              singleDeployRun.mode || "deploy"
                            )}
                            className="btn btn-sm"
                            style={{
                              background: "#7c2d12",
                              color: "#fed7aa",
                              border: "1px solid #c2410c",
                              padding: "5px 12px",
                              fontSize: 11,
                              fontWeight: 700,
                            }}
                            title="Borra el lock del servidor y vuelve a lanzar el deploy"
                          >
                            🔓 Liberar lock y reintentar
                          </button>
                          <span style={{ fontSize: 10, color: "#94a3b8" }}>
                            Solo si confirmás que el deploy bloqueante no está corriendo.
                          </span>
                        </div>
                      );
                    })()}
                    {Array.isArray(singleDeployRun.results) && singleDeployRun.results.length > 0 && (
                      <div style={{ background: "#080b10", borderRadius: 8, maxHeight: 180, overflowY: "auto", marginTop: 10 }}>
                        {singleDeployRun.results.map((result) => (
                          <div
                            key={result.id}
                            style={{
                              padding: "6px 12px",
                              fontSize: 11,
                              display: "flex",
                              gap: 8,
                              borderBottom: "1px solid #1e2a3a15",
                              alignItems: "center",
                            }}
                          >
                            <span style={{ color: result.status === "ok" ? "#22d3a5" : result.status === "error" ? "#f43f5e" : "#fbbf24", flexShrink: 0 }}>
                              {result.status}
                            </span>
                            <span style={{ color: "#94a3b8", flexShrink: 0 }}>{result.virt_user}</span>
                            <span style={{ wordBreak: "break-word" }}>{String(result.output || "").split("\n")[0] || "Sin detalle"}</span>
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                )}
                {singlePreview && (
                  <>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 12 }}>
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#64748b", background: "#64748b15" }}>
                    baseline cliente: {singlePreview.current_commit || "—"}
                  </span>
                  <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                    repo actual: {singlePreview.new_commit || "—"}
                  </span>
                  {syncPreviewIncomplete && (
                    <span className="tag" style={{ borderColor: "#f43f5e44", color: "#fbbf24", background: "#fbbf2415" }}>
                      preview parcial
                    </span>
                  )}
                  {!previewAligned && (
                    <span className="tag" style={{ borderColor: "#fbbf2444", color: "#fbbf24", background: "#fbbf2415" }}>
                      {syncPreviewTotal} archivo(s) pendiente(s) para este cliente
                    </span>
                  )}
                  {!previewAligned && syncPreviewNewTotal > 0 && (
                    <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                      {syncPreviewNewTotal} nuevo(s)
                    </span>
                  )}
                  {!previewAligned && syncPreviewModifiedTotal > 0 && (
                    <span className="tag" style={{ borderColor: "#fb923c44", color: "#fb923c", background: "#fb923c15" }}>
                      {syncPreviewModifiedTotal} modificado(s)
                    </span>
                  )}
                </div>
                {previewAligned ? (
                  <div className="alert alert-success">✔ Este cliente ya está alineado con el contenido actual de test</div>
                ) : (
                  <>
                    {syncPreviewIncomplete && singlePreview.sync_preview_warning && (
                      <div className="alert alert-warn" style={{ fontSize: 11, marginBottom: 12 }}>
                        {singlePreview.sync_preview_warning}
                      </div>
                    )}
                    <div className={`alert ${repoAlreadyUpdated ? "alert-info" : "alert-warn"}`} style={{ fontSize: 11, marginBottom: 12 }}>
                      {repoAlreadyUpdated
                        ? "El repo del servidor ya está al día. Este preview compara la línea base guardada del cliente contra el commit actual del repo."
                        : `El repo del servidor todavía tiene ${repoPendingChanges} cambio(s) por traer desde git antes del deploy.`}
                    </div>
                  <FilterableFileList
                    files={singlePreview.sync_preview_files || []}
                    placeholder="Buscar archivo en el preview…"
                    maxHeight={260}
                    emptyMessage="No hay archivos pendientes."
                    renderItem={(file, index) => (
                      <div
                        key={index}
                        style={{
                          padding: "4px 12px",
                          fontSize: 11,
                          display: "flex",
                          gap: 8,
                          borderBottom: "1px solid #1e2a3a15",
                        }}
                      >
                        <span
                          style={{
                            color: file.kind === "new" ? "#22d3a5" : "#fbbf24",
                            flexShrink: 0,
                            fontSize: 10,
                          }}
                        >
                          {file.kind === "new" ? "[nuevo]" : "[modif]"}
                        </span>
                        <span style={{ wordBreak: "break-all" }}>{file.path}</span>
                      </div>
                    )}
                  />
                  </>
                )}
                <div style={{ marginTop: 14 }}>
                  <div style={{ fontSize: 12, fontWeight: 700, color: "#cbd5e1", marginBottom: 8 }}>Historial reciente del usuario</div>
                  {!singleHistory.length ? (
                    <div className="alert alert-info" style={{ fontSize: 11 }}>
                      No hay deploys previos registrados para este usuario.
                    </div>
                  ) : (
                    <div style={{ background: "#080b10", borderRadius: 8, maxHeight: 220, overflowY: "auto" }}>
                  {singleHistory.map((entry) => (
                        <div
                          key={entry.id}
                          style={{
                            padding: "8px 12px",
                            fontSize: 11,
                            display: "flex",
                            gap: 10,
                            alignItems: "center",
                            borderBottom: "1px solid #1e2a3a15",
                          }}
                        >
                          <span
                            className="tag"
                            style={{
                              borderColor: entry.result_status === "ok" ? "#22d3a544" : "#f43f5e44",
                              color: entry.result_status === "ok" ? "#22d3a5" : "#f43f5e",
                              background: entry.result_status === "ok" ? "#22d3a515" : "#f43f5e15",
                              flexShrink: 0,
                            }}
                          >
                            {entry.result_status}
                          </span>
                          <span style={{ color: "#cbd5e1", flexShrink: 0 }}>#{entry.deploy_id}</span>
                          <span style={{ color: "#94a3b8", flexShrink: 0 }}>{entry.branch || "main"}</span>
                          <span style={{ color: "#64748b", flexShrink: 0 }}>{fmtDate(entry.started_at || entry.result_created_at)}</span>
                          <span style={{ wordBreak: "break-word" }}>
                            {summarizeDeployOutput(entry)}
                          </span>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
                  </>
                )}
              </div>
              <div className="modal-foot single-deploy-foot">
                <button
                  onClick={closeSingleDeployModal}
                  className="btn single-deploy-foot-cancel"
                  disabled={isTrackingSingleDeploy}
                >
                  {isTrackingSingleDeploy ? "Esperando..." : "Cancelar"}
                </button>

                <div className="single-deploy-foot-actions">
                  {!singleModalLoading && !singlePreview && (
                    <button
                      onClick={() => loadSingleDeployModalData(singleModal.serverId, singleModal.username)}
                      className="btn btn-ghost btn-sm"
                      disabled={isTrackingSingleDeploy}
                    >
                      ↻ Reintentar preview
                    </button>
                  )}
                  <button
                    onClick={() => runFileDiagnostic(singleModal.serverId, singleModal.username)}
                    className="btn btn-sm single-deploy-action-info"
                    disabled={isTrackingSingleDeploy || singleModalLoading || fileDiagnosticLoading}
                    title="Compara archivos del repo vs los archivos del cliente y reporta diferencias"
                  >
                    {fileDiagnosticLoading ? "⟳ Analizando..." : "🔍 Diagnóstico"}
                  </button>
                  <button
                    onClick={confirmSingleResync}
                    className="btn btn-sm single-deploy-action-repair"
                    disabled={isTrackingSingleDeploy || singleModalLoading}
                    title="Re-sincroniza TODOS los archivos no protegidos (uso para clientes desactualizados que tienen archivos faltantes)"
                  >
                    {isTrackingSingleDeploy && singleDeployRun?.mode === "resync"
                      ? "⟳ Reparando..."
                      : "🔧 Reparar"}
                  </button>

                  <span className="single-deploy-foot-divider" aria-hidden="true" />

                  <button
                    onClick={() => confirmSingleDeploy(true)}
                    className="btn btn-sm single-deploy-action-dryrun"
                    disabled={isTrackingSingleDeploy || singleModalLoading || !singlePreview || previewAligned}
                    title={previewAligned ? "No hay archivos pendientes para este cliente" : "Simula el deploy sin tocar archivos"}
                  >
                    ⚙ Dry Run
                  </button>
                  <button
                    onClick={() => confirmSingleDeploy(false)}
                    className="btn btn-sm single-deploy-action-deploy"
                    disabled={isTrackingSingleDeploy || singleModalLoading || !singlePreview || previewAligned}
                    title={previewAligned ? "No hay archivos pendientes para este cliente" : "Ejecuta el deploy real"}
                  >
                    {isTrackingSingleDeploy && singleDeployRun?.mode !== "resync"
                      ? "⟳ Ejecutando..."
                      : "🚀 Confirmar deploy"}
                  </button>
                </div>
              </div>
                  </>
                );
              })()}
            </div>
          </div>
        )}

        {scanModal && (
          <div className="modal-overlay">
            <div className="modal">
              <div className="modal-head">
                <span>⟳ Scan — {scanModal.label}</span>
              </div>
              <div className="modal-body">
                <div className="field">
                  <label>Contraseña SSH</label>
                  <input
                    type="password"
                    value={scanPass}
                    onChange={(event) => setScanPass(event.target.value)}
                    onKeyDown={(event) => event.key === "Enter" && confirmScan()}
                    autoFocus
                    className="input"
                    placeholder="Vacío = usar guardada"
                  />
                </div>
                <div
                  style={{
                    background: "#131820",
                    border: "1px solid #1e2a3a",
                    borderRadius: 10,
                    padding: "10px 12px",
                    marginBottom: 12,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "space-between",
                    gap: 12,
                  }}
                >
                  <div>
                    <div style={{ fontSize: 12, fontWeight: 700 }}>Auto clonar repo si falta</div>
                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                      Usa la configuración GitHub guardada y la branch por defecto
                    </div>
                  </div>
                  <Toggle value={autoInit} onChange={setAutoInit} color="#22d3a5" />
                </div>
                <div className="alert alert-info" style={{ fontSize: 11 }}>
                  El repo compartido <strong>/opt/datacole-repo/</strong> se usa para todos los usuarios. Usa "⬇ Clonar repo" si aún no existe.
                </div>
              </div>
              <div className="modal-foot btn-row">
                <button onClick={() => setScanModal(null)} className="btn btn-secondary">
                  Cancelar
                </button>
                <button onClick={confirmScan} className="btn btn-primary">
                  ⟳ Escanear servidor
                </button>
              </div>
            </div>
          </div>
        )}

        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14, gap: 10 }}>
          <div>
            <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 800, fontSize: 20 }}>Servidores</div>
            <div style={{ fontSize: 11, color: "#64748b", marginTop: 2 }}>{servers.length} configurados</div>
          </div>
          {canWrite && (
            <button
              onClick={() => {
                setShowAdd(true);
                setEditing(null);
                resetForm();
              }}
              className="btn btn-primary"
              style={{ width: "auto", padding: "9px 16px", fontSize: 13 }}
            >
              + Agregar
            </button>
          )}
        </div>

        <div className="server-summary-grid">
          <div className="server-summary-card">
            <div className="server-summary-label">Servidores</div>
            <div className="server-summary-value">{servers.length}</div>
            <div className="server-summary-meta">infraestructura registrada</div>
          </div>
          <div className="server-summary-card">
            <div className="server-summary-label">Usuarios globales</div>
            <div className="server-summary-value">{overallCounters.total}</div>
            <div className="server-summary-meta">todos los usuarios detectados</div>
          </div>
          <div className="server-summary-card server-summary-card-active">
            <div className="server-summary-label">Activos</div>
            <div className="server-summary-value">{overallCounters.active}</div>
            <div className="server-summary-meta">habilitados para deploy</div>
          </div>
          <div className="server-summary-card server-summary-card-danger">
            <div className="server-summary-label">Excluidos</div>
            <div className="server-summary-value">{overallCounters.excluded}</div>
            <div className="server-summary-meta">fuera del flujo operativo</div>
          </div>
        </div>

        <div className="search-wrap">
          <input
            value={serverSearch}
            onChange={(event) => setServerSearch(event.target.value)}
            placeholder="🔍 Buscar servidor, usuario, dominio o ruta..."
            className="input"
          />
          {serverSearch && (
            <button onClick={() => setServerSearch("")} className="search-clear">
              ×
            </button>
          )}
        </div>

        {serverSearch && (
          <div style={{ fontSize: 11, color: "#64748b", marginTop: -8, marginBottom: 12 }}>
            {searchLoading
              ? "Buscando también por usuarios, dominios y rutas..."
              : `${filteredServers.length} servidor${filteredServers.length !== 1 ? "es" : ""} coincide${filteredServers.length !== 1 ? "n" : ""} con la búsqueda`}
          </div>
        )}

        {(showAdd || editing) && canWrite && (
          <div className="card" style={{ animation: "fadeIn .2s ease" }}>
            <div className="card-head">{editing ? "✏ Editar" : "+ Nuevo servidor"}</div>
            <div className="card-body">
              <div className="grid-2">
                {[
                  ["label", "Nombre", "text"],
                  ["host", "Host / IP", "text"],
                  ["port", "Puerto", "number"],
                  ["username", "Usuario SSH", "text"],
                ].map(([key, label, type]) => (
                  <div key={key} className="field">
                    <label>{label}</label>
                    <input
                      type={type}
                      value={form[key]}
                      onChange={(event) => setForm((prev) => ({ ...prev, [key]: event.target.value }))}
                      className="input"
                    />
                  </div>
                ))}
                <div className="field" style={{ gridColumn: "1/-1" }}>
                  <label>Base path</label>
                  <input
                    value={form.base_path}
                    onChange={(event) => setForm((prev) => ({ ...prev, base_path: event.target.value }))}
                    className="input"
                  />
                </div>
                <div className="field" style={{ gridColumn: "1/-1" }}>
                  <label>Notas</label>
                  <input
                    value={form.notes}
                    onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))}
                    className="input"
                    placeholder="Opcional"
                  />
                </div>
              </div>
              <div className="btn-row">
                <button
                  onClick={() => {
                    resetForm();
                    setShowAdd(false);
                    setEditing(null);
                  }}
                  className="btn btn-secondary"
                >
                  Cancelar
                </button>
                <button onClick={saveServer} className="btn btn-primary">
                  {editing ? "Guardar" : "Agregar"}
                </button>
              </div>
            </div>
          </div>
        )}

        {filteredServers.length === 0 && !showAdd && (
          <div style={{ textAlign: "center", padding: "40px 20px", color: "#64748b" }}>
            <div style={{ fontSize: 32, marginBottom: 10 }}>⬡</div>
            <div>Sin servidores</div>
          </div>
        )}

        {filteredServers.map((server) => {
          const searchMatch = searchMatches[server.id];
          const hasSearchHints = normalizedSearch && searchMatch;
          const serverUsers = users[server.id] || [];
          const isLoadingUsers = Boolean(loadingUsers[server.id]);
          const counters = getServerCounters(server);
          const localUserSearch = String(userSearch[server.id] || "").trim().toLowerCase();
          const activeStatusFilter = userStatusFilter[server.id] || "all";
          const visibleUsers = serverUsers.filter((user) => {
            const matchesSearch = getUserMatchReasons(user, localUserSearch).length > 0 || !localUserSearch;
            if (!matchesSearch) return false;

            const deployState = getUserDeployFilterState(user);
            if (activeStatusFilter === "failed") return deployState === "failed";
            if (activeStatusFilter === "success") return deployState === "success";
            if (activeStatusFilter === "no_history") return deployState === "no_history";
            return true;
          });
          const deployStateCounts = serverUsers.reduce((acc, user) => {
            const state = getUserDeployFilterState(user);
            acc.all += 1;
            if (state === "failed") acc.failed += 1;
            if (state === "success") acc.success += 1;
            if (state === "no_history") acc.no_history += 1;
            return acc;
          }, { all: 0, failed: 0, success: 0, no_history: 0 });

          return (
          <div key={server.id} className="server-card">
            <div
              className="server-header"
              onClick={() => toggleExpand(server.id)}
              onKeyDown={(event) => {
                if (event.key === "Enter" || event.key === " ") {
                  event.preventDefault();
                  toggleExpand(server.id);
                }
              }}
              role="button"
              tabIndex="0"
              aria-expanded={expanded === server.id}
              title={expanded === server.id ? "Cerrar detalles del servidor" : "Abrir detalles del servidor"}
            >
              <div className="server-dot" style={{ background: "#22d3a5", boxShadow: "0 0 6px #22d3a5" }} />
              <div className="server-info">
                <div className="server-name">{server.label}</div>
                <div className="server-host">
                  {server.username}@{server.host}:{server.port} · {server.base_path}
                </div>
                <div className="server-stats">
                  <span className="tag" style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12" }}>
                    usuarios: {counters.total}
                  </span>
                  <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                    activos: {counters.active}
                  </span>
                  <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                    excluidos: {counters.excluded}
                  </span>
                </div>
                {hasSearchHints && (
                  <div className="server-search-hints">
                    {searchMatch.direct_match && (
                      <span className="tag" style={{ borderColor: "#00e5ff44", color: "#00e5ff", background: "#00e5ff15" }}>
                        servidor
                      </span>
                    )}
                    {searchMatch.matched_usernames?.length > 0 && (
                      <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                        usuarios: {searchMatch.matched_usernames.join(", ")}
                      </span>
                    )}
                    {searchMatch.matched_domains?.length > 0 && (
                      <span className="tag" style={{ borderColor: "#a78bfa44", color: "#c4b5fd", background: "#a78bfa15" }}>
                        dominios: {searchMatch.matched_domains.join(", ")}
                      </span>
                    )}
                    {searchMatch.matched_paths?.length > 0 && (
                      <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                        rutas: {searchMatch.matched_paths.map((path) => path.replace(/^\/home\//, "~/")).join(", ")}
                      </span>
                    )}
                  </div>
                )}
              </div>
              <div className="server-actions" onClick={(event) => event.stopPropagation()}>
                {canWrite && (
                  <button
                    onClick={() => {
                      setScanModal(server);
                      setScanPass("");
                    }}
                    disabled={scanning === server.id}
                    className="btn btn-ghost btn-sm"
                    style={{ color: "#00e5ff", borderColor: "#00e5ff44" }}
                  >
                    {scanning === server.id ? (
                      <span style={{ display: "inline-block", animation: "spin 1s linear infinite" }}>↻</span>
                    ) : (
                      "⟳"
                    )}
                  </button>
                )}
                <button
                  onClick={() => toggleExpand(server.id)}
                  className="btn btn-ghost btn-sm"
                  style={{ color: expanded === server.id ? "#00e5ff" : "#64748b" }}
                >
                  {expanded === server.id ? "▲" : "▼"}
                </button>

                {(repoStatus[server.id]?.exists || server.repo_exists === 1) &&
                  (repoStatus[server.id]?.commit || server.repo_commit) && (
                    <span
                      className="tag"
                      style={{
                        borderColor: "#22d3a544",
                        color: "#22d3a5",
                        background: "#22d3a515",
                        fontSize: 9,
                      }}
                    >
                      repo ✓ {repoStatus[server.id]?.commit || server.repo_commit}
                    </span>
                  )}
                {canWrite && (
                  <button
                    onClick={() => {
                      setForm({
                        label: server.label,
                        host: server.host,
                        port: server.port,
                        username: server.username,
                        base_path: server.base_path,
                        notes: server.notes || "",
                      });
                      setEditing(server.id);
                      setShowAdd(true);
                    }}
                    className="btn btn-ghost btn-sm"
                  >
                    ✏
                  </button>
                )}
                {isSuperAdmin && (
                  <button
                    onClick={() => deleteServer(server)}
                    className="btn btn-ghost btn-sm"
                    style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}
                    disabled={deletingId === server.id}
                  >
                    {deletingId === server.id ? "…" : "✕"}
                  </button>
                )}
              </div>
            </div>

            {expanded === server.id && (
              <div style={{ borderTop: "1px solid #1e2a3a" }}>
                {isLoadingUsers ? (
                  <div className="server-users-loading">
                    <span className="server-users-spinner" />
                    <span>Cargando usuarios del servidor...</span>
                  </div>
                ) : !users[server.id] || users[server.id].length === 0 ? (
                  <div style={{ padding: "18px 16px", fontSize: 12, color: "#64748b" }}>
                    Sin usuarios. Haz click en ⟳ Scan.
                  </div>
                ) : (
                  <div>
                    <div style={{ padding: "10px 16px 4px" }}>
                      {(() => {
                        const hasRepo = repoStatus[server.id]?.exists ?? (server.repo_exists === 1);
                        const commit = repoStatus[server.id]?.commit || server.repo_commit || "";
                        const known = repoStatus[server.id] !== undefined || server.repo_exists !== undefined;

                        if (!known) return null;

                        return (
                          <div style={{ marginBottom: 8, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                            <span
                              className="tag"
                              style={{
                                borderColor: hasRepo ? "#22d3a544" : "#f43f5e44",
                                color: hasRepo ? "#22d3a5" : "#f43f5e",
                                background: hasRepo ? "#22d3a515" : "#f43f5e15",
                              }}
                            >
                              {hasRepo ? `✓ Repo: ${commit}` : "✗ Sin repo compartido"}
                            </span>
                            {!hasRepo && canWrite && (
                              <button
                                onClick={() => cloneRepo(server)}
                                disabled={cloning === server.id}
                                className="btn btn-ghost btn-sm"
                                style={{ color: "#22d3a5", borderColor: "#22d3a544", fontSize: 11 }}
                              >
                                {cloning === server.id ? "↻ Clonando..." : "⬇ Clonar repo"}
                              </button>
                            )}
                          </div>
                        );
                      })()}
                      {canWrite && (
                        <div className="server-action-toolbar">
                          <button
                            onClick={() => openActionModal(server, users[server.id] || [])}
                            className="btn btn-secondary"
                            style={{ color: "#00e5ff", borderColor: "#00e5ff44", background: "#00e5ff10", flex: "1 1 auto" }}
                            title="Abrir el modal de acciones para elegir uno o varios presets"
                          >
                            ⚡ Acciones del servidor
                          </button>
                        </div>
                      )}
                      <div className="search-wrap" style={{ marginBottom: 8 }}>
                        <input
                          value={userSearch[server.id] || ""}
                          onChange={(event) => setUserSearch((prev) => ({ ...prev, [server.id]: event.target.value }))}
                          placeholder="🔍 Buscar usuario, dominio o ruta..."
                          className="input input-sm"
                        />
                        {userSearch[server.id] && (
                          <button
                            onClick={() => setUserSearch((prev) => ({ ...prev, [server.id]: "" }))}
                            className="search-clear"
                          >
                            ×
                          </button>
                        )}
                      </div>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                        {[
                          ["all", `todos ${deployStateCounts.all}`, "#64748b"],
                          ["failed", `fallidos ${deployStateCounts.failed}`, "#f43f5e"],
                          ["success", `exitosos ${deployStateCounts.success}`, "#22d3a5"],
                          ["no_history", `sin historial ${deployStateCounts.no_history}`, "#94a3b8"],
                        ].map(([key, label, color]) => (
                          <button
                            key={key}
                            onClick={() => setUserStatusFilter((prev) => ({ ...prev, [server.id]: key }))}
                            className="btn btn-ghost btn-sm"
                            style={{
                              padding: "5px 10px",
                              fontSize: 10,
                              borderColor: activeStatusFilter === key ? `${color}66` : "#64748b33",
                              color: activeStatusFilter === key ? color : "#94a3b8",
                              background: activeStatusFilter === key ? `${color}18` : "#64748b10",
                            }}
                          >
                            {label}
                          </button>
                        ))}
                      </div>
                      {localUserSearch && (
                        <div style={{ fontSize: 11, color: "#64748b", marginBottom: 8 }}>
                          {visibleUsers.length} usuario{visibleUsers.length !== 1 ? "s" : ""} coincide
                          {visibleUsers.length !== 1 ? "n" : ""} por usuario, dominio o ruta
                        </div>
                      )}
                      {!localUserSearch && activeStatusFilter !== "all" && (
                        <div style={{ fontSize: 11, color: "#64748b", marginBottom: 8 }}>
                          {visibleUsers.length} usuario{visibleUsers.length !== 1 ? "s" : ""} en el filtro actual.
                        </div>
                      )}
                    </div>
                    {visibleUsers.length === 0 ? (
                      <div style={{ padding: "0 16px 16px", fontSize: 12, color: "#64748b" }}>
                        No hay coincidencias en este servidor para esa búsqueda.
                      </div>
                    ) : (
                      visibleUsers.map((user) => {
                        const matchReasons = getUserMatchReasons(user, localUserSearch);
                        const userSaving = Boolean(savingUser[`${server.id}:${user.username}`]);

                        return (
                        <div key={user.username} className="user-row">
                          <div className="user-row-top">
                            <div style={{ flex: 1, minWidth: 100 }}>
                              <div className="user-name">{highlightMatch(user.username, localUserSearch)}</div>
                              <div className="user-path">{highlightMatch(user.deploy_path, localUserSearch)}</div>
                              {(user.last_deploy_id || user.last_deploy_status || user.last_deploy_result_status) && (
                                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }}>
                                  {user.last_deploy_id ? (
                                    <button
                                      onClick={() => openLastDeployDetail(server.id, user.username, user.last_deploy_id)}
                                      className="btn btn-ghost btn-sm"
                                      style={{ borderColor: "#64748b44", color: "#cbd5e1", background: "#64748b12", fontSize: 9, padding: "3px 8px" }}
                                      title="Ver último resultado"
                                    >
                                      deploy #{user.last_deploy_id}
                                    </button>
                                  ) : null}
                                  {user.last_deploy_result_status ? (
                                    <span
                                      className="tag"
                                      style={{
                                        ...getDeployBadgeTone(user.last_deploy_result_status),
                                        fontSize: 9,
                                      }}
                                      title={`Resultado del último intento${user.last_deploy_branch ? ` · ${user.last_deploy_branch}` : ""}`}
                                    >
                                      usuario: {user.last_deploy_result_status}
                                    </span>
                                  ) : null}
                                  {user.last_deploy_status ? (
                                    <span
                                      className="tag"
                                      style={{
                                        ...getDeployBadgeTone(user.last_deploy_status),
                                        fontSize: 9,
                                      }}
                                      title={user.last_deploy_triggered_user ? `Lanzado por ${user.last_deploy_triggered_user}` : "Estado del deploy"}
                                    >
                                      deploy: {user.last_deploy_status}
                                    </span>
                                  ) : null}
                                  <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15", fontSize: 9 }}>
                                    {fmtDate(user.last_deploy_finished_at || user.last_deploy_started_at)}
                                  </span>
                                </div>
                              )}
                              {matchReasons.length > 0 && (
                                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }}>
                                  {matchReasons.map((reason) => (
                                    <span
                                      key={reason}
                                      className="tag"
                                      style={{
                                        borderColor: "#00e5ff33",
                                        color: "#00e5ff",
                                        background: "#00e5ff12",
                                        fontSize: 9,
                                      }}
                                    >
                                      coincide por {reason}
                                    </span>
                                  ))}
                                </div>
                              )}
                            </div>
                            <DomainCell
                              serverId={server.id}
                              user={user}
                              onUpdate={(newDomain) => patchUserInState(server.id, user.username, { domain: newDomain })}
                              isViewer={isViewer}
                              highlightTerm={localUserSearch}
                            />
                            <div className="user-actions">
                              <button
                                onClick={() => openUserHistoryModal(server.id, user.username)}
                                className="btn btn-ghost btn-xs"
                                style={{ color: "#cbd5e1", borderColor: "#64748b44" }}
                                title="Ver historial del usuario"
                              >
                                🕘 Historial
                              </button>
                            {!isViewer && !user.excluded && (
                              <>
                                <button
                                  onClick={() => deploySingleUser(server.id, user.username)}
                                  className="btn btn-ghost btn-xs"
                                  style={{ color: "#00e5ff", borderColor: "#00e5ff44" }}
                                >
                                  ⚡ Deploy
                                </button>
                                <button
                                  onClick={() =>
                                    user.domain && openActionModal(server, [user])
                                  }
                                  className="btn btn-ghost btn-xs"
                                  style={{ color: "#22d3a5", borderColor: "#22d3a544", opacity: user.domain ? 1 : 0.4 }}
                                  title={user.domain ? "Ejecutar acción HTTP" : "Configura el dominio primero"}
                                >
                                  ⚡ Acción
                                </button>
                              </>
                            )}
                            </div>
                          </div>
                          <div className="user-row-bottom">
                            <div className="user-toggle-group">
                              Deploy{" "}
                              <Toggle
                                value={user.include_deploy === 1 && user.excluded === 0}
                                color="#22d3a5"
                                disabled={user.excluded === 1 || isViewer || userSaving}
                                onChange={(value) => patchUser(server.id, user.username, { include_deploy: value ? 1 : 0 })}
                              />
                            </div>
                            <div className="user-toggle-group">
                              Excluir{" "}
                              <Toggle
                                value={user.excluded === 1}
                                color="#f43f5e"
                                disabled={isViewer || userSaving}
                                onChange={(value) =>
                                  patchUser(server.id, user.username, {
                                    excluded: value ? 1 : 0,
                                    include_deploy: value ? 0 : 1,
                                  })
                                }
                              />
                            </div>

                            {userSaving && (
                              <span
                                className="tag"
                                style={{
                                  display: "inline-flex",
                                  alignItems: "center",
                                  gap: 6,
                                  borderColor: "#00e5ff44",
                                  color: "#00e5ff",
                                  background: "#00e5ff12",
                                  fontSize: 10,
                                }}
                              >
                                <span
                                  style={{
                                    width: 11,
                                    height: 11,
                                    borderRadius: 999,
                                    border: "2px solid #1e2a3a",
                                    borderTopColor: "#00e5ff",
                                    animation: "spin .8s linear infinite",
                                    display: "inline-block",
                                  }}
                                /> Guardando…
                              </span>
                            )}

                            {isSuperAdmin && user.excluded !== 1 && (
                              <MigrationHookControls
                                serverId={server.id}
                                user={user}
                                onSaved={(nextMeta) => patchUserInState(server.id, user.username, { metadata_json: JSON.stringify(nextMeta) })}
                                toast={toast}
                              />
                            )}

                            <span style={{ fontSize: 10, color: "#475569", marginLeft: "auto" }}>{fmtDate(user.last_seen)}</span>
                          </div>
                        </div>
                        );
                      })
                    )}
                  </div>
                )}
              </div>
            )}
          </div>
          );
        })}
      </div>
    );
  }

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