mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-01 21:46:38 +00:00
Fix strings and local sites
This commit is contained in:
@@ -15,6 +15,8 @@ import logger from "@server/logger";
|
|||||||
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types";
|
||||||
|
|
||||||
const REQUEST_TIMEOUT_MS = 15_000;
|
const REQUEST_TIMEOUT_MS = 15_000;
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const RETRY_BASE_DELAY_MS = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a single webhook POST for an alert event.
|
* Sends a single webhook POST for an alert event.
|
||||||
@@ -49,45 +51,70 @@ export async function sendAlertWebhook(
|
|||||||
const body = JSON.stringify(payload);
|
const body = JSON.stringify(payload);
|
||||||
const headers = buildHeaders(webhookConfig);
|
const headers = buildHeaders(webhookConfig);
|
||||||
|
|
||||||
const controller = new AbortController();
|
let lastError: Error | undefined;
|
||||||
const timeoutHandle = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
||||||
|
|
||||||
let response: Response;
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
try {
|
const controller = new AbortController();
|
||||||
response = await fetch(url, {
|
const timeoutHandle = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||||
method: webhookConfig.method ?? "POST",
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
||||||
if (isAbort) {
|
|
||||||
throw new Error(
|
|
||||||
`Alert webhook: request to "${url}" timed out after ${REQUEST_TIMEOUT_MS} ms`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
throw new Error(`Alert webhook: request to "${url}" failed – ${msg}`);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
let response: Response;
|
||||||
let snippet = "";
|
|
||||||
try {
|
try {
|
||||||
const text = await response.text();
|
response = await fetch(url, {
|
||||||
snippet = text.slice(0, 300);
|
method: webhookConfig.method ?? "POST",
|
||||||
} catch {
|
headers,
|
||||||
// best-effort
|
body,
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
const isAbort = err instanceof Error && err.name === "AbortError";
|
||||||
|
if (isAbort) {
|
||||||
|
lastError = new Error(
|
||||||
|
`Alert webhook: request to "${url}" timed out after ${REQUEST_TIMEOUT_MS} ms`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
lastError = new Error(`Alert webhook: request to "${url}" failed – ${msg}`);
|
||||||
|
}
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
||||||
|
logger.warn(
|
||||||
|
`Alert webhook: attempt ${attempt}/${MAX_RETRIES} failed – retrying in ${delay} ms. ${lastError.message}`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
}
|
}
|
||||||
throw new Error(
|
|
||||||
`Alert webhook: server at "${url}" returned HTTP ${response.status} ${response.statusText}` +
|
if (!response.ok) {
|
||||||
(snippet ? ` – ${snippet}` : "")
|
let snippet = "";
|
||||||
);
|
try {
|
||||||
|
const text = await response.text();
|
||||||
|
snippet = text.slice(0, 300);
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
lastError = new Error(
|
||||||
|
`Alert webhook: server at "${url}" returned HTTP ${response.status} ${response.statusText}` +
|
||||||
|
(snippet ? ` – ${snippet}` : "")
|
||||||
|
);
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
||||||
|
logger.warn(
|
||||||
|
`Alert webhook: attempt ${attempt}/${MAX_RETRIES} failed – retrying in ${delay} ms. ${lastError.message}`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Alert webhook sent successfully to "${url}" for event "${context.eventType}" (attempt ${attempt}/${MAX_RETRIES})`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Alert webhook sent successfully to "${url}" for event "${context.eventType}"`);
|
throw lastError ?? new Error(`Alert webhook: all ${MAX_RETRIES} attempts failed for "${url}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{site?.siteId && site?.orgId && (
|
{site?.siteId && site?.orgId && site.type != "local" && (
|
||||||
<UptimeAlertSection
|
<UptimeAlertSection
|
||||||
orgId={site.orgId}
|
orgId={site.orgId}
|
||||||
siteId={site.siteId}
|
siteId={site.siteId}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export type SiteRow = {
|
|||||||
mbIn: string;
|
mbIn: string;
|
||||||
mbOut: string;
|
mbOut: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
type: "newt" | "wireguard";
|
type: "newt" | "wireguard" | "local";
|
||||||
newtVersion?: string;
|
newtVersion?: string;
|
||||||
newtUpdateAvailable?: boolean;
|
newtUpdateAvailable?: boolean;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
@@ -236,6 +236,9 @@ export default function SitesTable({
|
|||||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
if (originalRow.type == "local") {
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<UptimeMiniBar siteId={originalRow.id} days={30} />
|
<UptimeMiniBar siteId={originalRow.id} days={30} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export default function SupporterStatus({
|
|||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<strong>Business & Enterprise Users:</strong> For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}
|
<strong>Business & Enterprise Users:</strong> For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://pangolin.net/pricing?hosting=self-host"
|
href="https://pangolin.net/pricing#Self-Hosted"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline inline-flex items-center gap-1"
|
className="underline inline-flex items-center gap-1"
|
||||||
|
|||||||
Reference in New Issue
Block a user