Files
headplane/app/utils/node-info.ts
T
eccgecko d5f76637f5 refactor: extract isNoExpiry utility for zero-time handling
Go's time.Time{} serialises to '0001-01-01T00:00:00Z' which headscale
may return instead of null for tagged nodes (juanfont/headscale#3170).

Commit extracts the the existing inline check into a shared function for reuse.

No behavior change — mapNodes() already handled both variants.
2026-04-11 00:00:37 +01:00

51 lines
1.6 KiB
TypeScript

import { HostInfo, Machine } from "~/types";
export interface PopulatedNode extends Machine {
routes: string[];
hostInfo?: HostInfo;
expired: boolean;
customRouting: {
exitRoutes: string[];
exitApproved: boolean;
subnetApprovedRoutes: string[];
subnetWaitingRoutes: string[];
};
}
const GO_ZERO_TIMES = new Set(["0001-01-01 00:00:00", "0001-01-01T00:00:00Z"]);
export function isNoExpiry(expiry: string | null | undefined): boolean {
return expiry == null || GO_ZERO_TIMES.has(expiry);
}
export function mapNodes(
nodes: Machine[],
stats?: Record<string, HostInfo> | undefined,
): PopulatedNode[] {
return nodes.map((node) => {
const customRouting = {
exitRoutes: node.availableRoutes.filter((route) => route === "::/0" || route === "0.0.0.0/0"),
exitApproved: node.approvedRoutes.some((route) => route === "::/0" || route === "0.0.0.0/0"),
subnetApprovedRoutes: node.approvedRoutes.filter(
(route) =>
route !== "::/0" && route !== "0.0.0.0/0" && node.availableRoutes.includes(route),
),
subnetWaitingRoutes: node.availableRoutes.filter(
(route) =>
route !== "::/0" && route !== "0.0.0.0/0" && !node.approvedRoutes.includes(route),
),
} satisfies PopulatedNode["customRouting"];
return {
...node,
routes: Array.from(new Set(node.availableRoutes)),
hostInfo: stats?.[node.nodeKey],
customRouting,
expired: isNoExpiry(node.expiry) ? false : new Date(node.expiry!).getTime() < Date.now(),
};
});
}
export function sortNodeTags(nodes: Machine[]): string[] {
return Array.from(new Set(nodes.flatMap((node) => node.tags))).sort();
}