diff --git a/messages/en-US.json b/messages/en-US.json
index c4990aae..734466b1 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1909,5 +1909,7 @@
"required": "Required",
"domainSettingsUpdated": "Domain settings updated successfully",
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
- "loadingDNSRecords": "Loading DNS records..."
+ "loadingDNSRecords": "Loading DNS records...",
+ "olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
+ "client": "Client"
}
diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts
index ff03b2e0..209b54b4 100644
--- a/server/routers/client/listClients.ts
+++ b/server/routers/client/listClients.ts
@@ -1,4 +1,4 @@
-import { db } from "@server/db";
+import { db, olms } from "@server/db";
import {
clients,
orgs,
@@ -16,6 +16,67 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
+import NodeCache from "node-cache";
+import semver from "semver";
+
+const olmVersionCache = new NodeCache({ stdTTL: 3600 });
+
+async function getLatestOlmVersion(): Promise {
+ try {
+ const cachedVersion = olmVersionCache.get("latestOlmVersion");
+ if (cachedVersion) {
+ return cachedVersion;
+ }
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
+
+ const response = await fetch(
+ "https://api.github.com/repos/fosrl/olm/tags",
+ {
+ signal: controller.signal
+ }
+ );
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ logger.warn(
+ `Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
+ );
+ return null;
+ }
+
+ const tags = await response.json();
+ if (!Array.isArray(tags) || tags.length === 0) {
+ logger.warn("No tags found for Olm repository");
+ return null;
+ }
+
+ const latestVersion = tags[0].name;
+
+ olmVersionCache.set("latestOlmVersion", latestVersion);
+
+ return latestVersion;
+ } catch (error: any) {
+ if (error.name === "AbortError") {
+ logger.warn(
+ "Request to fetch latest Olm version timed out (1.5s)"
+ );
+ } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
+ logger.warn(
+ "Connection timeout while fetching latest Olm version"
+ );
+ } else {
+ logger.warn(
+ "Error fetching latest Olm version:",
+ error.message || error
+ );
+ }
+ return null;
+ }
+}
+
const listClientsParamsSchema = z
.object({
@@ -50,10 +111,12 @@ function queryClients(orgId: string, accessibleClientIds: number[]) {
megabytesOut: clients.megabytesOut,
orgName: orgs.name,
type: clients.type,
- online: clients.online
+ online: clients.online,
+ olmVersion: olms.version
})
.from(clients)
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
+ .leftJoin(olms, eq(clients.clientId, olms.clientId))
.where(
and(
inArray(clients.clientId, accessibleClientIds),
@@ -77,12 +140,20 @@ async function getSiteAssociations(clientIds: number[]) {
.where(inArray(clientSites.clientId, clientIds));
}
+type OlmWithUpdateAvailable = Awaited>[0] & {
+ olmUpdateAvailable?: boolean;
+};
+
+
export type ListClientsResponse = {
- clients: Array>[0] & { sites: Array<{
- siteId: number;
- siteName: string | null;
- siteNiceId: string | null;
- }> }>;
+ clients: Array>[0] & {
+ sites: Array<{
+ siteId: number;
+ siteName: string | null;
+ siteNiceId: string | null;
+ }>
+ olmUpdateAvailable?: boolean;
+ }>;
pagination: { total: number; limit: number; offset: number };
};
@@ -206,6 +277,43 @@ export async function listClients(
sites: sitesByClient[client.clientId] || []
}));
+ const latestOlVersionPromise = getLatestOlmVersion();
+
+ const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
+ (client) => {
+ const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
+ // Initially set to false, will be updated if version check succeeds
+ OlmWithUpdate.olmUpdateAvailable = false;
+ return OlmWithUpdate;
+ }
+ );
+
+ // Try to get the latest version, but don't block if it fails
+ try {
+ const latestOlVersion = await latestOlVersionPromise;
+
+ if (latestOlVersion) {
+ olmsWithUpdates.forEach((client) => {
+ try {
+ client.olmUpdateAvailable = semver.lt(
+ client.olmVersion ? client.olmVersion : "",
+ latestOlVersion
+ );
+ } catch (error) {
+ client.olmUpdateAvailable = false;
+ }
+
+ });
+ }
+ } catch (error) {
+ // Log the error but don't let it block the response
+ logger.warn(
+ "Failed to check for OLM updates, continuing without update info:",
+ error
+ );
+ }
+
+
return response(res, {
data: {
clients: clientsWithSites,
diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx
index 0813ad3c..fd73b736 100644
--- a/src/app/[orgId]/settings/clients/page.tsx
+++ b/src/app/[orgId]/settings/clients/page.tsx
@@ -44,7 +44,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
mbIn: formatSize(client.megabytesIn || 0),
mbOut: formatSize(client.megabytesOut || 0),
orgId: params.orgId,
- online: client.online
+ online: client.online,
+ olmVersion: client.olmVersion || undefined,
+ olmUpdateAvailable: client.olmUpdateAvailable || false,
};
});
diff --git a/src/components/ClientsTable.tsx b/src/components/ClientsTable.tsx
index 95a6b55d..471cdf28 100644
--- a/src/components/ClientsTable.tsx
+++ b/src/components/ClientsTable.tsx
@@ -26,6 +26,8 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
+import { Badge } from "./ui/badge";
+import { InfoPopup } from "./ui/info-popup";
export type ClientRow = {
id: number;
@@ -36,6 +38,8 @@ export type ClientRow = {
mbOut: string;
orgId: string;
online: boolean;
+ olmVersion?: string;
+ olmUpdateAvailable: boolean;
};
type ClientTableProps = {
@@ -204,6 +208,45 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
);
}
},
+ {
+ accessorKey: "client",
+ header: ({ column }) => {
+ return (
+
+ );
+ },
+ cell: ({ row }) => {
+ const originalRow = row.original;
+
+ return (
+
+
+
+ Olm
+ {originalRow.olmVersion && (
+
+ v{originalRow.olmVersion}
+
+ )}
+
+
+ {originalRow.olmUpdateAvailable && (
+
+ )}
+
+ );
+ }
+ },
{
accessorKey: "subnet",
header: ({ column }) => {
@@ -282,7 +325,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
{t("deleteClientQuestion")}
- {t("clientMessageRemove")}
+ {t("clientMessageRemove")}
}