mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-04 19:44:47 +00:00
Compare commits
9 Commits
dependabot
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f286d66cbc | ||
|
|
81922f54d5 | ||
|
|
9474792e14 | ||
|
|
0c6acfe282 | ||
|
|
0ae20c0b25 | ||
|
|
bcd3bee148 | ||
|
|
e2814517d6 | ||
|
|
c24db3df0e | ||
|
|
7ecfc9cbd3 |
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function fireHealthCheckHealthyAlert(
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fireHealthCheckNotHealthyAlert(
|
||||
export async function fireHealthCheckUnhealthyAlert(
|
||||
orgId: string,
|
||||
healthCheckId: number,
|
||||
healthCheckName?: string,
|
||||
|
||||
20
server/lib/alerts/events/resourceEvents.ts
Normal file
20
server/lib/alerts/events/resourceEvents.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export async function fireResourceHealthyAlert(
|
||||
orgId: string,
|
||||
resourceId: number,
|
||||
resourceName?: string | null,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {}
|
||||
|
||||
export async function fireResourceUnhealthyAlert(
|
||||
orgId: string,
|
||||
resourceId: number,
|
||||
resourceName?: string | null,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {}
|
||||
|
||||
export async function fireResourceToggleAlert(
|
||||
orgId: string,
|
||||
resourceId: number,
|
||||
resourceName?: string | null,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./events/siteEvents";
|
||||
export * from "./events/healthCheckEvents";
|
||||
export * from "./events/resourceEvents";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>,
|
||||
useCache: boolean
|
||||
useCache: boolean = true
|
||||
): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
|
||||
@@ -416,7 +416,8 @@ export class TraefikConfigManager {
|
||||
// Get valid certificates for domains not covered by wildcards
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomains(
|
||||
domainsToFetch
|
||||
domainsToFetch,
|
||||
true
|
||||
);
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function fireHealthCheckUnhealthyAlert(
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`fireHealthCheckNotHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||
`fireHealthCheckUnhealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./types";
|
||||
export * from "./processAlerts";
|
||||
export * from "./sendAlertWebhook";
|
||||
export * from "./sendAlertEmail";
|
||||
export * from "./events/siteEvents";
|
||||
export * from "./events/healthCheckEvents";
|
||||
export * from "./events/healthCheckEvents";
|
||||
export * from "./events/resourceEvents";
|
||||
|
||||
@@ -27,9 +27,9 @@ import {
|
||||
import config from "@server/lib/config";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import { AlertContext, WebhookAlertConfig } from "./types";
|
||||
import { sendAlertWebhook } from "./sendAlertWebhook";
|
||||
import { sendAlertEmail } from "./sendAlertEmail";
|
||||
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||
|
||||
/**
|
||||
* Core alert processing pipeline.
|
||||
|
||||
@@ -15,7 +15,7 @@ import { sendEmail } from "@server/emails";
|
||||
import AlertNotification from "@server/emails/templates/AlertNotification";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { AlertContext } from "./types";
|
||||
import { AlertContext } from "@server/routers/alertRule/types";
|
||||
|
||||
/**
|
||||
* Sends an alert notification email to every address in `recipients`.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import { AlertContext, WebhookAlertConfig } from "./types";
|
||||
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 15_000;
|
||||
|
||||
@@ -137,4 +137,4 @@ function buildHeaders(webhookConfig: WebhookAlertConfig): Record<string, string>
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import { CreateAlertRuleResponse } from "@server/routers/alertRule/types";
|
||||
|
||||
export const SITE_EVENT_TYPES = ["site_online", "site_offline", "site_toggle"] as const;
|
||||
export const HC_EVENT_TYPES = [
|
||||
@@ -169,10 +170,6 @@ const bodySchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateAlertRuleResponse = {
|
||||
alertRuleId: number;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/alert-rule",
|
||||
|
||||
@@ -32,7 +32,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import { WebhookAlertConfig } from "#private/lib/alerts/types";
|
||||
import { GetAlertRuleResponse, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -41,43 +41,6 @@ const paramsSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetAlertRuleResponse = {
|
||||
alertRuleId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
eventType:
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle";
|
||||
enabled: boolean;
|
||||
cooldownSeconds: number;
|
||||
lastTriggeredAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
siteIds: number[];
|
||||
healthCheckIds: number[];
|
||||
resourceIds: number[];
|
||||
recipients: {
|
||||
recipientId: number;
|
||||
userId: string | null;
|
||||
roleId: number | null;
|
||||
email: string | null;
|
||||
}[];
|
||||
webhookActions: {
|
||||
webhookActionId: number;
|
||||
webhookUrl: string;
|
||||
enabled: boolean;
|
||||
lastSentAt: number | null;
|
||||
config: WebhookAlertConfig | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/alert-rule/{alertRuleId}",
|
||||
|
||||
@@ -27,6 +27,7 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -84,28 +85,6 @@ const HEALTH_CHECK_ALERT_EVENT_TYPES = [
|
||||
"health_check_toggle"
|
||||
] as const;
|
||||
|
||||
export type ListAlertRulesResponse = {
|
||||
alertRules: {
|
||||
alertRuleId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
eventType: string;
|
||||
enabled: boolean;
|
||||
cooldownSeconds: number;
|
||||
lastTriggeredAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
siteIds: number[];
|
||||
healthCheckIds: number[];
|
||||
resourceIds: number[];
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/alert-rules",
|
||||
|
||||
@@ -1,36 +1,65 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
export type ListAlertRulesResponse = {
|
||||
alertRules: {
|
||||
alertRuleId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
eventType: string;
|
||||
enabled: boolean;
|
||||
cooldownSeconds: number;
|
||||
lastTriggeredAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
siteIds: number[];
|
||||
healthCheckIds: number[];
|
||||
resourceIds: number[];
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Alert event types
|
||||
// ---------------------------------------------------------------------------
|
||||
export type CreateAlertRuleResponse = {
|
||||
alertRuleId: number;
|
||||
};
|
||||
|
||||
export type AlertEventType =
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webhook authentication config (stored as encrypted JSON in the DB)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type WebhookAuthType = "none" | "bearer" | "basic" | "custom";
|
||||
export type GetAlertRuleResponse = {
|
||||
alertRuleId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
eventType:
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle";
|
||||
enabled: boolean;
|
||||
cooldownSeconds: number;
|
||||
lastTriggeredAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
siteIds: number[];
|
||||
healthCheckIds: number[];
|
||||
resourceIds: number[];
|
||||
recipients: {
|
||||
recipientId: number;
|
||||
userId: string | null;
|
||||
roleId: number | null;
|
||||
email: string | null;
|
||||
}[];
|
||||
webhookActions: {
|
||||
webhookActionId: number;
|
||||
webhookUrl: string;
|
||||
enabled: boolean;
|
||||
lastSentAt: number | null;
|
||||
config: WebhookAlertConfig | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Stored as an encrypted JSON blob in `alertWebhookActions.config`.
|
||||
@@ -52,6 +81,27 @@ export interface WebhookAlertConfig {
|
||||
method?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Alert event types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type AlertEventType =
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webhook authentication config (stored as encrypted JSON in the DB)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type WebhookAuthType = "none" | "bearer" | "basic" | "custom";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal alert event passed through the processing pipeline
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
fireHealthCheckHealthyAlert,
|
||||
fireHealthCheckUnhealthyAlert
|
||||
} from "#dynamic/lib/alerts";
|
||||
import { fireResourceHealthyAlert, fireResourceUnhealthyAlert } from "@server/private/lib/alerts/events/resourceEvents";
|
||||
import {
|
||||
fireResourceHealthyAlert,
|
||||
fireResourceUnhealthyAlert
|
||||
} from "#dynamic/lib/alerts";
|
||||
|
||||
interface TargetHealthStatus {
|
||||
status: string;
|
||||
@@ -223,13 +226,13 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
||||
await fireHealthCheckUnhealthyAlert(
|
||||
orgId,
|
||||
targetCheck.targetHealthCheckId,
|
||||
targetCheck.name
|
||||
targetCheck.name ?? undefined
|
||||
);
|
||||
} else if (healthStatus.status === "healthy") {
|
||||
await fireHealthCheckHealthyAlert(
|
||||
orgId,
|
||||
targetCheck.targetHealthCheckId,
|
||||
targetCheck.name
|
||||
targetCheck.name ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import AlertingRulesTable from "@app/components/AlertingRulesTable";
|
||||
import DismissableBanner from "@app/components/DismissableBanner";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import type { ListAlertRulesResponse } from "@server/private/routers/alertRule";
|
||||
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { BellRing } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { GetAlertRuleResponse } from "@server/private/routers/alertRule";
|
||||
import type { GetAlertRuleResponse } from "@server/routers/alertRule/types";
|
||||
import type { AlertRuleFormValues } from "@app/lib/alertRuleForm";
|
||||
|
||||
export default function EditAlertRulePage() {
|
||||
|
||||
@@ -131,7 +131,7 @@ export function LayoutSidebar({
|
||||
const showTrial =
|
||||
build === "saas" &&
|
||||
Boolean(orgId) &&
|
||||
subscriptionContext?.isTrial
|
||||
subscriptionContext?.isTrial;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "@app/lib/alertRuleForm";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import type { CreateAlertRuleResponse } from "@server/private/routers/alertRule";
|
||||
import type { CreateAlertRuleResponse } from "@server/routers/alertRule/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeft, Cog, Flag, Zap } from "lucide-react";
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
ListResourceNamesResponse,
|
||||
ListResourcesResponse
|
||||
} from "@server/routers/resource";
|
||||
import type { ListAlertRulesResponse } from "@server/private/routers/alertRule";
|
||||
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||
import type { ListRolesResponse } from "@server/routers/role";
|
||||
import type { ListSitesResponse } from "@server/routers/site";
|
||||
import type {
|
||||
|
||||
Reference in New Issue
Block a user