import { CheckCircle, CircleSlash, Info, UserCircle } from "lucide-react"; import { useMemo, useState } from "react"; import { data } from "react-router"; import Attribute from "~/components/attribute"; import Button from "~/components/button"; import Card from "~/components/card"; import Chip from "~/components/chip"; import Link from "~/components/link"; import StatusCircle from "~/components/status-circle"; import Tooltip from "~/components/tooltip"; import { agentsContext, headscaleConfigContext, headscaleContext, headscaleLiveStoreContext, requestApiContext, } from "~/server/context"; import { nodesResource, usersResource } from "~/server/headscale/live-store"; import cn from "~/utils/cn"; import { getOSInfo, getTSVersion } from "~/utils/host-info"; import { isNoExpiry, mapNodes, sortAssignableTags } from "~/utils/node-info"; import { getUserDisplayName } from "~/utils/user"; import type { Route } from "./+types/machine"; import { mapTagsToComponents, uiTagsForNode } from "./components/machine-row"; import MenuOptions from "./components/menu"; import Routes from "./dialogs/routes"; import { machineAction } from "./machine-actions"; export async function loader({ request, params, context }: Route.LoaderArgs) { const agentsFeature = context.get(agentsContext); const getRequestApi = context.get(requestApiContext); const headscale = context.get(headscaleContext); const headscaleConfig = context.get(headscaleConfigContext); const headscaleLiveStore = context.get(headscaleLiveStoreContext); if (!params.id) { throw new Error("No machine ID provided"); } if (params.id.endsWith(".ico")) { throw data(null, { status: 204 }); } const magic = headscaleConfig.getMagicDNSBaseDomain(); const { api } = await getRequestApi(request); const [nodesSnap, usersSnap] = await Promise.all([ headscaleLiveStore.get(nodesResource, api), headscaleLiveStore.get(usersResource, api), ]); const nodes = nodesSnap.data; const users = usersSnap.data; const node = nodes.find((node) => node.id === params.id); if (node == null) { throw data(null, { status: 404 }); } const agents = agentsFeature.state === "enabled" ? agentsFeature.value : undefined; const [lookup, policyResult] = await Promise.allSettled([ agents?.lookup([node.nodeKey]), api.policy.get(), ]); const stats = lookup.status === "fulfilled" ? lookup.value : undefined; const [enhancedNode] = mapNodes([node], stats); const tags = [...node.tags].toSorted(); const supportsNodeOwnerChange = !headscale.capabilities.nodeOwnerIsImmutable; const supportsDisablingKeyExpiry = headscale.capabilities.keyExpiryCanBeDisabled; const agentSync = agents?.lastSync(); const policy = policyResult.status === "fulfilled" ? policyResult.value.policy : undefined; return { agent: agentSync ? { syncedAt: agentSync.syncedAt?.toISOString() ?? null, nodeCount: agentSync.nodeCount, nodeKey: agents?.agentNodeKey(), } : undefined, existingTags: sortAssignableTags(nodes, policy), magic, node: enhancedNode, stats: stats?.[enhancedNode.nodeKey], supportsNodeOwnerChange: supportsNodeOwnerChange, supportsDisablingKeyExpiry: supportsDisablingKeyExpiry, tags, users, }; } export const action = machineAction; export default function Page({ loaderData: { node, tags, users, magic, agent, stats, existingTags, supportsNodeOwnerChange, supportsDisablingKeyExpiry, }, }: Route.ComponentProps) { const [showRouting, setShowRouting] = useState(false); const uiTags = useMemo(() => { const tags = uiTagsForNode(node, agent?.nodeKey === node.nodeKey); return tags; }, [node, agent]); return (
All Machines / {node.givenName}
Status
Subnets let you expose physical network routes onto Tailscale.{" "} Learn More
Information about this machine’s network. Used to debug connection issues.
Addresses
Client Connectivity