> ## Documentation Index
> Fetch the complete documentation index at: https://metoro.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Install Hub

> Install the Metoro hub from pinned Helm chart values, Secrets, ingress, and first-login checks

export const usePlannerSelections = () => {
  const [selections, setSelections] = useState(defaultSelections);
  useEffect(() => {
    setSelections(readSelections());
    const handleChange = event => {
      setSelections({
        ...defaultSelections,
        ...event.detail
      });
    };
    window.addEventListener(eventName, handleChange);
    return () => window.removeEventListener(eventName, handleChange);
  }, []);
  return [selections, setSelections];
};

export const trustedCAInstructions = selections => {
  if (selections.tls !== "privateCa") {
    return null;
  }
  return {
    title: "Private CA bundle",
    body: <>
        Create this ConfigMap in the hub namespace before installing. This will allow metoro components
        to mount and use your CA bundle
      </>,
    command: `kubectl -n metoro-hub create configmap metoro-trusted-ca \\
  --from-file=ca.crt=/path/to/root-ca.pem`
  };
};

export const topologyText = selections => {
  if (selections.topology === "shared") {
    return "Install into the same cluster that will also run monitored workloads.";
  }
  return "Install into a dedicated hub cluster. Monitored clusters will be connected after the hub is ready.";
};

export const storageInstructions = selections => {
  if (selections.dependencies === "external") {
    return {
      title: "StorageClass is not used for PostgreSQL or ClickHouse",
      body: "Because PostgreSQL and ClickHouse are external, the hub values file does not need a StorageClass for those datastores. Storage ownership belongs to the teams operating those external systems."
    };
  }
  return {
    title: "Provision a retained StorageClass",
    body: <>
        Provision a StorageClass with <code>reclaimPolicy: Retain</code> named <code>metoro-retain</code>. This will be used for
        bundled PostgreSQL, ClickHouse, and Keeper PVCs. For production, <b>it is highly recommended</b> to use
        network-attachable storage rather than host-local storage so pods can
        move around the cluster without being tied to the node that first hosted
        the PVC. Use fast disks, ideally SSD-backed, because ClickHouse benefits
        from low-latency reads and writes. Additionally the disk should be able to grow without recreation
        to handle increases in telemetry data over time.
      </>
  };
};

export const storageKey = "metoro-onprem-installation-planner-v1";

export const startupOrderItems = selections => {
  if (selections.dependencies === "external") {
    return ["Validate external PostgreSQL and ClickHouse connectivity from hub pods.", "Run Temporal schema setup and start bundled Temporal against external PostgreSQL.", "Start apiserver and ingester against the external datastores.", "Start the self-monitoring exporter when self-monitoring is enabled."];
  }
  if (selections.dependencies === "preinstalledOperators") {
    return ["Use the preinstalled CloudNativePG and Altinity operators.", "Create bundled PostgreSQL and ClickHouse resources.", "Wait for PostgreSQL, ClickHouse, and Keeper readiness.", "Start bundled Temporal, apiserver, ingester, and the self-monitoring exporter."];
  }
  return ["Metoro can take around 10 minutes to become available for the first time", "You should first see postgres become available and scale to 3 replicas. This will allow temporal to come up.", "Clickhouse can take around 5-7 minutes to come up for the first time - you should see 3 chi and 3 chk nodes come up one a time.", "After clickhouse and postgres are ready, the rest of the applications will come up. You can manually restart them to speed things up."];
};

export const selectedDecisionItems = selections => [...sectionOrder.map(section => ({
  title: sections[section].title,
  label: optionLabel(section, selections[section])
})), {
  title: "Deployment hostname",
  label: cleanHostname(selections.deploymentHostname) || "Not set"
}, ...selections.network === "splitHosts" ? [{
  title: "Ingester hostname",
  label: cleanHostname(selections.ingesterHostname) || "Not set"
}] : []];

export const sections = {
  topology: {
    title: "Topology",
    prompt: "Where will the hub run?",
    options: [{
      id: "dedicated",
      label: "Dedicated hub cluster",
      recommended: true,
      description: "Run the hub in its own cluster and connect monitored clusters outbound to it."
    }, {
      id: "shared",
      label: "Shared workload cluster",
      description: "Run the hub in the same cluster as monitored workloads for smaller deployments."
    }]
  },
  network: {
    title: "Network access",
    prompt: "How will ingress be exposed?",
    options: [{
      id: "sharedHost",
      label: "Shared host",
      recommended: true,
      description: "Use one deploymentUrl host with path routing for UI/API and ingestion."
    }, {
      id: "splitHosts",
      label: "Split hosts",
      description: "Use separate apiserver and ingester hostnames when routing or ownership differs."
    }]
  },
  urlScheme: {
    title: "URL scheme",
    prompt: "Will the ingress use TLS?",
    options: [{
      id: "https",
      label: "HTTPS",
      recommended: true,
      description: "Use https:// URLs and configure TLS for the browser-facing hub host."
    }, {
      id: "http",
      label: "HTTP",
      description: "Use http:// URLs when the browser-facing hub host is not served with TLS."
    }]
  },
  tls: {
    title: "TLS and trusted CAs",
    prompt: "Which certificate trust path will be used?",
    options: [{
      id: "publicCa",
      label: "Public CA",
      recommended: true,
      description: "Use certificates already trusted by browsers and container images."
    }, {
      id: "privateCa",
      label: "Private CA bundle required",
      description: "Make the CA bundle available before installing the hub and monitored-cluster exporters."
    }]
  },
  authentication: {
    title: "Authentication",
    prompt: "How will users authenticate?",
    options: [{
      id: "oidcGroups",
      label: "OIDC with group mappings",
      recommended: true,
      description: "Prepare issuer, client, callback URL, scopes, group claim, and role mappings."
    }, {
      id: "emailPassword",
      label: "Email/password local users",
      description: "Use the bootstrap admin and locally managed users without an external identity provider."
    }]
  },
  dependencies: {
    title: "Dependencies",
    prompt: "Who will run PostgreSQL and ClickHouse?",
    options: [{
      id: "bundled",
      label: "Bundled dependencies and operators",
      recommended: true,
      description: "Let the hub chart install the datastore operators and manage PostgreSQL, ClickHouse, Temporal, and datastore resources."
    }, {
      id: "preinstalledOperators",
      label: "Bundled dependencies, operators managed in house",
      description: "Use centrally managed CloudNativePG and Altinity operators while the hub chart manages its datastore resources."
    }, {
      id: "external",
      label: "Externally managed ClickHouse and PostgreSQL",
      description: "Use customer-managed datastores with existing operational ownership."
    }]
  },
  clickhouseStorage: {
    title: "ClickHouse storage",
    prompt: "Where will ClickHouse keep telemetry data?",
    options: [{
      id: "hotPvcObjectStorage",
      label: "PVCs + object storage",
      recommended: true,
      description: "Use retained PVCs for about one day of hot data and object storage for longer-term telemetry."
    }, {
      id: "persistentVolumesOnly",
      label: "PVCs only",
      description: "Keep all ClickHouse telemetry on retained persistent volumes."
    }]
  }
};

export const sectionOrder = ["topology", "network", "urlScheme", "tls", "authentication", "dependencies", "clickhouseStorage"];

export const readSelections = () => {
  if (typeof window === "undefined") {
    return defaultSelections;
  }
  try {
    const stored = window.localStorage.getItem(storageKey);
    if (!stored) {
      return defaultSelections;
    }
    return {
      ...defaultSelections,
      ...JSON.parse(stored)
    };
  } catch {
    return defaultSelections;
  }
};

export const postInstallCommands = selections => {
  const commands = [{
    title: "Check Helm and pod readiness",
    command: "helm -n metoro-hub status metoro\nkubectl -n metoro-hub get pods"
  }];
  if (selections.dependencies !== "external") {
    commands.push({
      title: "Confirm datastore PVCs",
      command: "kubectl -n metoro-hub get pvc"
    });
  }
  commands.push({
    title: "Confirm ingress",
    command: "kubectl -n metoro-hub get ingress"
  });
  if (selections.network === "splitHosts") {
    commands.push({
      title: "Check readiness through split hosts",
      command: `export METORO_APISERVER_URL=${deploymentUrl(selections)}
export METORO_INGESTER_URL=${ingesterUrl(selections)}

curl -fsS "\${METORO_APISERVER_URL}/api/v1/readiness"
curl -fsS "\${METORO_INGESTER_URL}/ingest/api/v1/readiness"`
    });
  } else {
    commands.push({
      title: "Check readiness through the shared host",
      command: `export METORO_DEPLOYMENT_URL=${deploymentUrl(selections)}

curl -fsS "\${METORO_DEPLOYMENT_URL}/api/v1/readiness"
curl -fsS "\${METORO_DEPLOYMENT_URL}/ingest/api/v1/readiness"`
    });
  }
  commands.push({
    title: "Confirm bootstrap Secret",
    command: "kubectl -n metoro-hub get secret metoro-admin-bootstrap"
  });
  return commands;
};

export const optionLabel = (section, value) => {
  const config = sections[section];
  const option = config.options.find(item => item.id === value) || config.options.find(item => item.id === defaultSelections[section]);
  return option.label;
};

export const oidcCallbackUrl = selections => {
  return `${deploymentUrl(selections)}/api/v1/auth/oidc/callback`;
};

export const objectStorageInstructions = selections => {
  if (selections.clickhouseStorage !== "hotPvcObjectStorage") {
    return null;
  }
  if (selections.dependencies === "external") {
    return {
      title: "Confirm external ClickHouse object storage",
      body: <>
          Confirm that the externally managed ClickHouse service has the planned
          hot and cold storage tiers configured before installing. The hub chart
          will not create object-storage credentials or storage policies when
          ClickHouse is external.
        </>
    };
  }
  return {
    title: "Provision ClickHouse object storage",
    body: <>
        Provision an S3-compatible bucket or prefix for ClickHouse cold telemetry
        before installing. The endpoint must be reachable from the hub cluster,
        and the credentials should be scoped to that bucket or prefix.
      </>
  };
};

export const networkInstructions = selections => {
  const deploymentHost = deploymentHostname(selections);
  const ingestHost = ingesterHostname(selections);
  if (selections.network === "splitHosts") {
    return {
      title: "Split host ingress",
      body: <>
          Create external DNS records for both <code>{deploymentHost}</code> and{" "}
          <code>{ingestHost}</code>. Both records should point at the ingress
          controller address.
        </>,
      extra: ""
    };
  }
  return {
    title: "Point DNS at ingress",
    body: <>
        Create external DNS for <code>{deploymentHost}</code> and point it at the
        ingress controller address. E.g. <code>A {deploymentHost} - 111.111.111.111</code>
      </>,
    extra: ""
  };
};

export const ingesterUrl = selections => {
  return `${deploymentScheme(selections)}://${ingesterHostname(selections)}`;
};

export const ingesterHostname = selections => {
  if (selections.network !== "splitHosts") {
    return deploymentHostname(selections);
  }
  return cleanHostname(selections.ingesterHostname) || "CHANGE_ME_INGESTER_HOSTNAME";
};

export const firstLoginText = selections => {
  if (selections.authentication === "oidcGroups") {
    return {
      body: "Use the bootstrap admin for first access. The admin username is admin@metoro.io and the password is as above."
    };
  }
  return {
    body: "Use the bootstrap admin for first access and create or invite additional local users from the UI. Self-signup remains disabled unless you explicitly enable it in values."
  };
};

export const eventName = "metoro-onprem-installation-planner-change";

export const deploymentUrl = selections => {
  return `${deploymentScheme(selections)}://${deploymentHostname(selections)}`;
};

export const deploymentScheme = selections => {
  return selections.urlScheme === "http" ? "http" : "https";
};

export const deploymentHostname = selections => {
  return cleanHostname(selections.deploymentHostname) || "CHANGE_ME_METORO_HOSTNAME";
};

export const dependencyInstructions = selections => {
  if (selections.dependencies === "external") {
    return {
      title: "Externally managed PostgreSQL and ClickHouse",
      body: <>
          Disable bundled PostgreSQL and ClickHouse in the values file, leave
          bundled Temporal enabled, and point Temporal persistence at the
          external PostgreSQL Secret and endpoint. Prepare the external databases
          before installing: <code>metoro</code>, <code>temporal</code>, and{" "}
          <code>temporal_visibility</code> in PostgreSQL, plus the configured
          ClickHouse database.
        </>
    };
  }
  if (selections.dependencies === "preinstalledOperators") {
    return {
      title: "Bundled datastores with preinstalled operators",
      body: <>
          Keep bundled PostgreSQL, bundled ClickHouse, and
          bundled Temporal, but set the CloudNativePG and Altinity operator
          installs to <code>false</code>. The chart creates datastore resources
          and assumes the operators and CRDs already exist.
        </>
    };
  }
  return null;
};

export const defaultSelections = {
  topology: "dedicated",
  network: "sharedHost",
  urlScheme: "https",
  tls: "publicCa",
  authentication: "oidcGroups",
  dependencies: "bundled",
  clickhouseStorage: "hotPvcObjectStorage",
  deploymentHostname: "",
  ingesterHostname: ""
};

export const cleanHostname = value => {
  return String(value || "").trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
};

export const buildValuesSkeleton = selections => {
  const splitHosts = selections.network === "splitHosts";
  const privateCa = selections.tls === "privateCa";
  const oidc = selections.authentication === "oidcGroups";
  const external = selections.dependencies === "external";
  const preinstalledOperators = selections.dependencies === "preinstalledOperators";
  const ingressTls = deploymentScheme(selections) === "https";
  const objectStorage = selections.clickhouseStorage === "hotPvcObjectStorage" && !external;
  const apiHost = deploymentHostname(selections);
  const ingestHost = ingesterHostname(selections);
  const apiUrl = deploymentUrl(selections);
  const lines = ["# metoro-hub-values.yaml", "global:", "  # Optional: set this to a pull-through cache or mirror repository prefix.", "  # See the chart values.yaml for precedence details.", '  imageRepositoryOverride: ""', "", `deploymentUrl: ${apiUrl}`, ""];
  if (!external) {
    lines.push("storageClass:", "  name: metoro-retain", "");
  }
  lines.push("bootstrap:", "  organizationName: CHANGE_ME_ORGANIZATION_NAME", "", "ingress:", "  enabled: true", "  className: CHANGE_ME_INGRESS_CLASS");
  if (splitHosts) {
    lines.push('  host: ""', `  apiserverHost: ${apiHost}`, `  ingesterHost: ${ingestHost}`);
  } else {
    lines.push(`  host: ${apiHost}`);
  }
  lines.push("  tls:", `    enabled: ${ingressTls ? "true" : "false"}`);
  if (ingressTls) {
    lines.push("    secretName: metoro-tls");
  }
  if (oidc) {
    lines.push("", "auth:", "  oidc:", "    enabled: true", "    displayName: CHANGE_ME_IDP_NAME", '    issuerUrl: "https://CHANGE_ME_IDP/realms/CHANGE_ME_REALM"', "    clientId: metoro", "    clientSecretName: metoro-oidc-client-secret", "    organizationUUID: default-on-prem", `    redirectUrl: ${oidcCallbackUrl(selections)}`, '    scopes: "openid profile email groups"', "    groupRoleMappings:", '      - oidcGroup: "/metoro-admins"', '        metoroRole: "default-metoro-admin"', '      - oidcGroup: "/metoro-users"', '        metoroRole: "default-metoro-user"');
  }
  if (privateCa) {
    lines.push("", "trustedCAs:", "  enabled: true", "  existingConfigMap: metoro-trusted-ca", "  key: ca.crt", "", "self-monitoring-exporter:", "  trustedCAs:", "    enabled: true", "    existingConfigMap: metoro-trusted-ca", "    key: ca.crt", "    mountPath: /etc/ssl/certs/metoro-onprem-ca-bundle.crt");
  }
  if (external) {
    lines.push("", "postgres:", "  bundled:", "    enabled: false", "    operator:", "      install: false", "    backup:", "      enabled: false", "  external:", "    enabled: true", "    host: CHANGE_ME_POSTGRES_HOST", "    port: 5432", "    username: metoro", "    database: metoro", "    sslMode: require", "    passwordSecretName: metoro-postgres-external", "    passwordSecretKey: password", "", "clickhouse:", "  bundled:", "    enabled: false", "    operator:", "      install: false", "    objectStorage:", "      enabled: false", "    backup:", "      enabled: false", "  external:", "    enabled: true", "    url: clickhouse://CHANGE_ME_CLICKHOUSE_HOST:9440", "    username: metoro", "    database: default", "    passwordSecretName: metoro-clickhouse-external", "    passwordSecretKey: password", "    isOssClickhouse: true", "    ossClickhouseCluster: metoro", "    secure: true", "", "temporal:", "  bundled:", "    enabled: true", "  server:", "    config:", "      persistence:", "        default:", "          sql:", "            database: temporal", "            user: metoro", '            password: ""', "            existingSecret: metoro-postgres-external", "            secretKey: password", "            host: CHANGE_ME_POSTGRES_HOST", "            port: 5432", "        visibility:", "          sql:", "            database: temporal_visibility", "            user: metoro", '            password: ""', "            existingSecret: metoro-postgres-external", "            secretKey: password", "            host: CHANGE_ME_POSTGRES_HOST", "            port: 5432");
  } else {
    if (preinstalledOperators) {
      lines.push("", "postgres:", "  bundled:", "    operator:", "      install: false");
    }
    if (preinstalledOperators || objectStorage) {
      lines.push("", "clickhouse:", "  bundled:");
    }
    if (preinstalledOperators) {
      lines.push("    operator:", "      install: false");
    }
    if (objectStorage) {
      lines.push("    objectStorage:", "      enabled: true", "      provider: s3", "      existingSecret:", "        name: metoro-clickhouse-object-storage", "      s3:", "        endpoint: https://CHANGE_ME_S3_ENDPOINT/clickhouse-cold", '        region: ""');
    }
  }
  return lines.join("\n");
};

export const buildSecretSections = selections => {
  const external = selections.dependencies === "external";
  const oidc = selections.authentication === "oidcGroups";
  const ingressTls = deploymentScheme(selections) === "https";
  const objectStorage = selections.clickhouseStorage === "hotPvcObjectStorage" && !external;
  const sections = [{
    title: "Image pull Secret",
    description: "Gives the hub workloads access to Metoro images. You will have been provided this by your Metoro rep.",
    command: `kubectl -n metoro-hub create secret docker-registry metoro-image-pull-secret \\
  --docker-server=quay.io \\
  --docker-username=CHANGE_ME \\
  --docker-password=CHANGE_ME`
  }, {
    title: "Application signing Secret",
    description: "Provides a random AUTH_SECRET used by the application to sign JWTs.",
    command: `kubectl -n metoro-hub create secret generic metoro-auth-secret \\
  --from-literal=AUTH_SECRET='CHANGE_ME_RANDOM_SECRET'`
  }];
  if (oidc) {
    sections.push({
      title: "OIDC client Secret",
      description: "The OIDC client secret that allows metoro to communicate with the OIDC server.",
      command: `kubectl -n metoro-hub create secret generic metoro-oidc-client-secret \\
  --from-literal=client-secret='CHANGE_ME_OIDC_CLIENT_SECRET'`
    });
  }
  if (ingressTls) {
    sections.push({
      title: "Ingress TLS Secret",
      description: "Provides the certificate and private key for the metoro ingress.",
      command: `kubectl -n metoro-hub create secret tls metoro-tls \\
  --cert=/path/to/tls.crt \\
  --key=/path/to/tls.key`
    });
  }
  if (external) {
    sections.push({
      title: "External PostgreSQL password Secret",
      description: "Required when PostgreSQL is managed outside the chart.",
      command: `kubectl -n metoro-hub create secret generic metoro-postgres-external \\
  --from-literal=password='CHANGE_ME_POSTGRES_PASSWORD'`
    }, {
      title: "External ClickHouse password Secret",
      description: "Required when ClickHouse is managed outside the chart.",
      command: `kubectl -n metoro-hub create secret generic metoro-clickhouse-external \\
  --from-literal=password='CHANGE_ME_CLICKHOUSE_PASSWORD'`
    });
  } else {
    sections.push({
      title: "Bundled PostgreSQL application Secret",
      description: "A randomly generated password for the metoro user to access postgres.",
      command: `kubectl -n metoro-hub create secret generic metoro-postgresql-app \\
  --from-literal=username=metoro \\
  --from-literal=password='CHANGE_ME_RANDOM_POSTGRES_PASSWORD'`
    }, {
      title: "Bundled ClickHouse credentials Secret",
      description: "A randomly generated password for the metoro user to access Clickhouse.",
      command: `kubectl -n metoro-hub create secret generic metoro-clickhouse-credentials \\
  --from-literal=password='CHANGE_ME_CLICKHOUSE_PASSWORD'`
    });
  }
  if (objectStorage) {
    sections.push({
      title: "ClickHouse object-storage Secret",
      description: "Used by ClickHouse to access S3-compatible object storage for cold telemetry.",
      command: `kubectl -n metoro-hub create secret generic metoro-clickhouse-object-storage \\
  --from-literal=access_key_id='CHANGE_ME_ACCESS_KEY_ID' \\
  --from-literal=secret_access_key='CHANGE_ME_SECRET_ACCESS_KEY'`
    });
  }
  return sections;
};

export const buildPrerequisiteItems = selections => {
  const items = ["Helm 3 installed on the machine running the install.", "kubectl access to the target cluster and permission to create namespace-scoped Secrets, ConfigMaps, workloads, Services, Ingresses, and PVCs.", "An ingress controller already running in the target cluster.", "DNS control for the browser-facing hostnames used by the deployment."];
  if (selections.dependencies !== "external") {
    items.push("The ability to create a retained, network-attachable, SSD-backed StorageClass for bundled PostgreSQL, ClickHouse, and Keeper PVCs.");
  }
  if (selections.authentication === "oidcGroups") {
    items.push("An OIDC provider reachable from both browsers and hub pods, plus an OIDC client configured for the deployment callback URL.");
  }
  if (selections.tls === "privateCa") {
    items.push("The PEM CA bundle used by private PKI endpoints, ready to create as a ConfigMap before installing.");
  }
  if (selections.dependencies === "preinstalledOperators") {
    items.push("CloudNativePG and Altinity ClickHouse operators and CRDs preinstalled by the platform team.");
  }
  if (selections.dependencies === "external") {
    items.push("Externally managed PostgreSQL and ClickHouse endpoints reachable from the hub cluster.");
  } else if (selections.clickhouseStorage === "hotPvcObjectStorage") {
    items.push("S3-compatible object storage reachable from the hub cluster for bundled ClickHouse cold telemetry.");
  }
  return items;
};

export const authInstructions = selections => {
  if (selections.authentication === "oidcGroups") {
    return {
      title: "OIDC with group mappings",
      body: <>
          Provision the OIDC client for Metoro by following the{" "}
          <a href="/docs/on-premises/10.x/installation/oidc">OIDC setup guide</a>{" "}
          before installing. Use it to configure the callback URL, scopes, group
          claim, role mappings, and client Secret referenced by the values file.
        </>
    };
  }
  return {
    title: "Local email/password login",
    body: "No OIDC client Secret is needed. The install relies on the bootstrap admin Secret generated by the chart, and additional users are managed locally after first login."
  };
};

export const DecisionsCard = ({selections, includePlanningLink = false}) => <>
    <div>
      <p>
        These choices are saved in this browser.
        {includePlanningLink ? <>
            {" "}
            Change them on the{" "}
            <a href="/docs/on-premises/10.x/installation/planning">
              Installation Planning page
            </a>
            .
          </> : " They will carry forward to Install Hub."}
      </p>
    </div>

    <ul>
      {selectedDecisionItems(selections).map(item => <li key={item.title}>
          <strong>{item.title}:</strong> {item.label}
        </li>)}
    </ul>
  </>;

export const CommandBlock = ({children}) => <pre>
    <code>{children}</code>
  </pre>;

export const InstallHubValuesSkeleton = () => {
  const [selections] = usePlannerSelections();
  const values = buildValuesSkeleton(selections);
  return <>
      <p>
        Fill in the <code>CHANGE_ME</code> values and keep credentials in
        Kubernetes Secrets.
      </p>
      <CommandBlock>{values}</CommandBlock>
    </>;
};

export const InstallHubStorageClass = () => {
  const [selections] = usePlannerSelections();
  const storage = storageInstructions(selections);
  return <>
      <h3>{storage.title}</h3>
      <p>{storage.body}</p>
    </>;
};

export const InstallHubStartupOrder = () => {
  const [selections] = usePlannerSelections();
  const items = startupOrderItems(selections);
  return <ul>
      {items.map(item => <li key={item}>{item}</li>)}
    </ul>;
};

export const InstallHubSelectedDecisions = () => {
  const [selections] = usePlannerSelections();
  return <DecisionsCard selections={selections} includePlanningLink />;
};

export const InstallHubSecrets = () => {
  const [selections] = usePlannerSelections();
  const secretSections = buildSecretSections(selections);
  return <>
      {secretSections.map(section => <section key={section.title}>
          <h3>{section.title}</h3>
          <p>{section.description}</p>
          <CommandBlock>{section.command}</CommandBlock>
        </section>)}
    </>;
};

export const InstallHubPrerequisites = () => {
  const [selections] = usePlannerSelections();
  const items = buildPrerequisiteItems(selections);
  return <>
      <p>{topologyText(selections)}</p>

      <ul>
        {items.map(item => <li key={item}>{item}</li>)}
      </ul>
    </>;
};

export const InstallHubPostInstallChecks = () => {
  const [selections] = usePlannerSelections();
  const commands = postInstallCommands(selections);
  return <>
      {commands.map(item => <section key={item.title}>
          <h3>{item.title}</h3>
          <CommandBlock>{item.command}</CommandBlock>
        </section>)}
    </>;
};

export const InstallHubNetwork = () => {
  const [selections] = usePlannerSelections();
  const network = networkInstructions(selections);
  const auth = authInstructions(selections);
  const dependency = dependencyInstructions(selections);
  const trustedCA = trustedCAInstructions(selections);
  const objectStorage = objectStorageInstructions(selections);
  return <>
      {[network, trustedCA, objectStorage, auth, dependency].filter(Boolean).map(section => <section key={section.title}>
            <h3>{section.title}</h3>
            <p>{section.body}</p>
            {section.extra ? <p>{section.extra}</p> : null}
            {section.command ? <CommandBlock>{section.command}</CommandBlock> : null}
          </section>)}
    </>;
};

export const InstallHubNamespace = () => <>
    <p>
      Use <code>metoro-hub</code> as the hub namespace. This keeps Secrets,
      ConfigMaps, and operational runbooks consistent across installations.
    </p>
    <CommandBlock>{`kubectl create namespace metoro-hub`}</CommandBlock>
  </>;

export const LogIntoQuay = () => <>
      <p>
        Log into quay to be able to access the oci helm chart. You should use the credentials provided by your Metoro rep.
      </p>
      <CommandBlock>{`helm registry login quay.io`}</CommandBlock>
    </>;

export const InstallHubHelmInstall = () => <>
    <p>
      Install from the published chart and pin an explicit chart version. The
      Helm release name must be <code>metoro</code>; the chart fails fast for any other
      release name.
    </p>
    <CommandBlock>{`helm upgrade --install metoro oci://quay.io/metoro/charts/metoro-onprem \\
  --namespace metoro-hub \\
  --version 10.4.0 \\
  --values metoro-hub-values.yaml`}</CommandBlock>
  </>;

export const InstallHubFirstLogin = () => {
  const [selections] = usePlannerSelections();
  const text = firstLoginText(selections);
  return <>
      <p>
        The chart creates <code>metoro-admin-bootstrap</code> with a generated
        password and the organization name from{" "}
        <code>bootstrap.organizationName</code>. Retrieve the generated password:
      </p>
      <CommandBlock>{`kubectl -n metoro-hub get secret metoro-admin-bootstrap \\
  -o jsonpath='{.data.password}' | base64 -d; echo`}</CommandBlock>
      <p>{text.body}</p>
      <p>
        After the hub is ready, head to alerts and hit sync alerts to get predefined alerts running. Do the same for dashboards. Now you can connect monitored clusters from the Metoro UI.
      </p>
    </>;
};

<Info>
  The install steps, Secrets, values, and checks below use the decisions saved in Installation Planning.
</Info>

<Card title="Selected plan" icon="sliders">
  <InstallHubSelectedDecisions />
</Card>

<Steps>
  <Step title="Confirm prerequisites">
    <InstallHubPrerequisites />
  </Step>

  <Step title="Create the namespace">
    <InstallHubNamespace />
  </Step>

  <Step title="Prepare network, auth, and dependencies">
    <InstallHubNetwork />
  </Step>

  <Step title="Create required Secrets">
    The chart references existing Kubernetes Secrets for credentials. Do not put secret values into the Helm values file.

    <InstallHubSecrets />
  </Step>

  <Step title="Prepare storage">
    <InstallHubStorageClass />
  </Step>

  <Step title="Create the values file">
    Save the values content as `metoro-hub-values.yaml`.

    <InstallHubValuesSkeleton />
  </Step>

  <Step title="Log into quay">
    <LogIntoQuay />
  </Step>

  <Step title="Install with Helm">
    <InstallHubHelmInstall />
  </Step>

  <Step title="Wait for startup">
    <InstallHubStartupOrder />
  </Step>

  <Step title="Run post-install checks">
    <InstallHubPostInstallChecks />
  </Step>

  <Step title="Log in">
    <InstallHubFirstLogin />
  </Step>
</Steps>
