Merge branch 'dev' of https://github.com/fosrl/pangolin into dev

This commit is contained in:
miloschwartz
2026-04-21 17:39:24 -07:00
6 changed files with 135 additions and 157 deletions

71
package-lock.json generated
View File

@@ -44,7 +44,6 @@
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.90.21", "@tanstack/react-query": "5.90.21",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@xyflow/react": "^12.8.4",
"arctic": "3.7.0", "arctic": "3.7.0",
"axios": "1.13.5", "axios": "1.13.5",
"better-sqlite3": "11.9.1", "better-sqlite3": "11.9.1",
@@ -8719,6 +8718,7 @@
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/d3-selection": "*" "@types/d3-selection": "*"
@@ -8834,6 +8834,7 @@
"version": "3.0.11", "version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/d3-shape": { "node_modules/@types/d3-shape": {
@@ -8868,6 +8869,7 @@
"version": "3.0.9", "version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/d3-selection": "*" "@types/d3-selection": "*"
@@ -8877,6 +8879,7 @@
"version": "3.0.8", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/d3-interpolate": "*", "@types/d3-interpolate": "*",
@@ -9680,38 +9683,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@xyflow/react": {
"version": "12.8.4",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz",
"integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.68",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.68",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz",
"integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -10564,12 +10535,6 @@
"url": "https://polar.sh/cva" "url": "https://polar.sh/cva"
} }
}, },
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/cli-spinners": { "node_modules/cli-spinners": {
"version": "2.9.2", "version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
@@ -19881,34 +19846,6 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
} }
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
export async function getValidCertificatesForDomains( export async function getValidCertificatesForDomains(
domains: Set<string> domains: Set<string>,
useCache: boolean
): Promise< ): Promise<
Array<{ Array<{
id: number; id: number;

View File

@@ -1,3 +1,16 @@
/*
* 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.
*/
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, statusHistory } from "@server/db"; import { db, statusHistory } from "@server/db";

View File

@@ -17,7 +17,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { and, eq, like, sql } from "drizzle-orm"; import { and, eq, isNotNull, like, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
@@ -85,6 +85,7 @@ export async function listHealthChecks(
const whereClause = and( const whereClause = and(
eq(targetHealthCheck.orgId, orgId), eq(targetHealthCheck.orgId, orgId),
isNotNull(targetHealthCheck.hcMode), // filter out the null ones attached to targets
query query
? like( ? like(
sql`LOWER(${targetHealthCheck.name})`, sql`LOWER(${targetHealthCheck.name})`,

View File

@@ -329,7 +329,7 @@ export default function HealthChecksTable({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
disabled={!isPaid} disabled={!isPaid || !!r.resourceId}
onClick={() => { onClick={() => {
setSelected(r); setSelected(r);
setDeleteOpen(true); setDeleteOpen(true);
@@ -339,18 +339,31 @@ export default function HealthChecksTable({
{t("delete")} {t("delete")}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button {r.resourceId && r.resourceName && r.resourceNiceId ? (
variant="outline" <Link href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}>
disabled={!isPaid} <Button
onClick={() => { variant="outline"
setSelected(r); disabled={!isPaid}
setCredenzaOpen(true); >
}} {t("edit")}
> </Button>
{t("edit")} </Link>
</Button> ) : (
<Button
variant="outline"
disabled={!isPaid}
onClick={() => {
setSelected(r);
setCredenzaOpen(true);
}}
>
{t("edit")}
</Button>
)}
</div> </div>
); );
} }

View File

@@ -31,6 +31,9 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { orgQueries } from "@app/lib/queries"; import { orgQueries } from "@app/lib/queries";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
interface UptimeAlertSectionProps { interface UptimeAlertSectionProps {
orgId: string; orgId: string;
@@ -49,6 +52,8 @@ export default function UptimeAlertSection({
}: UptimeAlertSectionProps) { }: UptimeAlertSectionProps) {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { isPaidUser } = usePaidStatus();
const isPaid = isPaidUser(tierMatrix.alertingRules);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState( const [name, setName] = useState(
@@ -219,82 +224,90 @@ export default function UptimeAlertSection({
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <PaidFeaturesAlert tiers={tierMatrix.alertingRules} />
<Label htmlFor="alert-name">Name</Label> <fieldset
<Input disabled={!isPaid}
id="alert-name" className={!isPaid ? "opacity-50 pointer-events-none" : ""}
value={name} >
onChange={(e) => setName(e.target.value)} <div className="space-y-4">
placeholder="Alert name" <div className="space-y-2">
/> <Label htmlFor="alert-name">Name</Label>
</div> <Input
<div className="space-y-2"> id="alert-name"
<Label>Notify Users</Label> value={name}
<TagInput onChange={(e) => setName(e.target.value)}
activeTagIndex={activeUserTagIndex} placeholder="Alert name"
setActiveTagIndex={setActiveUserTagIndex} />
placeholder="Select users..." </div>
size="sm" <div className="space-y-2">
tags={userTags} <Label>Notify Users</Label>
setTags={(newTags) => { <TagInput
const next = activeTagIndex={activeUserTagIndex}
typeof newTags === "function" setActiveTagIndex={setActiveUserTagIndex}
? newTags(userTags) placeholder="Select users..."
: newTags; size="sm"
setUserTags(next as Tag[]); tags={userTags}
}} setTags={(newTags) => {
enableAutocomplete const next =
autocompleteOptions={allUsers} typeof newTags === "function"
restrictTagsToAutocompleteOptions ? newTags(userTags)
allowDuplicates={false} : newTags;
sortTags setUserTags(next as Tag[]);
/> }}
</div> enableAutocomplete
<div className="space-y-2"> autocompleteOptions={allUsers}
<Label>Notify Roles</Label> restrictTagsToAutocompleteOptions
<TagInput allowDuplicates={false}
activeTagIndex={activeRoleTagIndex} sortTags
setActiveTagIndex={setActiveRoleTagIndex} />
placeholder="Select roles..." </div>
size="sm" <div className="space-y-2">
tags={roleTags} <Label>Notify Roles</Label>
setTags={(newTags) => { <TagInput
const next = activeTagIndex={activeRoleTagIndex}
typeof newTags === "function" setActiveTagIndex={setActiveRoleTagIndex}
? newTags(roleTags) placeholder="Select roles..."
: newTags; size="sm"
setRoleTags(next as Tag[]); tags={roleTags}
}} setTags={(newTags) => {
enableAutocomplete const next =
autocompleteOptions={allRoles} typeof newTags === "function"
restrictTagsToAutocompleteOptions ? newTags(roleTags)
allowDuplicates={false} : newTags;
sortTags setRoleTags(next as Tag[]);
/> }}
</div> enableAutocomplete
<div className="space-y-2"> autocompleteOptions={allRoles}
<Label>Additional Emails</Label> restrictTagsToAutocompleteOptions
<TagInput allowDuplicates={false}
activeTagIndex={activeEmailTagIndex} sortTags
setActiveTagIndex={setActiveEmailTagIndex} />
placeholder="Enter email addresses..." </div>
size="sm" <div className="space-y-2">
tags={emailTags} <Label>Additional Emails</Label>
setTags={(newTags) => { <TagInput
const next = activeTagIndex={activeEmailTagIndex}
typeof newTags === "function" setActiveTagIndex={setActiveEmailTagIndex}
? newTags(emailTags) placeholder="Enter email addresses..."
: newTags; size="sm"
setEmailTags(next as Tag[]); tags={emailTags}
}} setTags={(newTags) => {
allowDuplicates={false} const next =
sortTags typeof newTags === "function"
validateTag={(tag) => ? newTags(emailTags)
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag) : newTags;
} setEmailTags(next as Tag[]);
delimiterList={[",", "Enter"]} }}
/> allowDuplicates={false}
</div> sortTags
validateTag={(tag) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(tag)
}
delimiterList={[",", "Enter"]}
/>
</div>
</div>
</fieldset>
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
@@ -304,7 +317,7 @@ export default function UptimeAlertSection({
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={loading} loading={loading}
disabled={loading} disabled={loading || !isPaid}
> >
Create Alert Create Alert
</Button> </Button>