Compare commits

..
Author SHA1 Message Date
gorkem-bwl faedb5ba4b feat(server): generate /monitors openapi spec from Zod (spike)
The hand-maintained openapi.json has drifted significantly from the
actual API surface (PUT vs PATCH mismatches, missing query params,
stale endpoints, missing routes). This is a spike of the long-term
fix: derive the spec from the Zod validation schemas we already have.

Scope: monitors resource only. Other resources still come from the
static openapi.json — the new merger splices generated paths in and
drops the static ones the registry now owns. Migration can proceed
one resource per PR.

What this PR does:

- Adds @asteasolutions/zod-to-openapi (peerDep zod 4 satisfied).
- New src/openapi/ module:
  - registry.ts — shared OpenAPIRegistry, bearerAuth scheme.
  - schemas/common.ts — success/error envelope helpers.
  - schemas/monitor.ts — Monitor / MonitorWithChecksList / etc.
  - routes/monitor.ts — registers all 19 monitor routes against
    the existing Zod validation schemas (no validation files modified).
  - index.ts — getOpenApiSpec() merges generated monitor paths into
    the static openapi.json and drops 2 stale entries.
- src/index.ts now calls getOpenApiSpec() instead of reading
  openapi.json directly. The /api-docs Swagger UI is unchanged.

Drift fixes that fall out automatically once monitors are derived:

- PUT /monitors/{monitorId} -> PATCH (matches actual route)
- POST /monitors response 201 -> 200 (matches actual handler)
- GET /monitors/team/with-checks: 1 documented param -> 8 actual
  (this is what #3354 fixed manually; now structural)
- /monitors/bulk and /monitors/test-email removed (no such routes)
- PATCH /monitors/notifications, POST /monitors/import/json, and
  GET /monitors/{id}/geo-checks are now documented (were missing)

Verified by booting the merged spec in isolation: 16 monitor paths
emitted, all stale entries dropped, Monitor schema and bearerAuth
scheme registered.

Follow-ups (separate PRs):
- Migrate auth, statusPage, maintenanceWindow, notification, etc.
- Once all resources are migrated, delete the hand-edited
  openapi.json and ship the generator output as the canonical doc.
- Optionally promote response shapes from passthrough() Monitor
  to fully typed response Zod schemas.
2026-04-24 19:58:56 -04:00
Gorkem Cetin ed49fd41db Merge pull request #3575 from ayshaL/fix/notification-channel-typo
Fixed typo in notification
2026-04-24 19:17:58 -04:00
Aicha Lassoued 625f9b588e Fix typo in heapAllocation string 2026-04-24 22:45:45 +01:00
Aicha Lassoued 6fa73dce49 fixed typo 2026-04-24 22:24:00 +01:00
Alexander Holliday e8892af8ba Merge pull request #3573 from bluewave-labs/fix/swagger-monitors-teams-args
docs(openapi): document all query params on /monitors/team/with-checks
2026-04-24 10:43:34 -07:00
Alexander Holliday e413991e63 Merge pull request #3571 from bluewave-labs/fix/check-schema-errors-warning
fix(server): suppress mongoose reserved-key warning on Check.errors
2026-04-24 10:19:03 -07:00
Alexander Holliday e909ddd78b Merge pull request #3572 from bluewave-labs/fix/version-from-package-json
fix(client): source app version from package.json
2026-04-24 10:12:20 -07:00
Alexander Holliday 2ed5015aee Merge pull request #3570 from bluewave-labs/fix/vite-allowed-hosts
fix(client): allow configurable Vite allowed hosts
2026-04-24 09:58:27 -07:00
Alexander Holliday 6e89be2ac5 Merge pull request #3574 from bluewave-labs/fix/open-site-action-internal-url
fix(client): hide Open site action when URL is not browser-openable
2026-04-24 09:51:01 -07:00
gorkem-bwl 50fb4f47fd fix(client): hide Open site action when URL is not browser-openable
The Open site action in monitor row menus called `window.open(monitor.url)`
unconditionally, which produced unresolvable navigation for monitors
targeting internal services:
- Hardware monitors always point at a Capture agent (e.g.
  http://capture:59232/api/v1/metrics) that the user's browser can't
  reach.
- Port, ping, docker, game, grpc, and websocket monitors store hostnames
  or non-HTTP endpoints, which browsers can't load anyway.

This change hides the action where it can't possibly work:
- Removed entirely from the Infrastructure (hardware) monitors table.
- In the Uptime monitors table, only shown for http and pagespeed
  monitor types whose URL starts with http:// or https://.

A more comprehensive fix that adds a separate user-facing siteUrl
field on the Monitor schema is left for a follow-up.

Closes #3434
2026-04-23 23:38:07 -04:00
gorkem-bwl 22f1d035b7 docs(openapi): document all query params on /monitors/team/with-checks
The OpenAPI spec only listed `limit` for GET /monitors/team/with-checks,
but the route's Zod schema (getMonitorsWithChecksQueryValidation) also
accepts page, rowsPerPage, filter, field, order, type, and explain.
Users hitting the endpoint via Swagger UI would either get rejected
with "page is not allowed" (when guessing param names) or miss
pagination entirely.

Adds the missing parameters with constraints mirrored from the
validation schema. Also bumps `limit` max from 50 to 100 to match
the schema (was 100 in code, 50 in spec).

Closes #3354
2026-04-23 23:35:52 -04:00
gorkem-bwl f654310c4d fix(client): source app version from package.json
The client version shown on the Settings page was hardcoded as a
literal in vite.config.ts, while client/package.json sat at the
placeholder "0.0.0". Bumping the displayed version required editing
two unrelated places, and stale Docker client images would keep
showing the old number after upgrades.

This change makes client/package.json the single source of truth
and reads it at build time. Bumps client/package.json from 0.0.0
to 3.6.1 to preserve current behaviour.

Closes #3271
2026-04-23 23:33:41 -04:00
gorkem-bwl d44f7497a5 fix(server): suppress mongoose reserved-key warning on Check.errors
Mongoose 8 logs a reserved-key warning at boot because the Check schema
defines an `errors` field, which collides with Mongoose's internal
document `errors` accessor. The field is intentional and safe to keep,
so opt out of the warning via `suppressReservedKeysWarning: true`.

Closes #1539
2026-04-23 23:32:33 -04:00
gorkem-bwl dbccce445b fix(client): allow configurable Vite allowed hosts
Vite >=4.3 rejects requests with a Host header not in the allowlist,
which broke status-page submissions and dev-server access for users
serving Checkmate behind a reverse proxy after upgrading.

Adds VITE_ALLOWED_HOSTS env var (comma-separated list). When unset,
falls back to allowing all hosts so existing self-hosted deployments
keep working without changes.

Closes #3397
Closes #2951
2026-04-23 23:31:05 -04:00
15 changed files with 593 additions and 37 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"version": "3.6.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -80,14 +80,6 @@ export const InfraMonitorsTable = ({
const getActions = (monitor: Monitor): ActionMenuItem[] => {
return [
{
id: 1,
label: t("pages.common.monitors.actions.openSite"),
action: () => {
window.open(monitor.url, "_blank", "noreferrer");
},
closeMenu: true,
},
{
id: 2,
label: t("pages.common.monitors.actions.details"),
@@ -71,16 +71,24 @@ export const MonitorTable = ({
refetch();
};
const isBrowserOpenable = (monitor: Monitor): boolean => {
if (monitor.type !== "http" && monitor.type !== "pagespeed") return false;
return /^https?:\/\//i.test(monitor.url ?? "");
};
const getActions = (monitor: Monitor): ActionMenuItem[] => {
return [
{
const actions: ActionMenuItem[] = [];
if (isBrowserOpenable(monitor)) {
actions.push({
id: 1,
label: t("pages.common.monitors.actions.openSite"),
action: () => {
window.open(monitor.url, "_blank", "noreferrer");
},
closeMenu: true,
},
});
}
actions.push(
{
id: 2,
label: t("pages.common.monitors.actions.details"),
@@ -130,8 +138,9 @@ export const MonitorTable = ({
),
action: () => setSelectedMonitor(monitor),
closeMenu: true,
},
];
}
);
return actions;
};
const getHeaders = (chartType: string) => {
+2 -2
View File
@@ -812,7 +812,7 @@
"osMemoryLimit": "OS memory limit"
},
"gauges": {
"heapAllocation": "Heap allocaton",
"heapAllocation": "Heap allocation",
"heapUsage": "Heap usage",
"heapUtilization": "Heap utilization",
"instantCpuUsage": "Instant CPU usage",
@@ -911,7 +911,7 @@
"Let engineers know when incidents happen",
"Keep administrators informed of system changes"
],
"title": "Notification channles are used to:"
"title": "Notification channels are used to:"
},
"form": {
"accessToken": {
+12 -1
View File
@@ -3,17 +3,28 @@ import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import path from "path";
import { fileURLToPath } from "url";
import { readFileSync } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.6.1";
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
const version = pkg.version;
const allowedHosts = process.env.VITE_ALLOWED_HOSTS
? process.env.VITE_ALLOWED_HOSTS.split(",").map((h) => h.trim())
: true;
return {
base: "/",
plugins: [svgr(), react()],
server: {
host: true,
allowedHosts,
},
preview: {
host: true,
allowedHosts,
},
resolve: {
alias: {
+73 -2
View File
@@ -597,8 +597,79 @@
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 10
"maximum": 100
}
},
{
"name": "page",
"in": "query",
"description": "Zero-based page index for pagination",
"schema": {
"type": "integer",
"minimum": 0
}
},
{
"name": "rowsPerPage",
"in": "query",
"description": "Number of monitors per page",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
{
"name": "filter",
"in": "query",
"description": "Search filter value",
"schema": {
"type": "string"
}
},
{
"name": "field",
"in": "query",
"description": "Field to sort by",
"schema": {
"type": "string"
}
},
{
"name": "order",
"in": "query",
"description": "Sort order",
"schema": {
"type": "string",
"enum": ["asc", "desc"]
}
},
{
"name": "type",
"in": "query",
"description": "Filter by monitor type",
"schema": {
"oneOf": [
{
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
},
{
"type": "array",
"items": {
"type": "string",
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
}
}
]
}
},
{
"name": "explain",
"in": "query",
"description": "Include MongoDB query explain output (debug)",
"schema": {
"type": "boolean"
}
}
],
+24 -16
View File
@@ -46,6 +46,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
@@ -126,6 +127,19 @@
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"license": "MIT"
},
"node_modules/@asteasolutions/zod-to-openapi": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz",
"integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"openapi3-ts": "^4.1.2"
},
"peerDependencies": {
"zod": "^4.0.0"
}
},
"node_modules/@aws-crypto/sha256-browser": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
@@ -851,7 +865,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1471,7 +1484,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1516,7 +1528,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -4230,7 +4241,6 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -4391,7 +4401,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4629,7 +4638,6 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
@@ -5243,7 +5251,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5822,7 +5829,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6716,7 +6722,6 @@
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssnano-preset-default": "^7.0.9",
"lilconfig": "^3.1.3"
@@ -7481,7 +7486,6 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7843,7 +7847,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -9512,7 +9515,6 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -11956,6 +11958,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi3-ts": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz",
"integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yaml": "^2.8.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -12347,7 +12359,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -14370,7 +14381,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14539,7 +14549,6 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -14703,7 +14712,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+1
View File
@@ -64,6 +64,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
+1
View File
@@ -271,6 +271,7 @@ const CheckSchema = new Schema<CheckDocument>(
{
timestamps: true,
strict: false,
suppressReservedKeysWarning: true,
timeseries: {
timeField: "createdAt",
metaField: "metadata",
+2 -2
View File
@@ -3,9 +3,9 @@ import { initializeControllers } from "./config/controllers.js";
import { createApp } from "./app.js";
import { initShutdownListener } from "@/shutdown.js";
import { validateEnv } from "@/validation/envValidation.js";
import { getOpenApiSpec } from "@/openapi/index.js";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import Logger, { ILogger } from "@/utils/logger.js";
import { SettingsService } from "@/service/index.js";
@@ -20,7 +20,7 @@ const startApp = async () => {
// FE path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
const openApiSpec = getOpenApiSpec();
const frontendPath = path.join(__dirname, "..", "public");
// Create settings service (env only — DB repository injected after connect)
+52
View File
@@ -0,0 +1,52 @@
import "./registry.js";
import "./routes/monitor.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import type { JsonObject } from "swagger-ui-express";
import { registry } from "./registry.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let cached: JsonObject | null = null;
export function getOpenApiSpec(): JsonObject {
if (cached) return cached;
const staticSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../../openapi.json"), "utf8")) as Record<string, unknown>;
const generator = new OpenApiGeneratorV3(registry.definitions);
const generated = generator.generateDocument({
openapi: "3.0.0",
info: { title: "generated", version: "0.0.0" },
});
const generatedPaths = (generated.paths ?? {}) as Record<string, unknown>;
const generatedSchemas = (generated.components?.schemas ?? {}) as Record<string, unknown>;
const generatedSecuritySchemes = (generated.components?.securitySchemes ?? {}) as Record<string, unknown>;
const staticPaths = (staticSpec.paths ?? {}) as Record<string, unknown>;
const staticComponents = (staticSpec.components ?? {}) as Record<string, unknown>;
const staticSchemas = (staticComponents.schemas ?? {}) as Record<string, unknown>;
const staticSecuritySchemes = (staticComponents.securitySchemes ?? {}) as Record<string, unknown>;
// Drop static paths that the registry now owns, plus stale entries
// for routes that no longer exist in the codebase.
const owned = new Set(Object.keys(generatedPaths));
const stale = new Set(["/monitors/bulk", "/monitors/test-email"]);
const filteredStaticPaths = Object.fromEntries(Object.entries(staticPaths).filter(([key]) => !owned.has(key) && !stale.has(key)));
cached = {
...staticSpec,
paths: { ...filteredStaticPaths, ...generatedPaths },
components: {
...staticComponents,
schemas: { ...staticSchemas, ...generatedSchemas },
securitySchemes: { ...staticSecuritySchemes, ...generatedSecuritySchemes },
},
};
return cached;
}
+20
View File
@@ -0,0 +1,20 @@
import { OpenAPIRegistry, OpenApiGeneratorV3, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
extendZodWithOpenApi(z);
export const registry = new OpenAPIRegistry();
registry.registerComponent("securitySchemes", "bearerAuth", {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
});
export function generateMonitorPaths() {
const generator = new OpenApiGeneratorV3(registry.definitions);
return generator.generateDocument({
openapi: "3.0.0",
info: { title: "Checkmate generated paths", version: "0.0.0" },
});
}
+301
View File
@@ -0,0 +1,301 @@
import "../registry.js";
import { z } from "zod";
import { registry } from "../registry.js";
import { successEnvelope, successEnvelopeNoData, errorEnvelope } from "../schemas/common.js";
import {
MonitorSchema,
MonitorWithChecksListSchema,
CertificateInfoSchema,
NotificationsUpdateResultSchema,
ImportResultSchema,
} from "../schemas/monitor.js";
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation,
getCertificateParamValidation,
createMonitorBodyValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
getUptimeDetailsByIdParamValidation,
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
} from "@/validation/monitorValidation.js";
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
const bearer = [{ bearerAuth: [] }];
const tags = ["monitors"];
const json = <T extends z.ZodTypeAny>(schema: T) => ({
"application/json": { schema },
});
const standardErrors = {
"401": { description: "Unauthorized", content: json(errorEnvelope) },
"403": { description: "Forbidden", content: json(errorEnvelope) },
"500": { description: "Internal server error", content: json(errorEnvelope) },
};
registry.registerPath({
method: "get",
path: "/monitors/team",
tags,
summary: "List monitors for the caller's team",
security: bearer,
request: { query: getMonitorsByTeamIdQueryValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(z.array(MonitorSchema))) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/team/with-checks",
tags,
summary: "List team monitors with their most recent checks (paginated)",
security: bearer,
request: { query: getMonitorsWithChecksQueryValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(MonitorWithChecksListSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/team/groups",
tags,
summary: "List monitor groups for the caller's team",
security: bearer,
responses: {
"200": { description: "OK", content: json(successEnvelope(z.array(z.string()))) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/uptime/details/{monitorId}",
tags,
summary: "Get uptime details for a monitor",
security: bearer,
request: {
params: getUptimeDetailsByIdParamValidation,
query: getUptimeDetailsByIdQueryValidation,
},
responses: {
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
"400": { description: "Validation error", content: json(errorEnvelope) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/hardware/details/{monitorId}",
tags,
summary: "Get hardware metrics detail for a monitor",
security: bearer,
request: {
params: getHardwareDetailsByIdParamValidation,
query: getHardwareDetailsByIdQueryValidation,
},
responses: {
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/pagespeed/details/{monitorId}",
tags,
summary: "Get PageSpeed detail for a monitor",
security: bearer,
request: {
params: getHardwareDetailsByIdParamValidation,
query: getHardwareDetailsByIdQueryValidation,
},
responses: {
"200": { description: "OK", content: json(successEnvelopeNoData) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/{monitorId}/geo-checks",
tags,
summary: "Get geo check results for a monitor",
security: bearer,
request: {
params: getMonitorByIdParamValidation,
query: getMonitorByIdQueryValidation,
},
responses: {
"200": { description: "OK", content: json(successEnvelopeNoData) },
...standardErrors,
},
});
registry.registerPath({
method: "post",
path: "/monitors/pause/{monitorId}",
tags,
summary: "Toggle pause state for a monitor",
security: bearer,
request: { params: pauseMonitorParamValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/certificate/{monitorId}",
tags,
summary: "Get SSL certificate info for a monitor",
security: bearer,
request: { params: getCertificateParamValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(CertificateInfoSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "patch",
path: "/monitors/notifications",
tags,
summary: "Bulk update notifications across monitors",
security: bearer,
request: { body: { content: json(updateNotificationsValidation) } },
responses: {
"200": { description: "OK", content: json(successEnvelope(NotificationsUpdateResultSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "post",
path: "/monitors",
tags,
summary: "Create a new monitor",
security: bearer,
request: { body: { content: json(createMonitorBodyValidation) } },
responses: {
"200": { description: "Monitor created", content: json(successEnvelope(MonitorSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "delete",
path: "/monitors",
tags,
summary: "Delete every monitor (superadmin only)",
security: bearer,
responses: {
"200": { description: "OK", content: json(successEnvelopeNoData) },
...standardErrors,
},
});
registry.registerPath({
method: "post",
path: "/monitors/demo",
tags,
summary: "Insert preconfigured demo monitors",
security: bearer,
responses: {
"200": { description: "OK", content: json(successEnvelope(z.number())) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/export/json",
tags,
summary: "Export all team monitors as JSON",
security: bearer,
responses: {
"200": { description: "OK", content: json(successEnvelope(z.unknown())) },
...standardErrors,
},
});
registry.registerPath({
method: "post",
path: "/monitors/import/json",
tags,
summary: "Import monitors from JSON",
security: bearer,
request: { body: { content: json(importMonitorsBodyValidation) } },
responses: {
"200": { description: "OK", content: json(successEnvelope(ImportResultSchema)) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/games",
tags,
summary: "List supported game-server types",
security: bearer,
responses: {
"200": { description: "OK", content: json(successEnvelope(z.array(z.string()))) },
...standardErrors,
},
});
registry.registerPath({
method: "get",
path: "/monitors/{monitorId}",
tags,
summary: "Get a monitor by id",
security: bearer,
request: { params: getMonitorByIdParamValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
"404": { description: "Monitor not found", content: json(errorEnvelope) },
...standardErrors,
},
});
registry.registerPath({
method: "patch",
path: "/monitors/{monitorId}",
tags,
summary: "Edit a monitor",
security: bearer,
request: {
params: getMonitorByIdParamValidation,
body: { content: json(editMonitorBodyValidation) },
},
responses: {
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
"404": { description: "Monitor not found", content: json(errorEnvelope) },
...standardErrors,
},
});
registry.registerPath({
method: "delete",
path: "/monitors/{monitorId}",
tags,
summary: "Delete a monitor",
security: bearer,
request: { params: getMonitorByIdParamValidation },
responses: {
"200": { description: "OK", content: json(successEnvelope(MonitorSchema)) },
"404": { description: "Monitor not found", content: json(errorEnvelope) },
...standardErrors,
},
});
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod";
export const successEnvelope = <T extends z.ZodTypeAny>(data: T) =>
z.object({
success: z.literal(true),
msg: z.string(),
data,
});
export const successEnvelopeNoData = z.object({
success: z.literal(true),
msg: z.string(),
});
export const errorEnvelope = z.object({
success: z.literal(false),
msg: z.string(),
data: z.unknown().optional(),
});
+71
View File
@@ -0,0 +1,71 @@
import "../registry.js";
import { z } from "zod";
import { GeoContinents } from "@/types/geoCheck.js";
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
export const MonitorSchema = z
.object({
_id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
name: z.string(),
description: z.string().optional(),
type: z.enum(MonitorTypes),
url: z.string(),
port: z.number().optional(),
isActive: z.boolean(),
interval: z.number(),
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]),
statusWindowSize: z.number(),
statusWindowThreshold: z.number(),
ignoreTlsErrors: z.boolean(),
useAdvancedMatching: z.boolean(),
jsonPath: z.string().optional(),
expectedValue: z.string().optional(),
matchMethod: z.enum(MonitorMatchMethods).optional(),
notifications: z.array(z.string()),
secret: z.string().optional(),
cpuAlertThreshold: z.number(),
memoryAlertThreshold: z.number(),
diskAlertThreshold: z.number(),
tempAlertThreshold: z.number(),
selectedDisks: z.array(z.string()),
gameId: z.string().optional(),
grpcServiceName: z.string().optional(),
group: z.string().nullable().optional(),
geoCheckEnabled: z.boolean(),
geoCheckLocations: z.array(z.enum(GeoContinents)),
geoCheckInterval: z.number(),
teamId: z.string(),
userId: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})
.passthrough()
.openapi("Monitor");
export const MonitorWithChecksListSchema = z
.object({
monitors: z.array(MonitorSchema),
monitorCount: z.number(),
filteredCount: z.number(),
})
.passthrough()
.openapi("MonitorWithChecksList");
export const CertificateInfoSchema = z
.object({
certificateDate: z.string(),
})
.openapi("CertificateInfo");
export const NotificationsUpdateResultSchema = z
.object({
modifiedCount: z.number(),
})
.openapi("NotificationsUpdateResult");
export const ImportResultSchema = z
.object({
imported: z.number(),
})
.passthrough()
.openapi("ImportResult");