diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts index 4bb4f58ba..e19dbe394 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts @@ -31,6 +31,7 @@ export const gatewayV2DalFactory = (db: TDbClient) => { if (isHeartbeatStale) { const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + void query.whereNotNull(`${TableName.GatewayV2}.heartbeat`); void query.where(`${TableName.GatewayV2}.heartbeat`, "<", oneHourAgo); void query.where((v) => { void v diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index 78a62bcb8..dce5bc299 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -9,6 +9,7 @@ import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { GatewayProxyProtocol } from "@app/lib/gateway/types"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { logger } from "@app/lib/logger"; @@ -902,13 +903,7 @@ export const gatewayV2ServiceFactory = ({ "Found gateways with last heartbeat over an hour ago. Sending notifications." ); - const gatewaysByOrg = unhealthyGateways.reduce>((acc, gw) => { - if (!acc[gw.orgId]) { - acc[gw.orgId] = []; - } - acc[gw.orgId].push(gw); - return acc; - }, {}); + const gatewaysByOrg = groupBy(unhealthyGateways, (gw) => gw.orgId); for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) { try { diff --git a/backend/src/ee/services/relay/relay-dal.ts b/backend/src/ee/services/relay/relay-dal.ts index 687b2d0e8..8e7eac8de 100644 --- a/backend/src/ee/services/relay/relay-dal.ts +++ b/backend/src/ee/services/relay/relay-dal.ts @@ -21,6 +21,7 @@ export const relayDalFactory = (db: TDbClient) => { if (isHeartbeatStale) { const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + void query.whereNotNull(`${TableName.Relay}.heartbeat`); void query.where(`${TableName.Relay}.heartbeat`, "<", oneHourAgo); void query.where((v) => { void v diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 6b56908f5..41fe91bfc 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -8,6 +8,7 @@ import { OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { groupBy } from "@app/lib/fn"; import { createRelayConnection } from "@app/lib/gateway-v2/gateway-v2"; import { logger } from "@app/lib/logger"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; @@ -1220,14 +1221,7 @@ export const relayServiceFactory = ({ "Found relays with last heartbeat over an hour ago. Sending notifications." ); - const relaysByOrg = unhealthyRelays.reduce>((acc, r) => { - const key = r.orgId ?? "instance"; - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(r); - return acc; - }, {}); + const relaysByOrg = groupBy(unhealthyRelays, (r) => r.orgId ?? "instance"); for await (const [orgId, relays] of Object.entries(relaysByOrg)) { try { @@ -1244,7 +1238,7 @@ export const relayServiceFactory = ({ recipients, subjectLine: "Relay Health Alert", substitutions: { - type: "relay", + type: "instance-relay", names: relayNames }, template: SmtpTemplates.HealthAlert diff --git a/backend/src/services/smtp/emails/HealthAlertTemplate.tsx b/backend/src/services/smtp/emails/HealthAlertTemplate.tsx index 9b9a1f591..cc90ae3cb 100644 --- a/backend/src/services/smtp/emails/HealthAlertTemplate.tsx +++ b/backend/src/services/smtp/emails/HealthAlertTemplate.tsx @@ -4,7 +4,7 @@ import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseLink } from "./BaseLink"; interface HealthAlertTemplateProps extends Omit { - type: "gateway" | "relay"; + type: "gateway" | "relay" | "instance-relay"; names: string; } @@ -24,8 +24,14 @@ export const HealthAlertTemplate = ({ siteUrl, names, type }: HealthAlertTemplat heartbeat in over an hour: {names}. - If your issue persists, you can contact the Infisical team at{" "} - support@infisical.com. + {type === "instance-relay" && ( + <> + If your issue persists, you can contact the Infisical team at{" "} + support@infisical.com. + + )} + {type === "relay" && <>Please contact your relay administrators.} + {type === "gateway" && <>Please contact your gateway administrators.} diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx index 152222b29..0e3069aee 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx @@ -48,7 +48,7 @@ import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; -export const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { +const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index 51accf6b6..24992a58e 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -42,7 +42,24 @@ import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; -import { GatewayHealthStatus } from "../GatewayTab/GatewayTab"; +const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { + const heartbeatDate = heartbeat ? new Date(heartbeat) : null; + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); + + const isHealthy = heartbeatDate && heartbeatDate >= oneHourAgo; + const tooltipContent = heartbeatDate + ? `Last heartbeat: ${heartbeatDate.toLocaleString()}` + : "No heartbeat data available"; + + return ( + + + {isHealthy ? "Healthy" : "Unreachable"} + + + ); +}; export const RelayTab = withPermission( () => { @@ -143,7 +160,7 @@ export const RelayTab = withPermission( {el.host} {formatRelative(new Date(el.createdAt), new Date())} - +