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: () => (
),