diff --git a/app/routes/machines/components/machine-row.tsx b/app/routes/machines/components/machine-row.tsx
index 489deac..d6bc4e5 100644
--- a/app/routes/machines/components/machine-row.tsx
+++ b/app/routes/machines/components/machine-row.tsx
@@ -13,7 +13,7 @@ import { TailscaleSSHTag } from "~/components/tags/TailscaleSSH";
import type { User } from "~/types";
import cn from "~/utils/cn";
import * as hinfo from "~/utils/host-info";
-import type { PopulatedNode } from "~/utils/node-info";
+import { isNoExpiry, type PopulatedNode } from "~/utils/node-info";
import { formatTimeDelta } from "~/utils/time";
import toast from "~/utils/toast";
import { getUserDisplayName } from "~/utils/user";
@@ -156,7 +156,7 @@ export function uiTagsForNode(node: PopulatedNode, isAgent?: boolean) {
uiTags.push("expired");
}
- if (node.expiry === null) {
+ if (!node.expired && isNoExpiry(node.expiry)) {
uiTags.push("no-expiry");
}
diff --git a/app/routes/machines/machine.tsx b/app/routes/machines/machine.tsx
index 3df43ac..3b6dd9e 100644
--- a/app/routes/machines/machine.tsx
+++ b/app/routes/machines/machine.tsx
@@ -12,7 +12,7 @@ import Tooltip from "~/components/tooltip";
import { nodesResource, usersResource } from "~/server/headscale/live-store";
import cn from "~/utils/cn";
import { getOSInfo, getTSVersion } from "~/utils/host-info";
-import { mapNodes, sortNodeTags } from "~/utils/node-info";
+import { isNoExpiry, mapNodes, sortNodeTags } from "~/utils/node-info";
import { getUserDisplayName } from "~/utils/user";
import type { Route } from "./+types/machine";
@@ -281,7 +281,7 @@ export default function Page({
/>
{magic ? (
diff --git a/app/utils/node-info.ts b/app/utils/node-info.ts
index f26d096..701a3f9 100644
--- a/app/utils/node-info.ts
+++ b/app/utils/node-info.ts
@@ -12,6 +12,11 @@ export interface PopulatedNode extends Machine {
};
}
+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 | undefined,
@@ -35,12 +40,7 @@ export function mapNodes(
routes: Array.from(new Set(node.availableRoutes)),
hostInfo: stats?.[node.nodeKey],
customRouting,
- expired:
- node.expiry === "0001-01-01 00:00:00" ||
- node.expiry === "0001-01-01T00:00:00Z" ||
- node.expiry === null
- ? false
- : new Date(node.expiry).getTime() < Date.now(),
+ expired: isNoExpiry(node.expiry) ? false : new Date(node.expiry!).getTime() < Date.now(),
};
});
}
diff --git a/tests/unit/utils/node-info.test.ts b/tests/unit/utils/node-info.test.ts
new file mode 100644
index 0000000..e8e3cb0
--- /dev/null
+++ b/tests/unit/utils/node-info.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, test } from "vitest";
+
+import { isNoExpiry } from "~/utils/node-info";
+
+describe("isNoExpiry", () => {
+ test("returns true for null", () => {
+ expect(isNoExpiry(null)).toBe(true);
+ });
+
+ test("returns true for undefined", () => {
+ expect(isNoExpiry(undefined)).toBe(true);
+ });
+
+ test("returns true for Go zero-time ISO format", () => {
+ expect(isNoExpiry("0001-01-01T00:00:00Z")).toBe(true);
+ });
+
+ test("returns true for Go zero-time space format", () => {
+ expect(isNoExpiry("0001-01-01 00:00:00")).toBe(true);
+ });
+
+ test("returns false for a real future expiry", () => {
+ expect(isNoExpiry("2030-01-01T00:00:00Z")).toBe(false);
+ });
+
+ test("returns false for a real past expiry", () => {
+ expect(isNoExpiry("2020-01-01T00:00:00Z")).toBe(false);
+ });
+});