Ctrl K
ComunidadGuías…

src/domain/vps-host-report.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Turns facts Relmio already holds (the connection probe and the cached n8n
// discovery) into short rows for the VPS pages. Nothing here contacts the
// server. A pass row repeats what Docker reported and proves nothing about a
// later build; a fail row marks a flow that certainly cannot work.
export const VPS_FLOWS = Object.freeze(["chatgpt", "assistant", "supergrok", "claude", "local-model"]);

const ROOT_ONLY_FLOWS = ["chatgpt", "assistant", "supergrok", "claude"];
// Pinned artifacts exist for x86_64 and arm64 only.
const PINNED_CPU_FLOWS = ["supergrok", "claude", "local-model"];
const UNPINNED_CPU_FLOWS = ["chatgpt", "assistant"];
const BUILD_FLOWS = ["chatgpt", "supergrok", "claude", "local-model"];
const BIND_MOUNT_FLOWS = ["chatgpt", "assistant", "claude"];
const OUTBOUND_FLOWS = ["chatgpt", "supergrok", "claude", "local-model"];
// Oldest versions seen in Relmio's tests and live checks. Older ones only warn.
const TESTED_DOCKER_MAJOR = 28;
const TESTED_COMPOSE = [2, 38];
const SMALL_MEMORY_BYTES = 2 * 1024 ** 3;

const row = (id, level, text, flows = []) => ({ id, level, text, flows });
const numbers = version => version.split(".").map(part => Number.parseInt(part, 10));
const olderThan = (version, [major, minor]) => {
  const [first, second = 0] = numbers(version);
  return first < major || (first === major && second < minor);
};

function loginRow({ username, loginUid, authentication, privilege }) {
  const signedIn = `Signed in as ${username} (UID ${loginUid}) with ${authentication === "agent" ? "the local SSH agent" : "a password"}`;
  return privilege === "root"
    ? row("login", "pass", `${signedIn}.`)
    : row("login", "fail", `${signedIn}; commands run through sudo -n. Only the self-hosted model setup accepts this session.`, ROOT_ONLY_FLOWS);
}

function osRow({ os, kernel }) {
  if (os === null) return row("os", "warn", "Docker did not report the operating system. Relmio is tested on Ubuntu 24.04.");
  const system = kernel === null ? os : `${os}, kernel ${kernel}`;
  return /^Ubuntu 24\.04(?:\.\d+)?(?: LTS)?$/u.test(os)
    ? row("os", "pass", `${system}.`)
    : row("os", "warn", `${system}. Relmio is untested on this system; it is tested on Ubuntu 24.04.`);
}

function cpuRows({ arch }) {
  if (arch === null) return [];
  if (arch === "x86_64" || arch === "amd64") return [row("cpu", "pass", "x86_64 CPU.")];
  if (arch === "aarch64" || arch === "arm64") return [row("cpu", "warn", "arm64 CPU (aarch64). Relmio is untested on arm64 servers.")];
  return [
    row("cpu", "fail", `${arch} CPU. Grok, Claude and the self-hosted model run only on x86_64 or arm64.`, PINNED_CPU_FLOWS),
    row("cpu-untested", "warn", `${arch} CPU. The ChatGPT bridge and the Assistant are untested on it and may not build or start.`, UNPINNED_CPU_FLOWS),
  ];
}

function versionRows({ dockerVersion, composeVersion, buildxVersion }) {
  const rows = [];
  if (dockerVersion !== null) {
    rows.push(numbers(dockerVersion)[0] < TESTED_DOCKER_MAJOR
      ? row("docker", "warn", `Docker Engine ${dockerVersion}. Relmio is untested on Docker Engine older than ${TESTED_DOCKER_MAJOR}.`)
      : row("docker", "pass", `Docker Engine ${dockerVersion}.`));
  }
  if (composeVersion !== null) {
    rows.push(olderThan(composeVersion, TESTED_COMPOSE)
      ? row("compose", "warn", `Docker Compose ${composeVersion}. Relmio is untested on Compose older than ${TESTED_COMPOSE.join(".")}.`)
      : row("compose", "pass", `Docker Compose ${composeVersion}.`));
  }
  if (typeof buildxVersion === "string") rows.push(row("buildx", "pass", `Buildx ${buildxVersion}.`));
  else if (buildxVersion === null) {
    rows.push(row("buildx", "warn", "Docker did not report a usable Buildx plugin. Compose can still build images. If a build fails, check the Buildx installation.", BUILD_FLOWS));
  }
  return rows;
}

function resourceRows({ memoryBytes, cpus }) {
  const parts = [
    ...(cpus === null ? [] : [`${cpus} ${cpus === 1 ? "CPU" : "CPUs"}`]),
    ...(memoryBytes === null ? [] : [`${(memoryBytes / 1024 ** 3).toFixed(1)} GB memory`]),
  ];
  if (parts.length === 0) return [];
  const small = (cpus !== null && cpus < 2) || (memoryBytes !== null && memoryBytes < SMALL_MEMORY_BYTES);
  return [small
    ? row("resources", "warn", `${parts.join(", ")}. This is a small server: image builds and first starts may be slow or fail.`)
    : row("resources", "pass", `${parts.join(", ")}.`)];
}

function securityRows({ security }) {
  if (security === null) return [];
  return [
    row("security", "pass", security.length === 0
      ? "Docker reported no security options."
      : `Docker security options: ${security.slice(0, 6).join(", ")}${security.length > 6 ? ", and more" : ""}.`),
    ...(security.includes("selinux") ? [row("security-selinux", "warn",
      "Docker runs with SELinux. The ChatGPT bridge, the Assistant's web search and Claude each mount a server folder, and SELinux may block that.", BIND_MOUNT_FLOWS)] : []),
    ...(security.includes("userns") ? [row("security-userns", "warn",
      "Docker remaps container users (userns-remap). File owners inside Relmio's containers will not match the server's, so setup may fail.")] : []),
  ];
}

function n8nRows(discovery, containerName) {
  if (!Array.isArray(discovery?.containers)) return [];
  const { containers } = discovery;
  if (containers.length === 0) {
    return [row("n8n", "warn", typeof discovery.noneFoundMessage === "string" && discovery.noneFoundMessage.length <= 240
      ? discovery.noneFoundMessage : "No running n8n container was found on this server.")];
  }
  const shown = containers.find(container => container.name === containerName) ??
    (containers.length === 1 ? containers[0] : null);
  if (!shown) return [row("n8n", "pass", `Found ${containers.length} running n8n containers.`)];
  // The image name is Docker's own text: show it only when it is short and plain.
  const image = typeof shown.image === "string" && /^[\x21-\x7e]{1,80}$/u.test(shown.image) ? ` (${shown.image})` : "";
  return [row("n8n", "pass", `Found n8n container ${shown.name}${image}.`)];
}

function networkRows(networks, networkName) {
  if (networkName === null || !Array.isArray(networks?.networks) || !networks.networks.includes(networkName)) return [];
  const support = networks.support?.[networkName];
  if (support?.supported === false && typeof support.message === "string") {
    return support.reason === "internal"
      ? [
        row("network", "fail", support.message, OUTBOUND_FLOWS),
        row("network-search", "warn", "The selected network is internal. The Assistant's optional web search cannot reach the internet from it.", ["assistant"]),
      ]
      : [row("network", "fail", support.message)];
  }
  const count = networks.networks.length;
  return [row("network", "pass", count > 1
    ? `n8n is on ${count} Docker networks; ${networkName} is selected.`
    : `Network ${networkName} is selected.`)];
}

export function buildVpsHostReport({ identity, host = null, discovery = null, networks, containerName = null, networkName = null }) {
  return {
    host,
    rows: [
      loginRow(identity),
      ...(host === null ? [] : [osRow(host), ...cpuRows(host), ...versionRows(host), ...resourceRows(host), ...securityRows(host)]),
      ...n8nRows(discovery, containerName),
      ...networkRows(networks, networkName),
    ],
  };
}