"use client";
import { useTranslation } from "react-i18next";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { CardContent } from "@kandev/ui/card";
import { IconLoader2 } from "@tabler/icons-react";
import type { SSHReachabilityRecord, SSHReachabilityReason } from "@/lib/types/http-ssh";
import { formatRelative } from "@/lib/i18n/formats";
import { useSSHReachability } from "@/hooks/domains/settings/use-ssh-reachability";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/settings-card-header";
import { settingsActionClassName } from "@/components/settings/settings-control";
export interface SSHReachabilityCardProps {
executorId: string;
}
const STALE_INTERVAL_MULTIPLIER = 3;
/**
* A record older than three probe intervals is stale rather than current.
* Disabled probing has no cadence to measure against, and a never-probed
* record has nothing to be stale relative to, so both read as not-stale.
*/
export function isReachabilityStale(
checkedAt: string | null,
probeIntervalSeconds: number,
probingEnabled: boolean,
now: number = Date.now(),
): boolean {
if (!probingEnabled || !checkedAt || probeIntervalSeconds <= 0) return false;
const checkedMs = Date.parse(checkedAt);
if (Number.isNaN(checkedMs)) return false;
return now - checkedMs > probeIntervalSeconds * 1000 * STALE_INTERVAL_MULTIPLIER;
}
export function reachabilityReasonKey(reason: SSHReachabilityReason): string | null {
switch (reason) {
case "config":
return "executors:sshReachabilityReasonConfig";
case "timeout":
return "executors:sshReachabilityReasonTimeout";
case "host_key":
return "executors:sshReachabilityReasonHostKey";
case "auth":
return "executors:sshReachabilityReasonAuth";
case "network":
return "executors:sshReachabilityReasonNetwork";
case "unknown":
return "executors:sshReachabilityReasonUnknown";
default:
return null;
}
}
export function reachabilityStateKey(state: SSHReachabilityRecord["state"]): string {
switch (state) {
case "reachable":
return "executors:sshReachabilityStateReachable";
case "unreachable":
return "executors:sshReachabilityStateUnreachable";
default:
return "executors:sshReachabilityStateUnknown";
}
}
/**
* Renders the most recent SSH reachability record for one executor: state,
* probed host, failure reason/message, consecutive-failure count, and the
* age of the last completed and last successful probe. Never fetches
* directly into local-only state — every fetch result is written through the
* store so a live executor.reachability.changed push and this card's own
* refresh share one reconciliation path.
*/
export function SSHReachabilityCard({ executorId }: SSHReachabilityCardProps) {
const { t } = useTranslation();
const { record, loadError, probing, probeNow, now } = useSSHReachability(executorId);
return (
{t("executors:sshReachabilityLoadFailed")}
); } return null; } const stale = isReachabilityStale( record.checked_at, record.probe_interval_seconds, record.probing_enabled, now, ); const reasonKey = record.reason ? reachabilityReasonKey(record.reason) : null; return ({t("executors:sshReachabilityHost", { host: record.host })}
) : null} {record.state === "unreachable" && reasonKey ? ({t(reasonKey)}
) : null} {record.state === "unreachable" && record.message ? ({record.message}
) : null}{t("executors:sshReachabilityConsecutiveFailures", { count: record.consecutive_failures })}
{record.checked_at ? t("executors:sshReachabilityAgeLastProbe", { age: formatRelative(record.checked_at) }) : t("executors:sshReachabilityNeverProbed")}
{record.last_success_at ? t("executors:sshReachabilityAgeLastSuccess", { age: formatRelative(record.last_success_at), }) : t("executors:sshReachabilityNoSuccess")}
{!record.probing_enabled ? ({t("executors:sshReachabilityProbingOff")}
) : null}