diff --git a/messages/en-US.json b/messages/en-US.json index 5d4facf1c..d2b79bd35 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3196,5 +3196,6 @@ "alertLabel": "Alert", "domainPickerWildcardSubdomainNotAllowed": "Wildcard subdomains are not allowed.", "domainPickerWildcardCertWarning": "Wildcard certificates must be configured separately in Traefik.", - "domainPickerWildcardCertWarningLink": "Learn more" + "domainPickerWildcardCertWarningLink": "Learn more", + "health": "Health" } diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index fc1fee5b0..ba93bc46a 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -179,6 +179,7 @@ export async function updateProxyResources( newHealthcheck.name, newHealthcheck.targetId, undefined, + true, trx ); } @@ -581,6 +582,7 @@ export async function updateProxyResources( newHealthcheck.name, newHealthcheck.targetId, undefined, + true, trx ); } diff --git a/server/private/lib/alerts/events/healthCheckEvents.ts b/server/private/lib/alerts/events/healthCheckEvents.ts index 04f197a8d..48aef424f 100644 --- a/server/private/lib/alerts/events/healthCheckEvents.ts +++ b/server/private/lib/alerts/events/healthCheckEvents.ts @@ -50,7 +50,8 @@ export async function fireHealthCheckHealthyAlert( healthCheckName?: string | null, healthCheckTargetId?: number | null, extra?: Record, - trx: Transaction | typeof db = db + send: boolean = true, + trx: Transaction | typeof db = db, ): Promise { try { await trx.insert(statusHistory).values({ @@ -63,6 +64,10 @@ export async function fireHealthCheckHealthyAlert( await handleResource(orgId, healthCheckTargetId, trx); + if (!send) { + return; + } + await processAlerts({ eventType: "health_check_healthy", orgId, @@ -108,6 +113,7 @@ export async function fireHealthCheckUnhealthyAlert( healthCheckName?: string | null, healthCheckTargetId?: number | null, extra?: Record, + send: boolean = true, trx: Transaction | typeof db = db ): Promise { try { @@ -121,6 +127,10 @@ export async function fireHealthCheckUnhealthyAlert( await handleResource(orgId, healthCheckTargetId, trx); + if (!send) { + return; + } + await processAlerts({ eventType: "health_check_unhealthy", orgId, @@ -155,6 +165,7 @@ export async function fireHealthCheckUnknownAlert( healthCheckName?: string | null, healthCheckTargetId?: number | null, extra?: Record, + send: boolean = true, trx: Transaction | typeof db = db ): Promise { try { @@ -167,6 +178,10 @@ export async function fireHealthCheckUnknownAlert( }); await handleResource(orgId, healthCheckTargetId, trx); + + if (!send) { + return; + } } catch (err) { logger.error( `fireHealthCheckUnknownAlert: unexpected error for healthCheckId ${healthCheckId}`, diff --git a/server/private/lib/alerts/events/siteEvents.ts b/server/private/lib/alerts/events/siteEvents.ts index 562accc18..36e3dacff 100644 --- a/server/private/lib/alerts/events/siteEvents.ts +++ b/server/private/lib/alerts/events/siteEvents.ts @@ -125,6 +125,7 @@ export async function fireSiteOfflineAlert( healthCheck.name, undefined, undefined, + true, trx ); } diff --git a/server/private/routers/alertEvents/triggerHealthCheckAlert.ts b/server/private/routers/alertEvents/triggerHealthCheckAlert.ts index 0590c2bcd..530557463 100644 --- a/server/private/routers/alertEvents/triggerHealthCheckAlert.ts +++ b/server/private/routers/alertEvents/triggerHealthCheckAlert.ts @@ -14,7 +14,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { targetHealthCheck, statusHistory } from "@server/db"; +import { targetHealthCheck } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/private/routers/alertEvents/triggerResourceAlert.ts b/server/private/routers/alertEvents/triggerResourceAlert.ts index a43b8e201..afda63e9a 100644 --- a/server/private/routers/alertEvents/triggerResourceAlert.ts +++ b/server/private/routers/alertEvents/triggerResourceAlert.ts @@ -14,7 +14,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, statusHistory } from "@server/db"; +import { resources } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -24,7 +24,6 @@ import { eq, and } from "drizzle-orm"; import { fireResourceHealthyAlert, fireResourceUnhealthyAlert, - fireResourceToggleAlert, fireResourceDegradedAlert } from "#private/lib/alerts/events/resourceEvents"; diff --git a/server/private/routers/alertEvents/triggerSiteAlert.ts b/server/private/routers/alertEvents/triggerSiteAlert.ts index a7fa0cafc..25b14acb9 100644 --- a/server/private/routers/alertEvents/triggerSiteAlert.ts +++ b/server/private/routers/alertEvents/triggerSiteAlert.ts @@ -14,7 +14,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { sites, statusHistory } from "@server/db"; +import { sites } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/private/routers/healthChecks/createHealthCheck.ts b/server/private/routers/healthChecks/createHealthCheck.ts index ada583a70..ead58e996 100644 --- a/server/private/routers/healthChecks/createHealthCheck.ts +++ b/server/private/routers/healthChecks/createHealthCheck.ts @@ -22,6 +22,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { addStandaloneHealthCheck } from "@server/routers/newt/targets"; +import { fireHealthCheckUnhealthyAlert } from "#private/lib/alerts"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -146,6 +147,15 @@ export async function createHealthCheck( }) .returning(); + await fireHealthCheckUnhealthyAlert( + record.orgId, + record.targetHealthCheckId, + record.name || "", + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + // Push health check to newt if the site is a newt site if (siteId) { const [site] = await db diff --git a/server/private/routers/healthChecks/updateHealthCheck.ts b/server/private/routers/healthChecks/updateHealthCheck.ts index 47a9518a9..8afeca6a4 100644 --- a/server/private/routers/healthChecks/updateHealthCheck.ts +++ b/server/private/routers/healthChecks/updateHealthCheck.ts @@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { and, eq, isNull } from "drizzle-orm"; import { addStandaloneHealthCheck } from "@server/routers/newt/targets"; +import { fireHealthCheckUnhealthyAlert, fireHealthCheckUnknownAlert, fireHealthCheckHealthyAlert } from "#private/lib/alerts"; const paramsSchema = z .object({ @@ -233,6 +234,37 @@ export async function updateHealthCheck( ) .returning(); + if (updated.hcHealth === "unhealthy" && existingHealthCheck.hcHealth !== "unhealthy") { + await fireHealthCheckUnhealthyAlert( + updated.orgId, + updated.targetHealthCheckId, + updated.name || "", + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (updated.hcHealth === "unknown" && existingHealthCheck.hcHealth !== "unknown") { + // if the health is unknown, we want to fire an alert to notify users to enable health checks + await fireHealthCheckUnknownAlert( + updated.orgId, + updated.targetHealthCheckId, + updated.name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (updated.hcHealth === "healthy" && existingHealthCheck.hcHealth !== "healthy") { + await fireHealthCheckHealthyAlert( + updated.orgId, + updated.targetHealthCheckId, + updated.name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } + + // Push updated health check to newt if the site is a newt site const [newt] = await db .select() diff --git a/server/routers/newt/handleNewtDisconnectingMessage.ts b/server/routers/newt/handleNewtDisconnectingMessage.ts index 15c7d3662..a05d410c8 100644 --- a/server/routers/newt/handleNewtDisconnectingMessage.ts +++ b/server/routers/newt/handleNewtDisconnectingMessage.ts @@ -6,7 +6,7 @@ import { } from "@server/db"; import { eq } from "drizzle-orm"; import logger from "@server/logger"; -import { fireSiteOfflineAlert } from "@server/lib/alerts"; +import { fireSiteOfflineAlert } from "#dynamic/lib/alerts"; /** * Handles disconnecting messages from sites to show disconnected in the ui diff --git a/server/routers/newt/offlineChecker.ts b/server/routers/newt/offlineChecker.ts index 1dc51d5da..6ff43688a 100644 --- a/server/routers/newt/offlineChecker.ts +++ b/server/routers/newt/offlineChecker.ts @@ -1,10 +1,7 @@ import { db, newts, - sites, - targetHealthCheck, - targets, - statusHistory + sites } from "@server/db"; import { hasActiveConnections } from "#dynamic/routers/ws"; import { eq, lt, isNull, and, or, ne, not, inArray } from "drizzle-orm"; diff --git a/server/routers/newt/registerNewt.ts b/server/routers/newt/registerNewt.ts index cc53e48df..440a62198 100644 --- a/server/routers/newt/registerNewt.ts +++ b/server/routers/newt/registerNewt.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, statusHistory } from "@server/db"; import { siteProvisioningKeys, siteProvisioningKeyOrg, @@ -223,6 +223,14 @@ export async function registerNewt( }) .returning(); + await trx.insert(statusHistory).values({ + entityType: "site", + entityId: newSite.siteId, + orgId: orgId, + status: "offline", + timestamp: Math.floor(Date.now() / 1000) + }); + newSiteId = newSite.siteId; // Grant admin role access to the new site diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index f9b26799e..0ac0de3d7 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { clients, db, exitNodes } from "@server/db"; +import { clients, db, exitNodes, statusHistory } from "@server/db"; import { roles, userSites, sites, roleSites, Site, orgs } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -321,12 +321,7 @@ export async function createSite( const existingSite = await db .select() .from(sites) - .where( - and( - eq(sites.niceId, niceId), - eq(sites.orgId, orgId) - ) - ) + .where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))) .limit(1); if (existingSite.length > 0) { @@ -344,7 +339,8 @@ export async function createSite( if (type == "newt") { [newSite] = await trx .insert(sites) - .values({ // NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT + .values({ + // NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT orgId, name, niceId: updatedNiceId!, @@ -354,6 +350,14 @@ export async function createSite( status: "approved" }) .returning(); + + await trx.insert(statusHistory).values({ + entityType: "site", + entityId: newSite.siteId, + orgId: orgId, + status: "offline", + timestamp: Math.floor(Date.now() / 1000) + }); } else if (type == "wireguard") { // we are creating a site with an exit node (tunneled) if (!subnet) { diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index e37f12490..c58157e75 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -1,6 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, TargetHealthCheck, targetHealthCheck } from "@server/db"; +import { + db, + statusHistory, + TargetHealthCheck, + targetHealthCheck +} from "@server/db"; import { newts, resources, sites, Target, targets } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -14,6 +19,7 @@ import { eq } from "drizzle-orm"; import { pickPort } from "./helpers"; import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; +import { fireHealthCheckHealthyAlert, fireHealthCheckUnhealthyAlert, fireHealthCheckUnknownAlert } from "#dynamic/lib/alerts"; const createTargetParamsSchema = z.strictObject({ resourceId: z.string().transform(Number).pipe(z.int().positive()) @@ -252,6 +258,36 @@ export async function createTarget( }) .returning(); + if (healthCheck[0].hcHealth === "unhealthy") { + await fireHealthCheckUnhealthyAlert( + healthCheck[0].orgId, + healthCheck[0].targetHealthCheckId, + healthCheck[0].name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (healthCheck[0].hcHealth === "unknown") { + // if the health is unknown, we want to fire an alert to notify users to enable health checks + await fireHealthCheckUnknownAlert( + healthCheck[0].orgId, + healthCheck[0].targetHealthCheckId, + healthCheck[0].name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (healthCheck[0].hcHealth === "healthy") { + await fireHealthCheckHealthyAlert( + healthCheck[0].orgId, + healthCheck[0].targetHealthCheckId, + healthCheck[0].name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } + if (site.pubKey) { if (site.type == "wireguard") { await addPeer(site.exitNodeId!, { diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index c3bcb6d8e..0fe5caf7b 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -1,10 +1,6 @@ import { db, - targets, - resources, - sites, - targetHealthCheck, - statusHistory + targetHealthCheck } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { Newt } from "@server/db"; @@ -142,6 +138,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( targetCheck.name ?? undefined, targetCheck.targetId, undefined, + true, trx ); } else if (healthStatus.status === "healthy") { @@ -151,6 +148,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( targetCheck.name ?? undefined, targetCheck.targetId, undefined, + true, trx ); } diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 21f52566f..0766f87b5 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -10,10 +10,11 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { addPeer } from "../gerbil/peers"; import { addTargets } from "../newt/targets"; -import { fireHealthCheckUnknownAlert } from "#dynamic/lib/alerts"; +import { fireHealthCheckHealthyAlert, fireHealthCheckUnknownAlert } from "#dynamic/lib/alerts"; import { pickPort } from "./helpers"; import { isTargetValid } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; +import { fireHealthCheckUnhealthyAlert } from "@server/lib/alerts"; const updateTargetParamsSchema = z.strictObject({ @@ -256,12 +257,33 @@ export async function updateTarget( .where(eq(targetHealthCheck.targetId, targetId)) .returning(); - if (isDisablingHc) { + if (updatedHc.hcHealth === "unhealthy" && existingHc.hcHealth !== "unhealthy") { + await fireHealthCheckUnhealthyAlert( + updatedHc.orgId, + updatedHc.targetHealthCheckId, + updatedHc.name || "", + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (updatedHc.hcHealth === "unknown" && existingHc.hcHealth !== "unknown") { + // if the health is unknown, we want to fire an alert to notify users to enable health checks await fireHealthCheckUnknownAlert( - resource.orgId, - existingHc.targetHealthCheckId, - existingHc.name, - updatedHc.targetId + updatedHc.orgId, + updatedHc.targetHealthCheckId, + updatedHc.name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet + ); + } else if (updatedHc.hcHealth === "healthy" && existingHc.hcHealth !== "healthy") { + await fireHealthCheckHealthyAlert( + updatedHc.orgId, + updatedHc.targetHealthCheckId, + updatedHc.name, + undefined, + undefined, + false // dont send the alert because we just want to create the alert, not notify users yet ); } diff --git a/src/app/[orgId]/settings/alerting/(list)/health-checks/page.tsx b/src/app/[orgId]/settings/alerting/(list)/health-checks/page.tsx index 5cbb9ea3d..f3cf0160f 100644 --- a/src/app/[orgId]/settings/alerting/(list)/health-checks/page.tsx +++ b/src/app/[orgId]/settings/alerting/(list)/health-checks/page.tsx @@ -151,6 +151,7 @@ export default async function AlertingHealthChecksPage( fullDomain: string | null; niceId: string; ssl: boolean; + wildcard: boolean; } | null = null; if (resourceIdParam) { try { @@ -165,7 +166,8 @@ export default async function AlertingHealthChecksPage( resourceId: r.resourceId, fullDomain: r.fullDomain, niceId: r.niceId, - ssl: r.ssl + ssl: r.ssl, + wildcard: r.wildcard }; } } catch { diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 555bcb264..89cc4fed9 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -557,6 +557,7 @@ export default function DomainPicker({ )}

null; if (env.flags.disableEnterpriseFeatures) { return null; diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 01cd17635..525b28809 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -63,13 +63,6 @@ import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ControlledDataTable } from "./ui/controlled-data-table"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@app/components/ui/tooltip"; -import type { StatusHistoryResponse } from "@server/lib/statusHistory"; import UptimeMiniBar from "./UptimeMiniBar"; export type TargetHealth = { @@ -466,7 +459,7 @@ export default function ProxyResourcesTable({ { id: "status", accessorKey: "status", - friendlyName: t("status"), + friendlyName: t("health"), header: () => ( ),