Openstatus www.openstatus.dev

๐Ÿ’ธ pricing update (#932)

* ๐Ÿ’ธ pricing update

* chore: versioning

* ๐Ÿคฃ add fallback

* ๐Ÿš€ circular

* ๐Ÿš€ fix server

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* feat: pricing slider (#935)

* feat: pricing slider

* chore:

* chore: wording

* fix: typo

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ’ธ

* ๐Ÿคฃ

---------

Co-authored-by: Maximilian Kaske <maximilian@kaske.org>
Co-authored-by: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com>

authored by

Thibault Le Ouay
Maximilian Kaske
Maximilian Kaske
and committed by
GitHub
d002030c 5e73937c

+2812 -1166
-1
apps/server/package.json
··· 22 22 "@openstatus/notification-pagerduty": "workspace:*", 23 23 "@openstatus/notification-slack": "workspace:*", 24 24 "@openstatus/notification-twillio-sms": "workspace:*", 25 - "@openstatus/plans": "workspace:*", 26 25 "@openstatus/tinybird": "workspace:*", 27 26 "@openstatus/tracker": "workspace:*", 28 27 "@openstatus/upstash": "workspace:*",
+2 -4
apps/server/src/checker/alerting.ts
··· 1 1 import { db, eq, schema } from "@openstatus/db"; 2 - import type { 3 - MonitorFlyRegion, 4 - MonitorStatus, 5 - } from "@openstatus/db/src/schema"; 2 + import type { MonitorStatus } from "@openstatus/db/src/schema"; 6 3 import { 7 4 selectMonitorSchema, 8 5 selectNotificationSchema, ··· 10 7 11 8 import { checkerAudit } from "../utils/audit-log"; 12 9 import { providerToFunction } from "./utils"; 10 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 13 11 14 12 export const triggerNotifications = async ({ 15 13 monitorId,
+1 -1
apps/server/src/checker/index.ts
··· 4 4 5 5 import { and, db, eq, isNull, schema } from "@openstatus/db"; 6 6 import { incidentTable } from "@openstatus/db/src/schema"; 7 - import { flyRegions } from "@openstatus/db/src/schema/monitors/constants"; 8 7 import { 9 8 monitorStatusSchema, 10 9 selectMonitorSchema, ··· 14 13 import { env } from "../env"; 15 14 import { checkerAudit } from "../utils/audit-log"; 16 15 import { triggerNotifications, upsertMonitorStatus } from "./alerting"; 16 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 17 17 18 18 export const checkerRoute = new Hono(); 19 19 const redis = Redis.fromEnv();
+1 -2
apps/server/src/env.ts
··· 1 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 1 2 import { createEnv } from "@t3-oss/env-core"; 2 3 import { z } from "zod"; 3 - 4 - import { flyRegions } from "@openstatus/db/src/schema"; 5 4 6 5 export const env = createEnv({ 7 6 server: {
+2 -3
apps/server/src/v1/index.ts
··· 3 3 import { logger } from "hono/logger"; 4 4 import { apiReference } from "@scalar/hono-api-reference"; 5 5 6 - import type { Limits } from "@openstatus/plans/src/types"; 7 - 8 6 import { handleError, handleZodError } from "../libs/errors"; 9 7 import { checkAPI } from "./check"; 10 8 import { incidentsApi } from "./incidents"; ··· 15 13 import { pagesApi } from "./pages"; 16 14 import { statusReportUpdatesApi } from "./statusReportUpdates"; 17 15 import { statusReportsApi } from "./statusReports"; 16 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 18 17 19 18 export type Variables = { 20 19 workspaceId: string; ··· 22 21 title: "Hobby" | "Starter" | "Growth" | "Pro"; 23 22 description: string; 24 23 price: number; 25 - limits: Limits; 26 24 }; 25 + limits: Limits; 27 26 }; 28 27 29 28 export const api = new OpenAPIHono<{ Variables: Variables }>({
+4 -3
apps/server/src/v1/middleware.ts
··· 2 2 import type { Context, Next } from "hono"; 3 3 4 4 import { db, eq } from "@openstatus/db"; 5 - import { workspace } from "@openstatus/db/src/schema"; 6 - import { getPlanConfig } from "@openstatus/plans"; 5 + import { selectWorkspaceSchema, workspace } from "@openstatus/db/src/schema"; 6 + import { getPlanConfig } from "@openstatus/db/src/schema/plan/utils"; 7 7 import type { Variables } from "./index"; 8 8 import { HTTPException } from "hono/http-exception"; 9 9 ··· 34 34 console.error("Workspace not found"); 35 35 throw new HTTPException(401, { message: "Unauthorized" }); 36 36 } 37 - 37 + const _work = selectWorkspaceSchema.parse(_workspace); 38 38 c.set("workspacePlan", getPlanConfig(_workspace.plan)); 39 39 c.set("workspaceId", `${result.ownerId}`); 40 + c.set("limits", _work.limits); 40 41 41 42 await next(); 42 43 }
+5 -5
apps/server/src/v1/monitors/post.ts
··· 12 12 import type { monitorsApi } from "./index"; 13 13 import { MonitorSchema } from "./schema"; 14 14 import { getAssertions } from "./utils"; 15 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 15 16 16 17 const postRoute = createRoute({ 17 18 method: "post", ··· 44 45 export function registerPostMonitor(api: typeof monitorsApi) { 45 46 return api.openapi(postRoute, async (c) => { 46 47 const workspaceId = c.get("workspaceId"); 47 - const workspacePlan = c.get("workspacePlan"); 48 - 48 + const limits = c.get("limits"); 49 49 const input = c.req.valid("json"); 50 50 const count = ( 51 51 await db ··· 60 60 .all() 61 61 )[0].count; 62 62 63 - if (count >= workspacePlan.limits.monitors) { 63 + if (count >= getLimit(limits, "monitors")) { 64 64 throw new HTTPException(403, { 65 65 message: "Upgrade for more monitors", 66 66 }); 67 67 } 68 68 69 - if (!workspacePlan.limits.periodicity.includes(input.periodicity)) { 69 + if (!getLimit(limits, "periodicity").includes(input.periodicity)) { 70 70 throw new HTTPException(403, { message: "Forbidden" }); 71 71 } 72 72 73 73 for (const region of input.regions) { 74 - if (!workspacePlan.limits.regions.includes(region)) { 74 + if (!getLimit(limits, "regions").includes(region)) { 75 75 throw new HTTPException(403, { message: "Upgrade for more region" }); 76 76 } 77 77 }
+3 -3
apps/server/src/v1/monitors/put.ts
··· 42 42 export function registerPutMonitor(api: typeof monitorsApi) { 43 43 return api.openapi(putRoute, async (c) => { 44 44 const workspaceId = c.get("workspaceId"); 45 - const workspacePlan = c.get("workspacePlan"); 45 + const limits = c.get("limits"); 46 46 const { id } = c.req.valid("param"); 47 47 const input = c.req.valid("json"); 48 48 49 - if (!workspacePlan.limits.periodicity.includes(input.periodicity)) { 49 + if (!limits.periodicity.includes(input.periodicity)) { 50 50 throw new HTTPException(403, { message: "Forbidden" }); 51 51 } 52 52 53 53 for (const region of input.regions) { 54 - if (!workspacePlan.limits.regions.includes(region)) { 54 + if (!limits.regions.includes(region)) { 55 55 throw new HTTPException(403, { message: "Upgrade for more region" }); 56 56 } 57 57 }
+7 -7
apps/server/src/v1/monitors/schema.ts
··· 1 1 import { z } from "@hono/zod-openapi"; 2 2 3 - import { 4 - flyRegions, 5 - monitorMethods, 6 - monitorPeriodicitySchema, 7 - } from "@openstatus/db/src/schema"; 3 + import { monitorMethods } from "@openstatus/db/src/schema"; 8 4 import { ZodError } from "zod"; 9 5 import { 10 6 numberCompare, 11 7 stringCompare, 12 8 } from "../../../../../packages/assertions/src/v1"; 9 + import { 10 + flyRegions, 11 + monitorPeriodicitySchema, 12 + } from "@openstatus/db/src/schema/constants"; 13 13 14 14 const statusAssertion = z 15 15 .object({ ··· 113 113 ]); 114 114 } 115 115 }, 116 - z.array(z.enum(flyRegions)), 116 + z.array(z.enum(flyRegions)) 117 117 ) 118 118 .default([]) 119 119 .openapi({ ··· 161 161 ]); 162 162 } 163 163 }, 164 - z.array(z.object({ key: z.string(), value: z.string() })).default([]), 164 + z.array(z.object({ key: z.string(), value: z.string() })).default([]) 165 165 ) 166 166 .nullish() 167 167 .openapi({
+3 -1
apps/server/src/v1/notifications/post.ts
··· 12 12 import { openApiErrorResponses } from "../../libs/errors/openapi-error-responses"; 13 13 import type { notificationsApi } from "./index"; 14 14 import { NotificationSchema } from "./schema"; 15 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 15 16 16 17 const postRoute = createRoute({ 17 18 method: "post", ··· 45 46 return api.openapi(postRoute, async (c) => { 46 47 const workspaceId = c.get("workspaceId"); 47 48 const workspacePlan = c.get("workspacePlan"); 49 + const limits = c.get("limits"); 48 50 const input = c.req.valid("json"); 49 51 50 52 if (input.provider === "sms" && workspacePlan.title === "Hobby") { ··· 59 61 .all() 60 62 )[0].count; 61 63 62 - if (count >= workspacePlan.limits["notification-channels"]) { 64 + if (count >= getLimit(limits, "notification-channels")) { 63 65 throw new HTTPException(403, { 64 66 message: "Upgrade for more notification channels", 65 67 });
+4 -3
apps/server/src/v1/pages/post.ts
··· 9 9 import { isNumberArray } from "../utils"; 10 10 import type { pagesApi } from "./index"; 11 11 import { PageSchema } from "./schema"; 12 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 12 13 13 14 const postRoute = createRoute({ 14 15 method: "post", ··· 41 42 export function registerPostPage(api: typeof pagesApi) { 42 43 return api.openapi(postRoute, async (c) => { 43 44 const workspaceId = c.get("workspaceId"); 44 - const workspacePlan = c.get("workspacePlan"); 45 + const limits = c.get("limits"); 45 46 const input = c.req.valid("json"); 46 47 47 48 const count = ( ··· 52 53 .all() 53 54 )[0].count; 54 55 55 - if (count >= workspacePlan.limits["status-pages"]) { 56 + if (count >= getLimit(limits, "status-pages")) { 56 57 throw new HTTPException(403, { 57 58 message: "Upgrade for more status pages", 58 59 }); 59 60 } 60 61 61 62 if ( 62 - workspacePlan.limits["password-protection"] === false && 63 + getLimit(limits, "password-protection") === false && 63 64 input?.passwordProtected === true 64 65 ) { 65 66 throw new HTTPException(403, {
+2 -2
apps/server/src/v1/pages/put.ts
··· 42 42 export function registerPutPage(api: typeof pagesApi) { 43 43 return api.openapi(putRoute, async (c) => { 44 44 const workspaceId = c.get("workspaceId"); 45 - const workspacePlan = c.get("workspacePlan"); 45 + const limits = c.get("limits"); 46 46 const { id } = c.req.valid("param"); 47 47 const input = c.req.valid("json"); 48 48 49 49 if ( 50 - workspacePlan.limits["password-protection"] === false && 50 + limits["password-protection"] === false && 51 51 input?.passwordProtected === true 52 52 ) { 53 53 throw new HTTPException(403, {
+16 -16
apps/server/src/v1/statusReports/post.ts
··· 1 1 import { createRoute, z } from "@hono/zod-openapi"; 2 2 3 - import { and, asc, db, eq, inArray, isNotNull, isNull } from "@openstatus/db"; 3 + import { and, db, eq, inArray, isNotNull, isNull } from "@openstatus/db"; 4 4 import { 5 5 monitor, 6 6 monitorsToStatusReport, ··· 16 16 import { isoDate } from "../utils"; 17 17 import type { statusReportsApi } from "./index"; 18 18 import { StatusReportSchema } from "./schema"; 19 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 19 20 20 21 const postRoute = createRoute({ 21 22 method: "post", ··· 59 60 return api.openapi(postRoute, async (c) => { 60 61 const input = c.req.valid("json"); 61 62 const workspaceId = c.get("workspaceId"); 62 - const workspacePlan = c.get("workspacePlan"); 63 + const limits = c.get("limits"); 63 64 64 65 const { monitorIds, date, ...rest } = input; 65 66 ··· 81 82 } 82 83 } 83 84 84 - 85 - if(rest.pageId){ 85 + if (rest.pageId) { 86 86 const _pages = await db 87 - .select() 88 - .from(page) 89 - .where( 90 - and( 91 - eq(page.workspaceId, Number(workspaceId)), 92 - eq(page.id, rest.pageId) 87 + .select() 88 + .from(page) 89 + .where( 90 + and( 91 + eq(page.workspaceId, Number(workspaceId)), 92 + eq(page.id, rest.pageId) 93 + ) 93 94 ) 94 - ) 95 - .all(); 95 + .all(); 96 96 97 - if (_pages.length !== 1) { 98 - throw new HTTPException(400, { message: "Page not found" }); 99 - } 97 + if (_pages.length !== 1) { 98 + throw new HTTPException(400, { message: "Page not found" }); 99 + } 100 100 } 101 101 102 102 const _newStatusReport = await db ··· 132 132 .returning(); 133 133 } 134 134 135 - if (workspacePlan.limits.notifications && _newStatusReport.pageId) { 135 + if (getLimit(limits, "status-subscribers") && _newStatusReport.pageId) { 136 136 const subscribers = await db 137 137 .select() 138 138 .from(pageSubscriber)
+4 -5
apps/server/src/v1/statusReports/schema.ts
··· 1 1 import { z } from "@hono/zod-openapi"; 2 2 3 - import { page, statusReportStatusSchema } from "@openstatus/db/src/schema"; 3 + import { statusReportStatusSchema } from "@openstatus/db/src/schema"; 4 4 5 5 export const ParamsSchema = z.object({ 6 6 id: z ··· 50 50 }) 51 51 .nullable(), 52 52 53 - pageId: z.number().optional().nullable().openapi({ 54 - description: "The id of the page this status report belongs to", 55 - 56 - }), 53 + pageId: z.number().optional().nullable().openapi({ 54 + description: "The id of the page this status report belongs to", 55 + }), 57 56 }); 58 57 59 58 export type StatusReportSchema = z.infer<typeof StatusReportSchema>;
-1
apps/web/package.json
··· 28 28 "@openstatus/notification-emails": "workspace:*", 29 29 "@openstatus/notification-pagerduty": "workspace:*", 30 30 "@openstatus/notification-slack": "workspace:*", 31 - "@openstatus/plans": "workspace:*", 32 31 "@openstatus/react": "workspace:*", 33 32 "@openstatus/rum": "workspace:*", 34 33 "@openstatus/tinybird": "workspace:*",
+12 -6
apps/web/src/app/(content)/features/monitoring/page.tsx
··· 3 3 import { RegionsPreset } from "@/components/monitor-dashboard/region-preset"; 4 4 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 5 5 import { marketingProductPagesConfig } from "@/config/pages"; 6 - import { flyRegions } from "@openstatus/db/src/schema"; 6 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 7 7 import type { Region } from "@openstatus/tinybird"; 8 8 import { Button } from "@openstatus/ui"; 9 9 import { allUnrelateds } from "contentlayer/generated"; ··· 15 15 import { InteractiveFeature } from "../_components/interactive-feature"; 16 16 import { mockChartData, mockResponseData } from "../mock"; 17 17 import type { Metadata } from "next"; 18 - import { defaultMetadata, ogMetadata, twitterMetadata } from "@/app/shared-metadata"; 18 + import { 19 + defaultMetadata, 20 + ogMetadata, 21 + twitterMetadata, 22 + } from "@/app/shared-metadata"; 19 23 20 24 const { description, subtitle } = marketingProductPagesConfig[0]; 21 25 const code = allUnrelateds.find( ··· 25 29 export const metadata: Metadata = { 26 30 ...defaultMetadata, 27 31 title: "API & Website Monitoring", 28 - description:'Get insights of the latency of your API and website from all over the world.', 32 + description: 33 + "Get insights of the latency of your API and website from all over the world.", 29 34 twitter: { 30 35 ...twitterMetadata, 31 36 title: "API & Website Monitoring", 32 - description:'Get insights of the latency of your API and website from all over the world.', 37 + description: 38 + "Get insights of the latency of your API and website from all over the world.", 33 39 }, 34 40 openGraph: { 35 41 ...ogMetadata, 36 42 title: "API & Website Monitoring", 37 - description:'Get insights of the latency of your API and website from all over the world.', 43 + description: 44 + "Get insights of the latency of your API and website from all over the world.", 38 45 }, 39 46 }; 40 - 41 47 42 48 export default function FeaturePage() { 43 49 return (
+1 -1
apps/web/src/app/api/checker/test/route.ts
··· 1 1 import { NextResponse } from "next/server"; 2 2 import { z } from "zod"; 3 3 4 - import { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 4 + import { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 5 5 6 6 import { checkRegion } from "@/components/ping-response-analysis/utils"; 7 7 import { payloadSchema } from "../schema";
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx
··· 20 20 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 21 21 import { api } from "@/trpc/client"; 22 22 import type { z } from "zod"; 23 - import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 23 + import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 24 24 25 25 // EXAMPLE: get the type of the response of the endpoint 26 26 // biome-ignore lint/correctness/noUnusedVariables: <explanation>
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/details/page.tsx
··· 6 6 import { EmptyState } from "@/components/dashboard/empty-state"; 7 7 import { ResponseDetails } from "@/components/monitor-dashboard/response-details"; 8 8 import { api } from "@/trpc/server"; 9 - import { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 9 + import { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 10 10 // 11 11 12 12 /**
+2 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/edit/page.tsx
··· 49 49 ) 50 50 .map(({ id }) => id), 51 51 }} 52 - plan={workspace?.plan} 52 + limits={workspace.limits} 53 53 notifications={notifications} 54 54 tags={tags} 55 55 pages={pages} 56 + plan={workspace.plan} 56 57 /> 57 58 ); 58 59 }
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 2 2 import * as React from "react"; 3 3 import * as z from "zod"; 4 4 5 - import { flyRegions } from "@openstatus/db/src/schema"; 5 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 6 6 import type { Region } from "@openstatus/tinybird"; 7 7 import { OSTinybird } from "@openstatus/tinybird"; 8 8 import { Separator } from "@openstatus/ui";
+2 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/new/page.tsx
··· 27 27 28 28 return ( 29 29 <MonitorForm 30 - plan={workspace?.plan} 31 30 defaultSection={search.success ? search.data.section : undefined} 32 31 notifications={notifications} 33 32 pages={pages} 34 33 tags={tags} 34 + limits={workspace.limits} 35 35 nextUrl="./" // back to the overview page 36 + plan={workspace.plan} 36 37 /> 37 38 ); 38 39 }
+4 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx
··· 1 1 import type { Workspace } from "@openstatus/db/src/schema"; 2 - import { getLimit } from "@openstatus/plans"; 2 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 3 + 3 4 import { Button, Separator } from "@openstatus/ui"; 4 5 import Link from "next/link"; 5 6 ··· 11 12 } 12 13 13 14 export default function ChannelTable({ workspace, disabled }: ChannelTable) { 14 - const isPagerDutyAllowed = getLimit(workspace.plan, "pagerduty"); 15 - const isSMSAllowed = getLimit(workspace.plan, "sms"); 15 + const isPagerDutyAllowed = getLimit(workspace.limits, "pagerduty"); 16 + const isSMSAllowed = getLimit(workspace.limits, "sms"); 16 17 return ( 17 18 <div className="col-span-full w-full rounded-lg border border-border border-dashed bg-background p-8"> 18 19 <h2 className="font-cal text-2xl">Channels</h2>
+3 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx
··· 2 2 import { NotificationForm } from "@/components/forms/notification/form"; 3 3 import { api } from "@/trpc/server"; 4 4 import { notificationProviderSchema } from "@openstatus/db/src/schema"; 5 - import { getLimit } from "@openstatus/plans"; 5 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 6 + 6 7 import { notFound } from "next/navigation"; 7 8 8 9 export default async function ChannelPage({ ··· 22 23 const provider = validation.data; 23 24 24 25 const allowed = 25 - provider === "sms" ? getLimit(workspace.plan, provider) : true; 26 + provider === "sms" ? getLimit(workspace.limits, provider) : true; 26 27 27 28 if (!allowed) return <ProFeatureAlert feature="SMS channel notification" />; 28 29
+5 -4
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/pagerduty/page.tsx
··· 2 2 import { NotificationForm } from "@/components/forms/notification/form"; 3 3 import { api } from "@/trpc/server"; 4 4 import { PagerDutySchema } from "@openstatus/notification-pagerduty"; 5 - import { getLimit } from "@openstatus/plans"; 5 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 6 + 6 7 import { z } from "zod"; 7 8 8 9 // REMINDER: PagerDuty requires a different workflow, thus the separate page ··· 29 30 return <div>Invalid data</div>; 30 31 } 31 32 32 - const allowed = getLimit(workspace.plan, "pagerduty"); 33 - 34 - if (!allowed) return <ProFeatureAlert feature="SMS channel notification" />; 33 + const allowed = getLimit(workspace.limits, "pagerduty"); 34 + if (!allowed) 35 + return <ProFeatureAlert feature="PagerDuty channel notification" />; 35 36 36 37 return ( 37 38 <>
+1 -4
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/billing/page.tsx
··· 1 - import { getLimits } from "@openstatus/plans"; 2 1 import { Progress, Separator } from "@openstatus/ui"; 3 2 4 3 import { api } from "@/trpc/server"; ··· 8 7 export default async function BillingPage() { 9 8 const workspace = await api.workspace.getWorkspace.query(); 10 9 const currentNumbers = await api.workspace.getCurrentWorkspaceNumbers.query(); 11 - 12 - const limits = getLimits(workspace.plan); 13 10 14 11 return ( 15 12 <div className="grid gap-4"> ··· 21 18 </div> 22 19 <div className="grid max-w-lg gap-3"> 23 20 {Object.entries(currentNumbers).map(([key, value]) => { 24 - const limit = limits[key as keyof typeof currentNumbers]; 21 + const limit = workspace.limits[key as keyof typeof currentNumbers]; 25 22 return ( 26 23 <div key={key}> 27 24 <div className="mb-1 flex items-center justify-between text-muted-foreground">
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/domain/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 3 - import { allPlans } from "@openstatus/plans"; 3 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 4 4 5 5 import { ProFeatureAlert } from "@/components/billing/pro-feature-alert"; 6 6 import { CustomDomainForm } from "@/components/forms/custom-domain-form";
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/subscribers/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 3 - import { allPlans } from "@openstatus/plans"; 3 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 4 4 5 5 import { ProFeatureAlert } from "@/components/billing/pro-feature-alert"; 6 6 import { columns } from "@/components/data-table/page-subscriber/columns";
+3
apps/web/src/app/app/[workspaceSlug]/onboarding/page.tsx
··· 16 16 }) { 17 17 const { workspaceSlug } = params; 18 18 19 + const workspace = await api.workspace.getWorkspace.query(); 19 20 const allMonitors = await api.monitor.getMonitorsByWorkspace.query(); 20 21 const allPages = await api.page.getPagesByWorkspace.query(); 21 22 const allNotifications = ··· 38 39 <MonitorForm 39 40 notifications={allNotifications} 40 41 defaultSection="request" 42 + limits={workspace.limits} 43 + plan={workspace.plan} 41 44 /> 42 45 </div> 43 46 <div className="hidden h-full md:col-span-1 md:block">
+1 -1
apps/web/src/app/play/checker/[id]/page.tsx
··· 3 3 import { redirect } from "next/navigation"; 4 4 import * as z from "zod"; 5 5 6 - import { monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 6 + import { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 7 7 import { Separator } from "@openstatus/ui"; 8 8 9 9 import {
+5 -12
apps/web/src/app/pricing/page.tsx
··· 1 1 import type { Metadata } from "next"; 2 - import Link from "next/link"; 3 2 4 3 import { Shell } from "@/components/dashboard/shell"; 5 4 import { MarketingLayout } from "@/components/layout/marketing-layout"; 6 5 import { FAQs } from "@/components/marketing/faqs"; 7 6 import { EnterpricePlan } from "@/components/marketing/pricing/enterprice-plan"; 7 + import { PricingSlider } from "@/components/marketing/pricing/pricing-slider"; 8 8 import { PricingWrapperSuspense } from "@/components/marketing/pricing/pricing-wrapper"; 9 9 import { 10 10 defaultMetadata, 11 11 ogMetadata, 12 12 twitterMetadata, 13 13 } from "../shared-metadata"; 14 + import { Separator } from "@openstatus/ui"; 14 15 15 16 export const metadata: Metadata = { 16 17 ...defaultMetadata, ··· 30 31 return ( 31 32 <MarketingLayout> 32 33 <div className="grid w-full gap-6"> 33 - <Shell className="grid w-full gap-8"> 34 + <Shell className="grid w-full gap-12"> 34 35 <div className="grid gap-3 text-center"> 35 36 <h1 className="font-cal text-4xl text-foreground">Pricing</h1> 36 37 <p className="text-muted-foreground"> ··· 38 39 </p> 39 40 </div> 40 41 <PricingWrapperSuspense /> 41 - <p className="text-muted-foreground text-sm"> 42 - Learn more about the{" "} 43 - <Link 44 - href="/blog/our-new-pricing-explained" 45 - className="text-foreground underline underline-offset-4 hover:no-underline" 46 - > 47 - decision behind the plans 48 - </Link> 49 - . 50 - </p> 42 + <Separator /> 43 + <PricingSlider /> 51 44 </Shell> 52 45 <Shell> 53 46 <EnterpricePlan />
+1 -1
apps/web/src/app/public/monitors/[id]/page.tsx
··· 2 2 import * as React from "react"; 3 3 import * as z from "zod"; 4 4 5 - import { flyRegions } from "@openstatus/db/src/schema"; 5 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 6 6 import type { Region } from "@openstatus/tinybird"; 7 7 import { OSTinybird } from "@openstatus/tinybird"; 8 8 import { Separator } from "@openstatus/ui";
+2 -2
apps/web/src/app/status-page/[domain]/_components/footer.tsx
··· 1 - import { allPlans } from "@openstatus/plans"; 2 - import type { WorkspacePlan } from "@openstatus/plans"; 1 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 2 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 3 3 4 4 import { ThemeToggle } from "@/components/theme-toggle"; 5 5
+2 -2
apps/web/src/app/status-page/[domain]/_components/header.tsx
··· 4 4 import { useSelectedLayoutSegment } from "next/navigation"; 5 5 6 6 import type { PublicPage } from "@openstatus/db/src/schema"; 7 - import { allPlans } from "@openstatus/plans"; 8 - import type { WorkspacePlan } from "@openstatus/plans"; 7 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 8 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 9 9 10 10 import { Shell } from "@/components/dashboard/shell"; 11 11 import { TabsContainer, TabsLink } from "@/components/dashboard/tabs-link";
+1 -1
apps/web/src/app/status-page/[domain]/_components/password-protected.tsx
··· 1 - import type { WorkspacePlan } from "@openstatus/plans"; 1 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 2 2 3 3 import { Shell } from "@/components/dashboard/shell"; 4 4 import { Footer } from "../_components/footer";
+1 -2
apps/web/src/app/status-page/[domain]/monitors/[id]/page.tsx
··· 2 2 import * as React from "react"; 3 3 import * as z from "zod"; 4 4 5 - import { flyRegions } from "@openstatus/db/src/schema"; 5 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 6 6 import type { Region } from "@openstatus/tinybird"; 7 7 import { OSTinybird } from "@openstatus/tinybird"; 8 8 import { Separator } from "@openstatus/ui"; ··· 74 74 75 75 const { period, quantile, interval, regions } = search.data; 76 76 77 - console.log(regions.length); 78 77 // TODO: work it out easier 79 78 const intervalMinutes = getMinutesByInterval(interval); 80 79 const periodicityMinutes = getMinutesByInterval(monitor.periodicity);
+1 -1
apps/web/src/components/billing/pro-feature-alert.tsx
··· 5 5 import { useParams } from "next/navigation"; 6 6 7 7 import { Alert, AlertDescription, AlertTitle } from "@openstatus/ui"; 8 - import type { WorkspacePlan } from "@openstatus/plans"; 8 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 9 9 10 10 interface Props { 11 11 feature: string;
+1 -1
apps/web/src/components/billing/pro-feature-hover-card.tsx
··· 3 3 import { useState } from "react"; 4 4 5 5 import { workspacePlanHierarchy } from "@openstatus/db/src/schema"; 6 - import type { WorkspacePlan } from "@openstatus/plans"; 6 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 7 7 import { HoverCard, HoverCardContent, HoverCardTrigger } from "@openstatus/ui"; 8 8 9 9 function upgradePlan(current: WorkspacePlan, required: WorkspacePlan) {
-729
apps/web/src/components/forms/monitor-form.tsx
··· 1 - // REMINDER: legacy form - please use /forms/monitor/form.tsx 2 - 3 - "use client"; 4 - 5 - import { zodResolver } from "@hookform/resolvers/zod"; 6 - import { Check, ChevronsUpDown, Wand2, X } from "lucide-react"; 7 - import { useRouter } from "next/navigation"; 8 - import * as React from "react"; 9 - import { useFieldArray, useForm } from "react-hook-form"; 10 - 11 - import type { 12 - InsertMonitor, 13 - Notification, 14 - WorkspacePlan, 15 - } from "@openstatus/db/src/schema"; 16 - import { 17 - flyRegions, 18 - insertMonitorSchema, 19 - monitorMethods, 20 - monitorMethodsSchema, 21 - monitorPeriodicitySchema, 22 - } from "@openstatus/db/src/schema"; 23 - import { getLimit } from "@openstatus/plans"; 24 - import { 25 - Accordion, 26 - AccordionContent, 27 - AccordionItem, 28 - AccordionTrigger, 29 - Button, 30 - Checkbox, 31 - Command, 32 - CommandEmpty, 33 - CommandGroup, 34 - CommandInput, 35 - CommandItem, 36 - CommandList, 37 - Dialog, 38 - DialogTrigger, 39 - Form, 40 - FormControl, 41 - FormDescription, 42 - FormField, 43 - FormItem, 44 - FormLabel, 45 - FormMessage, 46 - Input, 47 - Popover, 48 - PopoverContent, 49 - PopoverTrigger, 50 - Select, 51 - SelectContent, 52 - SelectItem, 53 - SelectTrigger, 54 - SelectValue, 55 - Switch, 56 - Textarea, 57 - Tooltip, 58 - TooltipContent, 59 - TooltipProvider, 60 - TooltipTrigger, 61 - } from "@openstatus/ui"; 62 - import { flyRegionsDict } from "@openstatus/utils"; 63 - 64 - import { LoadingAnimation } from "@/components/loading-animation"; 65 - import { FailedPingAlertConfirmation } from "@/components/modals/failed-ping-alert-confirmation"; 66 - import type { RegionChecker } from "@/components/ping-response-analysis/utils"; 67 - import { toastAction } from "@/lib/toast"; 68 - import { cn } from "@/lib/utils"; 69 - import { api } from "@/trpc/client"; 70 - 71 - const cronJobs = [ 72 - { value: "30s", label: "30 seconds" }, 73 - { value: "1m", label: "1 minute" }, 74 - { value: "5m", label: "5 minutes" }, 75 - { value: "10m", label: "10 minutes" }, 76 - { value: "30m", label: "30 minutes" }, 77 - { value: "1h", label: "1 hour" }, 78 - ] as const; 79 - 80 - interface Props { 81 - defaultValues?: InsertMonitor; 82 - plan?: WorkspacePlan; 83 - notifications?: Notification[]; 84 - nextUrl?: string; 85 - } 86 - 87 - export function MonitorForm({ 88 - defaultValues, 89 - plan = "free", 90 - notifications, 91 - nextUrl, 92 - }: Props) { 93 - const form = useForm<InsertMonitor>({ 94 - resolver: zodResolver(insertMonitorSchema), 95 - defaultValues: { 96 - url: defaultValues?.url || "", 97 - name: defaultValues?.name || "", 98 - description: defaultValues?.description || "", 99 - periodicity: defaultValues?.periodicity || "30m", 100 - active: defaultValues?.active ?? true, 101 - id: defaultValues?.id || 0, 102 - regions: defaultValues?.regions || getLimit("free", "regions"), 103 - headers: defaultValues?.headers?.length 104 - ? defaultValues?.headers 105 - : [{ key: "", value: "" }], 106 - body: defaultValues?.body ?? "", 107 - method: defaultValues?.method ?? "GET", 108 - notifications: defaultValues?.notifications ?? [], 109 - }, 110 - }); 111 - const router = useRouter(); 112 - const [isPending, startTransition] = React.useTransition(); 113 - const [isTestPending, startTestTransition] = React.useTransition(); 114 - const [pingFailed, setPingFailed] = React.useState(false); 115 - const [openDialog, setOpenDialog] = React.useState(false); 116 - const watchMethod = form.watch("method"); 117 - 118 - const { fields, append, remove } = useFieldArray({ 119 - name: "headers", 120 - control: form.control, 121 - }); 122 - 123 - const handleDataUpdateOrInsertion = async (props: InsertMonitor) => { 124 - try { 125 - if (defaultValues) { 126 - await api.monitor.update.mutate(props); 127 - } else { 128 - await api.monitor.create.mutate(props); 129 - } 130 - if (nextUrl) { 131 - router.push(nextUrl); 132 - } 133 - router.refresh(); 134 - toastAction("saved"); 135 - } catch (_error) { 136 - toastAction("error"); 137 - } 138 - }; 139 - 140 - const onSubmit = ({ ...props }: InsertMonitor) => { 141 - startTransition(async () => { 142 - const pingResult = await pingEndpoint(); 143 - if (!pingResult) { 144 - setPingFailed(true); 145 - return; 146 - } 147 - await handleDataUpdateOrInsertion(props); 148 - }); 149 - }; 150 - 151 - const validateJSON = (value?: string) => { 152 - if (!value) return; 153 - try { 154 - const obj = JSON.parse(value) as Record<string, unknown>; 155 - form.clearErrors("body"); 156 - return obj; 157 - } catch (_e) { 158 - form.setError("body", { 159 - message: "Not a valid JSON object", 160 - }); 161 - return false; 162 - } 163 - }; 164 - 165 - const onPrettifyJSON = () => { 166 - const body = form.getValues("body"); 167 - const obj = validateJSON(body); 168 - if (obj) { 169 - const pretty = JSON.stringify(obj, undefined, 4); 170 - form.setValue("body", pretty); 171 - } 172 - }; 173 - 174 - const pingEndpoint = async () => { 175 - const { url, body, method, headers } = form.getValues(); 176 - const res = await fetch("/api/checker/test", { 177 - method: "POST", 178 - headers: new Headers({ 179 - "Content-Type": "application/json", 180 - }), 181 - body: JSON.stringify({ url, body, method, headers }), 182 - }); 183 - const data = (await res.json()) as RegionChecker; 184 - return data; 185 - }; 186 - 187 - const sendTestPing = () => { 188 - if (isTestPending) { 189 - return; 190 - } 191 - const { url } = form.getValues(); 192 - if (!url) { 193 - toastAction("test-warning-empty-url"); 194 - return; 195 - } 196 - 197 - startTestTransition(async () => { 198 - try { 199 - const data = await pingEndpoint(); 200 - if (data.status >= 200 && data.status < 300) { 201 - toastAction("test-success"); 202 - } else { 203 - toastAction("test-error"); 204 - } 205 - } catch { 206 - toastAction("error"); 207 - } 208 - }); 209 - }; 210 - 211 - const periodicityLimit = getLimit(plan, "periodicity"); 212 - const notificationLimit = getLimit(plan, "notification-channels"); 213 - const notificationLimitReached = notifications 214 - ? notifications.length >= notificationLimit 215 - : false; 216 - 217 - return ( 218 - <Dialog open={openDialog} onOpenChange={(val) => setOpenDialog(val)}> 219 - <Form {...form}> 220 - <form 221 - onSubmit={form.handleSubmit(onSubmit)} 222 - className="grid w-full gap-6" 223 - > 224 - <div className="grid gap-4 sm:grid-cols-3"> 225 - <div className="my-1.5 flex flex-col gap-2"> 226 - <p className="font-semibold text-sm leading-none"> 227 - Endpoint Check 228 - </p> 229 - <p className="text-muted-foreground text-sm"> 230 - The easiest way to get started. 231 - </p> 232 - </div> 233 - <div className="grid gap-6 sm:col-span-2"> 234 - <FormField 235 - control={form.control} 236 - name="name" 237 - render={({ field }) => ( 238 - <FormItem> 239 - <FormLabel>Name</FormLabel> 240 - <FormControl> 241 - <Input placeholder="Documenso" {...field} /> 242 - </FormControl> 243 - <FormDescription> 244 - Displayed on the status page. 245 - </FormDescription> 246 - <FormMessage /> 247 - </FormItem> 248 - )} 249 - /> 250 - <FormField 251 - control={form.control} 252 - name="url" 253 - render={({ field }) => ( 254 - <FormItem> 255 - <FormLabel>URL</FormLabel> 256 - <FormControl> 257 - {/* <InputWithAddons 258 - leading="https://" 259 - placeholder="documenso.com/api/health" 260 - {...field} 261 - /> */} 262 - <Input 263 - placeholder="https://documenso.com/api/health" 264 - {...field} 265 - /> 266 - </FormControl> 267 - <FormDescription> 268 - Here is the URL you want to monitor.{" "} 269 - </FormDescription> 270 - <FormMessage /> 271 - </FormItem> 272 - )} 273 - /> 274 - </div> 275 - </div> 276 - <Accordion type="single" collapsible> 277 - <AccordionItem value="http-request-settings"> 278 - <AccordionTrigger>HTTP Request Settings</AccordionTrigger> 279 - <AccordionContent> 280 - <div className="grid gap-4 sm:grid-cols-3"> 281 - <div className="my-1.5 flex flex-col gap-2"> 282 - <p className="font-semibold text-sm leading-none"> 283 - Custom Request 284 - </p> 285 - <p className="text-muted-foreground text-sm"> 286 - If your endpoint is secured, add additional configuration 287 - to the request we send. 288 - </p> 289 - </div> 290 - <div className="grid gap-6 sm:col-span-2 sm:grid-cols-4"> 291 - <FormField 292 - control={form.control} 293 - name="method" 294 - render={({ field }) => ( 295 - <FormItem className="sm:col-span-1 sm:self-baseline"> 296 - <FormLabel>Method</FormLabel> 297 - <Select 298 - onValueChange={(value) => { 299 - field.onChange(monitorMethodsSchema.parse(value)); 300 - form.resetField("body", { defaultValue: "" }); 301 - }} 302 - defaultValue={field.value} 303 - > 304 - <FormControl> 305 - <SelectTrigger> 306 - <SelectValue placeholder="Select" /> 307 - </SelectTrigger> 308 - </FormControl> 309 - <SelectContent> 310 - {monitorMethods.map((method) => ( 311 - <SelectItem key={method} value={method}> 312 - {method} 313 - </SelectItem> 314 - ))} 315 - </SelectContent> 316 - </Select> 317 - <FormMessage /> 318 - </FormItem> 319 - )} 320 - /> 321 - <div className="space-y-2 sm:col-span-full"> 322 - <FormLabel>Request Header</FormLabel> 323 - {fields.map((field, index) => ( 324 - <div key={field.id} className="grid grid-cols-6 gap-6"> 325 - <FormField 326 - control={form.control} 327 - name={`headers.${index}.key`} 328 - render={({ field }) => ( 329 - <FormItem className="col-span-2"> 330 - <FormControl> 331 - <Input placeholder="key" {...field} /> 332 - </FormControl> 333 - </FormItem> 334 - )} 335 - /> 336 - <div className="col-span-4 flex items-center space-x-2"> 337 - <FormField 338 - control={form.control} 339 - name={`headers.${index}.value`} 340 - render={({ field }) => ( 341 - <FormItem className="w-full"> 342 - <FormControl> 343 - <Input placeholder="value" {...field} /> 344 - </FormControl> 345 - </FormItem> 346 - )} 347 - /> 348 - <Button 349 - size="icon" 350 - variant="ghost" 351 - type="button" 352 - onClick={() => remove(Number(field.id))} 353 - > 354 - <X className="h-4 w-4" /> 355 - </Button> 356 - </div> 357 - </div> 358 - ))} 359 - <div> 360 - <Button 361 - type="button" 362 - variant="outline" 363 - onClick={() => append({ key: "", value: "" })} 364 - > 365 - Add Custom Header 366 - </Button> 367 - </div> 368 - </div> 369 - {watchMethod === "POST" && ( 370 - <div className="sm:col-span-full"> 371 - <FormField 372 - control={form.control} 373 - name="body" 374 - render={({ field }) => ( 375 - <FormItem> 376 - <div className="flex items-end justify-between"> 377 - <FormLabel>Body</FormLabel> 378 - <TooltipProvider> 379 - <Tooltip> 380 - <TooltipTrigger asChild> 381 - <Button 382 - type="button" 383 - variant="ghost" 384 - size="icon" 385 - onClick={onPrettifyJSON} 386 - > 387 - <Wand2 className="h-4 w-4" /> 388 - </Button> 389 - </TooltipTrigger> 390 - <TooltipContent> 391 - <p>Prettify JSON</p> 392 - </TooltipContent> 393 - </Tooltip> 394 - </TooltipProvider> 395 - </div> 396 - <FormControl> 397 - <Textarea 398 - rows={8} 399 - placeholder='{ "hello": "world" }' 400 - {...field} 401 - /> 402 - </FormControl> 403 - <FormDescription> 404 - Write your json payload. 405 - </FormDescription> 406 - <FormMessage /> 407 - </FormItem> 408 - )} 409 - /> 410 - </div> 411 - )} 412 - </div> 413 - </div> 414 - </AccordionContent> 415 - </AccordionItem> 416 - <AccordionItem value="advanced-settings"> 417 - <AccordionTrigger>Advanced Settings</AccordionTrigger> 418 - <AccordionContent> 419 - <div className="grid gap-4 sm:grid-cols-3"> 420 - <div className="my-1.5 flex flex-col gap-2"> 421 - <p className="font-semibold text-sm leading-none"> 422 - More Configurations 423 - </p> 424 - <p className="text-muted-foreground text-sm"> 425 - Make it your own. Contact us if you wish for more and we 426 - will implement it! 427 - </p> 428 - </div> 429 - <div className="grid gap-6 sm:col-span-2 sm:grid-cols-2"> 430 - <FormField 431 - control={form.control} 432 - name="periodicity" 433 - render={({ field }) => ( 434 - <FormItem className="sm:col-span-1 sm:self-baseline"> 435 - <FormLabel>Frequency</FormLabel> 436 - <Select 437 - onValueChange={(value) => 438 - field.onChange( 439 - monitorPeriodicitySchema.parse(value) 440 - ) 441 - } 442 - defaultValue={field.value} 443 - > 444 - <FormControl> 445 - <SelectTrigger> 446 - <SelectValue placeholder="How often should it check your endpoint?" /> 447 - </SelectTrigger> 448 - </FormControl> 449 - <SelectContent> 450 - {cronJobs.map(({ label, value }) => ( 451 - <SelectItem 452 - key={value} 453 - value={value} 454 - disabled={!periodicityLimit.includes(value)} 455 - > 456 - {label} 457 - </SelectItem> 458 - ))} 459 - </SelectContent> 460 - </Select> 461 - <FormDescription> 462 - Frequency of how often your endpoint will be pinged. 463 - </FormDescription> 464 - <FormMessage /> 465 - </FormItem> 466 - )} 467 - /> 468 - <FormField 469 - control={form.control} 470 - name="regions" 471 - render={({ field }) => { 472 - const numberOfSelectedRegions = 473 - field.value?.length || 0; 474 - function renderText() { 475 - if (numberOfSelectedRegions === 0) 476 - return "Select region"; 477 - if (numberOfSelectedRegions === flyRegions.length) 478 - return "All regions"; 479 - return `${numberOfSelectedRegions} regions`; 480 - } 481 - return ( 482 - <FormItem className="sm:col-span-1 sm:self-baseline"> 483 - <FormLabel>Regions</FormLabel> 484 - <Popover> 485 - <PopoverTrigger asChild> 486 - <FormControl> 487 - <Button 488 - variant="outline" 489 - role="combobox" 490 - className={cn( 491 - "h-10 w-full justify-between", 492 - !field.value && "text-muted-foreground" 493 - )} 494 - > 495 - {renderText()} 496 - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 497 - </Button> 498 - </FormControl> 499 - </PopoverTrigger> 500 - <PopoverContent className="w-full p-0"> 501 - <Command> 502 - <CommandInput placeholder="Select a region..." /> 503 - <CommandList> 504 - <CommandEmpty> 505 - No regions found. 506 - </CommandEmpty> 507 - <CommandGroup className="max-h-[150px] overflow-y-scroll"> 508 - {Object.keys(flyRegionsDict).map( 509 - (region) => { 510 - const { code, location } = 511 - flyRegionsDict[ 512 - region as keyof typeof flyRegionsDict 513 - ]; 514 - const isSelected = 515 - field.value?.includes(code); 516 - return ( 517 - <CommandItem 518 - value={code} 519 - key={code} 520 - onSelect={() => { 521 - const currentRegions = 522 - form.getValues("regions") || 523 - []; 524 - form.setValue( 525 - "regions", 526 - currentRegions.includes(code) 527 - ? currentRegions.filter( 528 - (r) => r !== code 529 - ) 530 - : [...currentRegions, code] 531 - ); 532 - }} 533 - > 534 - <Check 535 - className={cn( 536 - "mr-2 h-4 w-4", 537 - isSelected 538 - ? "opacity-100" 539 - : "opacity-0" 540 - )} 541 - /> 542 - {location} 543 - </CommandItem> 544 - ); 545 - } 546 - )} 547 - </CommandGroup> 548 - </CommandList> 549 - </Command> 550 - </PopoverContent> 551 - </Popover> 552 - <FormDescription> 553 - Select your regions. If none, region will be 554 - random. 555 - </FormDescription> 556 - <FormMessage /> 557 - </FormItem> 558 - ); 559 - }} 560 - /> 561 - <FormField 562 - control={form.control} 563 - name="description" 564 - render={({ field }) => ( 565 - <FormItem className="sm:col-span-2"> 566 - <FormLabel>Description</FormLabel> 567 - <FormControl> 568 - <Input 569 - placeholder="Determines the api health of our services." 570 - {...field} 571 - /> 572 - </FormControl> 573 - <FormDescription> 574 - Provide your users with information about it. 575 - </FormDescription> 576 - <FormMessage /> 577 - </FormItem> 578 - )} 579 - /> 580 - <FormField 581 - control={form.control} 582 - name="active" 583 - render={({ field }) => ( 584 - <FormItem className="flex flex-row items-center justify-between sm:col-span-full"> 585 - <div className="space-y-0.5"> 586 - <FormLabel>Active</FormLabel> 587 - <FormDescription> 588 - This will start ping your endpoint on based on the 589 - selected frequence. 590 - </FormDescription> 591 - </div> 592 - <FormControl> 593 - <Switch 594 - checked={field.value || false} 595 - onCheckedChange={(value) => field.onChange(value)} 596 - /> 597 - </FormControl> 598 - <FormMessage /> 599 - </FormItem> 600 - )} 601 - /> 602 - </div> 603 - </div> 604 - </AccordionContent> 605 - </AccordionItem> 606 - <AccordionItem value="notification-settings"> 607 - <AccordionTrigger>Notification Settings</AccordionTrigger> 608 - <AccordionContent> 609 - <div className="grid gap-4 sm:grid-cols-3"> 610 - <div className="my-1.5 flex flex-col gap-2"> 611 - <p className="font-semibold text-sm leading-none">Alerts</p> 612 - <p className="text-muted-foreground text-sm"> 613 - How do you want to get informed if things break? 614 - </p> 615 - </div> 616 - <div className="grid gap-6 sm:col-span-2"> 617 - <FormField 618 - control={form.control} 619 - name="notifications" 620 - render={() => ( 621 - <FormItem> 622 - <div className="mb-4"> 623 - <FormLabel className="text-base"> 624 - Notifications 625 - </FormLabel> 626 - <FormDescription> 627 - Select the notification channels you want to be 628 - informed. 629 - </FormDescription> 630 - </div> 631 - {notifications?.map((item) => ( 632 - <FormField 633 - key={item.id} 634 - control={form.control} 635 - name="notifications" 636 - render={({ field }) => { 637 - return ( 638 - <FormItem 639 - key={item.id} 640 - className="flex flex-row items-start space-x-3 space-y-0" 641 - > 642 - <FormControl> 643 - <Checkbox 644 - checked={field.value?.includes(item.id)} 645 - onCheckedChange={(checked) => { 646 - return checked 647 - ? field.onChange([ 648 - ...(field.value || []), 649 - item.id, 650 - ]) 651 - : field.onChange( 652 - field.value?.filter( 653 - (value) => value !== item.id 654 - ) 655 - ); 656 - }} 657 - /> 658 - </FormControl> 659 - <div className="space-y-1 leading-none"> 660 - <FormLabel className="font-normal"> 661 - {item.name} 662 - </FormLabel> 663 - <FormDescription> 664 - {item.provider} 665 - </FormDescription> 666 - </div> 667 - </FormItem> 668 - ); 669 - }} 670 - /> 671 - ))} 672 - <FormMessage /> 673 - <div className="sm:col-span-2 sm:col-start-1"> 674 - <DialogTrigger asChild> 675 - <Button 676 - variant="outline" 677 - disabled={notificationLimitReached} 678 - > 679 - Add Notifications 680 - </Button> 681 - </DialogTrigger> 682 - </div> 683 - </FormItem> 684 - )} 685 - /> 686 - </div> 687 - </div> 688 - </AccordionContent> 689 - </AccordionItem> 690 - </Accordion> 691 - <div className="grid justify-end gap-3"> 692 - <div className="flex flex-col gap-6 sm:col-span-full sm:flex-row sm:justify-end"> 693 - <Button 694 - type="button" 695 - variant="secondary" 696 - className="w-full sm:w-auto" 697 - size="lg" 698 - onClick={sendTestPing} 699 - > 700 - {!isTestPending ? ( 701 - "Test Request" 702 - ) : ( 703 - <LoadingAnimation variant="inverse" /> 704 - )} 705 - </Button> 706 - <Button 707 - className="w-full sm:w-auto" 708 - size="lg" 709 - disabled={isPending} 710 - > 711 - {!isPending ? "Confirm" : <LoadingAnimation />} 712 - </Button> 713 - </div> 714 - <div className="flex w-full justify-end"> 715 - <p className="text-muted-foreground text-xs"> 716 - We test your endpoint connection on submit. 717 - </p> 718 - </div> 719 - </div> 720 - </form> 721 - </Form> 722 - <FailedPingAlertConfirmation 723 - monitor={form.getValues()} 724 - {...{ pingFailed, setPingFailed }} 725 - onConfirm={handleDataUpdateOrInsertion} 726 - /> 727 - </Dialog> 728 - ); 729 - }
+14 -11
apps/web/src/components/forms/monitor/form.tsx
··· 4 4 import { usePathname, useRouter } from "next/navigation"; 5 5 import * as React from "react"; 6 6 import { useForm } from "react-hook-form"; 7 - import { getLimit } from "@openstatus/plans"; 7 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 8 8 9 9 import * as assertions from "@openstatus/assertions"; 10 10 import type { 11 11 InsertMonitor, 12 - MonitorFlyRegion, 13 12 MonitorTag, 14 13 Notification, 15 14 Page, ··· 38 37 import { SectionRequests } from "./section-requests"; 39 38 import { SectionScheduling } from "./section-scheduling"; 40 39 import { SectionStatusPage } from "./section-status-page"; 40 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 41 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 41 42 42 43 interface Props { 43 44 defaultSection?: string; 45 + limits: Limits; 46 + plan: WorkspacePlan; 44 47 defaultValues?: InsertMonitor; 45 - plan?: WorkspacePlan; 46 48 notifications?: Notification[]; 47 49 tags?: MonitorTag[]; 48 50 pages?: Page[]; ··· 55 57 export function MonitorForm({ 56 58 defaultSection, 57 59 defaultValues, 58 - plan = "free", 59 60 notifications, 60 61 pages, 61 62 tags, 62 63 nextUrl, 64 + limits, 65 + plan, 63 66 withTestButton = true, 64 67 }: Props) { 65 68 const _assertions = defaultValues?.assertions ··· 74 77 periodicity: defaultValues?.periodicity || "30m", 75 78 active: defaultValues?.active ?? true, 76 79 id: defaultValues?.id || 0, 77 - regions: defaultValues?.regions || getLimit("free", "regions"), 80 + regions: defaultValues?.regions || getLimit(limits, "regions"), 78 81 headers: defaultValues?.headers?.length 79 82 ? defaultValues?.headers 80 83 : [{ key: "", value: "" }], ··· 253 256 onKeyDown={(e) => e.key === "Enter" && e.preventDefault()} 254 257 className="flex w-full flex-col gap-6" 255 258 > 256 - <General {...{ form, plan, tags }} /> 259 + <General {...{ form, tags }} /> 257 260 <Tabs 258 261 defaultValue={defaultSection} 259 262 className="w-full" ··· 291 294 ) : null} 292 295 </TabsList> 293 296 <TabsContent value="request"> 294 - <SectionRequests {...{ form, plan, pingEndpoint }} /> 297 + <SectionRequests {...{ form, pingEndpoint }} /> 295 298 </TabsContent> 296 299 <TabsContent value="assertions"> 297 300 <SectionAssertions {...{ form }} /> 298 301 </TabsContent> 299 302 <TabsContent value="scheduling"> 300 - <SectionScheduling {...{ form, plan }} /> 303 + <SectionScheduling {...{ form, limits, plan }} /> 301 304 </TabsContent> 302 305 <TabsContent value="notifications"> 303 - <SectionNotifications {...{ form, plan, notifications }} /> 306 + <SectionNotifications {...{ form, notifications }} /> 304 307 </TabsContent> 305 308 <TabsContent value="status-page"> 306 - <SectionStatusPage {...{ form, plan, pages }} /> 309 + <SectionStatusPage {...{ form, pages }} /> 307 310 </TabsContent> 308 311 {defaultValues?.id ? ( 309 312 <TabsContent value="danger"> ··· 313 316 </Tabs> 314 317 <div className="grid gap-4 sm:flex sm:items-start sm:justify-end"> 315 318 {withTestButton ? ( 316 - <RequestTestButton {...{ form, pingEndpoint }} /> 319 + <RequestTestButton {...{ form, limits, pingEndpoint }} /> 317 320 ) : null} 318 321 <SaveButton 319 322 isPending={isPending}
-1
apps/web/src/components/forms/monitor/general.tsx
··· 24 24 25 25 interface Props { 26 26 form: UseFormReturn<InsertMonitor>; 27 - plan: WorkspacePlan; 28 27 tags?: MonitorTag[]; 29 28 } 30 29
+10 -8
apps/web/src/components/forms/monitor/request-test-button.tsx
··· 5 5 import type { UseFormReturn } from "react-hook-form"; 6 6 7 7 import { deserialize } from "@openstatus/assertions"; 8 - import type { 9 - InsertMonitor, 10 - MonitorFlyRegion, 11 - } from "@openstatus/db/src/schema"; 12 - import { flyRegions } from "@openstatus/db/src/schema"; 8 + import type { InsertMonitor } from "@openstatus/db/src/schema"; 9 + import { 10 + flyRegions, 11 + type MonitorFlyRegion, 12 + } from "@openstatus/db/src/schema/constants"; 13 13 import { 14 14 Button, 15 15 Dialog, ··· 33 33 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 34 34 import type { RegionChecker } from "@/components/ping-response-analysis/utils"; 35 35 import { toast, toastAction } from "@/lib/toast"; 36 - import { getLimit } from "@openstatus/plans"; 36 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 37 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 37 38 38 39 interface Props { 39 40 form: UseFormReturn<InsertMonitor>; 41 + limits: Limits; 40 42 pingEndpoint( 41 43 region?: MonitorFlyRegion 42 44 ): Promise<{ data?: RegionChecker; error?: string }>; 43 45 } 44 46 45 - export function RequestTestButton({ form, pingEndpoint }: Props) { 47 + export function RequestTestButton({ form, pingEndpoint, limits }: Props) { 46 48 const [check, setCheck] = React.useState< 47 49 { data: RegionChecker; error?: string } | undefined 48 50 >(); ··· 79 81 80 82 const { statusAssertions, headerAssertions } = form.getValues(); 81 83 82 - const regions = getLimit("free", "regions"); 84 + const regions = getLimit(limits, "regions"); 83 85 84 86 return ( 85 87 <Dialog open={!!check} onOpenChange={() => setCheck(undefined)}>
+1 -2
apps/web/src/components/forms/monitor/section-notifications.tsx
··· 22 22 23 23 interface Props { 24 24 form: UseFormReturn<InsertMonitor>; 25 - plan: WorkspacePlan; 26 25 notifications?: Notification[]; 27 26 } 28 27 29 - export function SectionNotifications({ form, plan, notifications }: Props) { 28 + export function SectionNotifications({ form, notifications }: Props) { 30 29 return ( 31 30 <div className="grid w-full gap-4"> 32 31 {/* <div className="grid gap-1">
-1
apps/web/src/components/forms/monitor/section-requests.tsx
··· 35 35 36 36 interface Props { 37 37 form: UseFormReturn<InsertMonitor>; 38 - plan: WorkspacePlan; 39 38 } 40 39 41 40 // TODO: add Dialog with response informations when pingEndpoint!
+8 -9
apps/web/src/components/forms/monitor/section-scheduling.tsx
··· 3 3 import type { UseFormReturn } from "react-hook-form"; 4 4 5 5 import type { InsertMonitor, WorkspacePlan } from "@openstatus/db/src/schema"; 6 - import { 7 - flyRegions, 8 - monitorPeriodicitySchema, 9 - } from "@openstatus/db/src/schema"; 10 - import { getLimit } from "@openstatus/plans"; 6 + import { monitorPeriodicitySchema } from "@openstatus/db/src/schema/constants"; 7 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 8 + 11 9 import { 12 10 FormControl, 13 11 FormDescription, ··· 25 23 26 24 import { CheckboxLabel } from "../shared/checkbox-label"; 27 25 import { SectionHeader } from "../shared/section-header"; 26 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 28 27 29 28 // TODO: centralize in a shared file! 30 29 const cronJobs = [ ··· 38 37 39 38 interface Props { 40 39 form: UseFormReturn<InsertMonitor>; 40 + limits: Limits; 41 41 plan: WorkspacePlan; 42 42 } 43 43 44 - export function SectionScheduling({ form, plan }: Props) { 45 - const periodicityLimit = getLimit(plan, "periodicity"); 46 - const regionsLimit = getLimit(plan, "regions"); 47 - console.log(form.getValues()); 44 + export function SectionScheduling({ form, limits, plan }: Props) { 45 + const periodicityLimit = getLimit(limits, "periodicity"); 46 + const regionsLimit = getLimit(limits, "regions"); 48 47 return ( 49 48 <div className="grid w-full gap-4"> 50 49 <SectionHeader
+2 -3
apps/web/src/components/forms/monitor/section-status-page.tsx
··· 22 22 23 23 interface Props { 24 24 form: UseFormReturn<InsertMonitor>; 25 - plan: WorkspacePlan; 26 25 pages?: Page[]; 27 26 } 28 27 ··· 89 88 ]) 90 89 : field.onChange( 91 90 field.value?.filter( 92 - (value) => value !== item.id, 93 - ), 91 + (value) => value !== item.id 92 + ) 94 93 ); 95 94 }} 96 95 >
+8 -8
apps/web/src/components/forms/notification/config.ts
··· 4 4 } from "@openstatus/db/src/schema"; 5 5 import { sendTestDiscordMessage } from "@openstatus/notification-discord"; 6 6 import { sendTestSlackMessage } from "@openstatus/notification-slack"; 7 - import { allPlans, plans } from "@openstatus/plans"; 8 - 7 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 8 + import { workspacePlans } from "@openstatus/db/src/schema/workspaces/constants"; 9 9 export function getDefaultProviderData(defaultValues?: InsertNotification) { 10 10 if (!defaultValues?.provider) return ""; // FIXME: input can empty - needs to be undefined 11 11 return JSON.parse(defaultValues?.data || "{}")[defaultValues?.provider]; ··· 24 24 placeholder: "dev@documenso.com", 25 25 setupDocLink: null, 26 26 sendTest: null, 27 - plans: plans, 27 + plans: workspacePlans, 28 28 }; 29 29 30 30 case "slack": ··· 35 35 setupDocLink: 36 36 "https://api.slack.com/messaging/webhooks#getting_started", 37 37 sendTest: sendTestSlackMessage, 38 - plans: plans, 38 + plans: workspacePlans, 39 39 }; 40 40 41 41 case "discord": ··· 45 45 placeholder: "https://discord.com/api/webhooks/{channelId}/xxx...", 46 46 setupDocLink: "https://support.discord.com/hc/en-us/articles/228383668", 47 47 sendTest: sendTestDiscordMessage, 48 - plans: plans, 48 + plans: workspacePlans, 49 49 }; 50 50 case "sms": 51 51 return { ··· 54 54 placeholder: "+123456789", 55 55 setupDocLink: null, 56 56 sendTest: null, 57 - plans: plans.filter((plan) => allPlans[plan].limits.sms), 57 + plans: workspacePlans.filter((plan) => allPlans[plan].limits.sms), 58 58 }; 59 59 60 60 case "pagerduty": ··· 65 65 setupDocLink: 66 66 "https://docs.openstatus.dev/synthetic/features/notification/pagerduty", 67 67 sendTest: null, 68 - plans: plans.filter((plan) => allPlans[plan].limits.pagerduty), 68 + plans: workspacePlans.filter((plan) => allPlans[plan].limits.pagerduty), 69 69 }; 70 70 71 71 default: ··· 75 75 placeholder: "xxxx", 76 76 setupDocLink: `https://docs.openstatus.dev/integrations/${provider}`, 77 77 send: null, 78 - plans: plans, 78 + plans: workspacePlans, 79 79 }; 80 80 } 81 81 }
+4 -4
apps/web/src/components/marketing/pricing/pricing-plan-radio.tsx
··· 1 1 "use client"; 2 2 3 3 import { useRouter } from "next/navigation"; 4 - 5 - import { allPlans, plans } from "@openstatus/plans"; 4 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 5 + import { workspacePlans } from "@openstatus/db/src/schema/workspaces/constants"; 6 6 import { Label, RadioGroup, RadioGroupItem } from "@openstatus/ui"; 7 7 8 8 import useUpdateSearchParams from "@/hooks/use-update-search-params"; ··· 20 20 router.replace(`?${searchParams}`, { scroll: false }); 21 21 }} 22 22 > 23 - {plans.map((key) => ( 23 + {workspacePlans.map((key) => ( 24 24 <div key={key}> 25 25 <RadioGroupItem value={key} id={key} className="peer sr-only" /> 26 26 <Label 27 27 htmlFor={key} 28 28 className={cn( 29 29 "flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 [&:has([data-state=checked])]:border-primary peer-data-[state=checked]:border-primary hover:bg-accent hover:text-accent-foreground", 30 - key === "team" && "bg-muted/50", 30 + key === "team" && "bg-muted/50" 31 31 )} 32 32 > 33 33 <span className="text-sm capitalize">{key}</span>
+114
apps/web/src/components/marketing/pricing/pricing-slider.tsx
··· 1 + "use client"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + import { InputWithAddons, Slider } from "@openstatus/ui"; 5 + import { useMemo, useState } from "react"; 6 + 7 + const MAX_REGIONS = 35; 8 + 9 + const slides = [ 10 + { 11 + key: "30s", 12 + value: 2 * 60 * 24 * 30, 13 + }, 14 + { 15 + key: "1m", 16 + value: 60 * 24 * 30, 17 + }, 18 + { 19 + key: "5m", 20 + value: 12 * 24 * 30, 21 + }, 22 + { 23 + key: "10m", 24 + value: 6 * 24 * 30, 25 + }, 26 + { 27 + key: "30m", 28 + value: 2 * 24 * 30, 29 + }, 30 + { 31 + key: "1h", 32 + value: 24 * 30, 33 + }, 34 + ]; 35 + 36 + export function PricingSlider() { 37 + const [index, setIndex] = useState<number[]>([2]); 38 + const [inputValue, setInputValue] = useState<number>(6); 39 + const region = useMemo(() => slides[index[0]].value, [index]); 40 + const total = useMemo(() => region * inputValue, [region, inputValue]); 41 + 42 + return ( 43 + <div className="grid gap-4"> 44 + <div className="grid gap-8 sm:grid-cols-3"> 45 + <div className="grid gap-2 sm:col-span-2"> 46 + <h4 className="font-semibold text-2xl">Total requests per monitor</h4> 47 + <p className="text-muted-foreground"> 48 + Check how many requests you will make with OpenStatus for a single 49 + monitor over the selected time period. 50 + </p> 51 + </div> 52 + <div className="flex justify-end"> 53 + <div className="min-w-36 max-w-min"> 54 + <InputWithAddons 55 + type="number" 56 + min={0} 57 + max={MAX_REGIONS} 58 + step={1} 59 + trailing="regions" 60 + className="text-right font-mono" 61 + value={inputValue} 62 + onChange={(e) => 63 + setInputValue(Number.parseInt(e.target.value) || 0) 64 + } 65 + /> 66 + </div> 67 + </div> 68 + </div> 69 + <div className="grid gap-8 sm:grid-cols-3"> 70 + <div className="mt-2 grid w-full gap-2 sm:col-span-2"> 71 + <Slider 72 + min={0} 73 + max={slides.length - 1} 74 + step={1} 75 + minStepsBetweenThumbs={1} 76 + value={index} 77 + onValueChange={setIndex} 78 + /> 79 + <div className="flex items-center justify-between"> 80 + {slides.map((slide, i) => ( 81 + // TODO: make them clickable 82 + <div 83 + key={slide.key} 84 + className={cn( 85 + "text-left font-mono text-muted-foreground text-xs" 86 + )} 87 + > 88 + {slide.key} 89 + </div> 90 + ))} 91 + </div> 92 + </div> 93 + <div> 94 + <p className="mt-0.5 text-right text-sm"> 95 + <span className="font-medium font-mono"> 96 + {new Intl.NumberFormat("us").format(region).toString()} pings{" "} 97 + </span>{" "} 98 + <span className="font-normal text-muted-foreground"> 99 + / region / month 100 + </span> 101 + </p> 102 + </div> 103 + </div> 104 + <div> 105 + <p className="text-right"> 106 + <span className="font-medium font-mono text-lg"> 107 + {new Intl.NumberFormat("us").format(total).toString()} pings{" "} 108 + </span>{" "} 109 + <span className="font-normal text-muted-foreground">/ month</span> 110 + </p> 111 + </div> 112 + </div> 113 + ); 114 + }
+13 -11
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 4 4 import { useRouter } from "next/navigation"; 5 5 import { Fragment } from "react"; 6 6 7 - import type { WorkspacePlan } from "@openstatus/plans"; 8 - import { 9 - allPlans, 10 - plans as defaultPlans, 11 - pricingTableConfig, 12 - } from "@openstatus/plans"; 7 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 8 + import { pricingTableConfig } from "../../../config/pricing-table"; 9 + import { workspacePlans } from "@openstatus/db/src/schema/workspaces/constants"; 13 10 import { 14 11 Badge, 15 12 Button, ··· 24 21 25 22 import { LoadingAnimation } from "@/components/loading-animation"; 26 23 import { cn } from "@/lib/utils"; 24 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 27 25 28 26 export function PricingTable({ 29 - plans = defaultPlans, 27 + plans = workspacePlans, 30 28 currentPlan, 31 29 events, 32 30 isLoading, ··· 57 55 key={key} 58 56 className={cn( 59 57 "h-auto px-3 py-3 align-bottom text-foreground", 60 - key === "team" ? "bg-muted/30" : "bg-background", 58 + key === "team" ? "bg-muted/30" : "bg-background" 61 59 )} 62 60 > 63 61 <p className="sticky top-0 mb-2 font-cal text-2xl"> ··· 140 138 } 141 139 if (typeof limitValue === "number") { 142 140 return ( 143 - <span className="font-mono">{limitValue}</span> 141 + <span className="font-mono"> 142 + {new Intl.NumberFormat("us") 143 + .format(limitValue) 144 + .toString()} 145 + </span> 144 146 ); 145 147 } 146 148 if ( ··· 157 159 key={key} 158 160 className={cn( 159 161 "p-3", 160 - plan.key === "team" && "bg-muted/30", 162 + plan.key === "team" && "bg-muted/30" 161 163 )} 162 164 > 163 165 {renderContent()} ··· 169 171 })} 170 172 </Fragment> 171 173 ); 172 - }, 174 + } 173 175 )} 174 176 </TableBody> 175 177 </Table>
+1 -1
apps/web/src/components/marketing/pricing/pricing-wrapper.tsx
··· 2 2 3 3 import { useSearchParams } from "next/navigation"; 4 4 5 - import type { WorkspacePlan } from "@openstatus/plans"; 5 + import type { WorkspacePlan } from "@openstatus/db/src/schema/workspaces/validation"; 6 6 7 7 import { Suspense } from "react"; 8 8 import { PricingPlanRadio } from "./pricing-plan-radio";
+2 -2
apps/web/src/components/ping-response-analysis/select-region.tsx
··· 2 2 3 3 import { usePathname, useRouter } from "next/navigation"; 4 4 5 - import type { MonitorFlyRegion } from "@openstatus/db/src/schema"; 6 - import { flyRegions } from "@openstatus/db/src/schema"; 5 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 6 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 7 7 import { 8 8 Label, 9 9 Select,
+5 -2
apps/web/src/components/ping-response-analysis/utils.ts
··· 1 1 import { Redis } from "@upstash/redis"; 2 2 import { z } from "zod"; 3 3 4 - import { flyRegions, monitorFlyRegionSchema } from "@openstatus/db/src/schema"; 5 - import type { MonitorFlyRegion } from "@openstatus/db/src/schema"; 4 + import { 5 + flyRegions, 6 + monitorFlyRegionSchema, 7 + } from "@openstatus/db/src/schema/constants"; 8 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 6 9 import { flyRegionsDict } from "@openstatus/utils"; 7 10 8 11 export function latencyFormatter(value: number) {
-1
packages/api/package.json
··· 12 12 "@openstatus/db": "workspace:*", 13 13 "@openstatus/emails": "workspace:*", 14 14 "@openstatus/error": "workspace:*", 15 - "@openstatus/plans": "workspace:*", 16 15 "@openstatus/tinybird": "workspace:*", 17 16 "@t3-oss/env-core": "0.7.0", 18 17 "@trpc/client": "10.45.2",
+8 -8
packages/api/src/router/invitation.ts
··· 9 9 user, 10 10 usersToWorkspaces, 11 11 } from "@openstatus/db/src/schema"; 12 - import { allPlans } from "@openstatus/plans"; 12 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 13 13 14 14 import { trackNewInvitation } from "../analytics"; 15 15 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; ··· 34 34 where: and( 35 35 eq(invitation.workspaceId, opts.ctx.workspace.id), 36 36 gte(invitation.expiresAt, new Date()), 37 - isNull(invitation.acceptedAt), 37 + isNull(invitation.acceptedAt) 38 38 ), 39 39 }) 40 40 ).length; ··· 60 60 61 61 if (process.env.NODE_ENV === "development") { 62 62 console.log( 63 - `>>>> Invitation token: http://localhost:3000/app/invite?token=${token} <<<< `, 63 + `>>>> Invitation token: http://localhost:3000/app/invite?token=${token} <<<< ` 64 64 ); 65 65 } else { 66 66 await fetch("https://api.resend.com/emails", { ··· 104 104 .where( 105 105 and( 106 106 eq(invitation.id, opts.input.id), 107 - eq(invitation.workspaceId, opts.ctx.workspace.id), 108 - ), 107 + eq(invitation.workspaceId, opts.ctx.workspace.id) 108 + ) 109 109 ) 110 110 .run(); 111 111 }), ··· 115 115 where: and( 116 116 eq(invitation.workspaceId, opts.ctx.workspace.id), 117 117 gte(invitation.expiresAt, new Date()), 118 - isNull(invitation.acceptedAt), 118 + isNull(invitation.acceptedAt) 119 119 ), 120 120 }); 121 121 return _invitations; ··· 144 144 z.object({ 145 145 message: z.string(), 146 146 data: selectWorkspaceSchema.optional(), 147 - }), 147 + }) 148 148 ) 149 149 .mutation(async (opts) => { 150 150 const _invitation = await opts.ctx.db.query.invitation.findFirst({ 151 151 where: and( 152 152 eq(invitation.token, opts.input.token), 153 - isNull(invitation.acceptedAt), 153 + isNull(invitation.acceptedAt) 154 154 ), 155 155 with: { 156 156 workspace: true,
+1 -1
packages/api/src/router/monitor.ts
··· 24 24 selectNotificationSchema, 25 25 selectPublicMonitorSchema, 26 26 } from "@openstatus/db/src/schema"; 27 - import { allPlans } from "@openstatus/plans"; 27 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 28 28 29 29 import { trackNewMonitor } from "../analytics"; 30 30 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
+15 -23
packages/api/src/router/notification.ts
··· 11 11 selectMonitorSchema, 12 12 selectNotificationSchema, 13 13 } from "@openstatus/db/src/schema"; 14 - import { getLimit } from "@openstatus/plans"; 15 14 16 15 import { SchemaError } from "@openstatus/error"; 17 16 import { trackNewNotification } from "../analytics"; ··· 24 23 .mutation(async (opts) => { 25 24 const { monitors, ...props } = opts.input; 26 25 27 - const notificationLimit = getLimit( 28 - opts.ctx.workspace.plan, 29 - "notification-channels", 30 - ); 26 + const notificationLimit = 27 + opts.ctx.workspace.limits["notification-channels"]; 31 28 32 29 const notificationNumber = ( 33 30 await opts.ctx.db.query.notification.findMany({ ··· 97 94 .where( 98 95 and( 99 96 eq(notification.id, opts.input.id), 100 - eq(notification.workspaceId, opts.ctx.workspace.id), 101 - ), 97 + eq(notification.workspaceId, opts.ctx.workspace.id) 98 + ) 102 99 ) 103 100 .returning() 104 101 .get(); ··· 109 106 const allMonitors = await opts.ctx.db.query.monitor.findMany({ 110 107 where: and( 111 108 eq(monitor.workspaceId, opts.ctx.workspace.id), 112 - inArray(monitor.id, monitors), 109 + inArray(monitor.id, monitors) 113 110 ), 114 111 }); 115 112 ··· 125 122 .select() 126 123 .from(notificationsToMonitors) 127 124 .where( 128 - eq(notificationsToMonitors.notificationId, currentNotification.id), 125 + eq(notificationsToMonitors.notificationId, currentNotification.id) 129 126 ) 130 127 .all(); 131 128 ··· 139 136 .where( 140 137 and( 141 138 inArray(notificationsToMonitors.monitorId, removedMonitors), 142 - eq( 143 - notificationsToMonitors.notificationId, 144 - currentNotification.id, 145 - ), 146 - ), 139 + eq(notificationsToMonitors.notificationId, currentNotification.id) 140 + ) 147 141 ); 148 142 } 149 143 ··· 170 164 .where( 171 165 and( 172 166 eq(notification.id, opts.input.id), 173 - eq(notification.id, opts.input.id), 174 - ), 167 + eq(notification.id, opts.input.id) 168 + ) 175 169 ) 176 170 .run(); 177 171 }), ··· 183 177 where: and( 184 178 eq(notification.id, opts.input.id), 185 179 eq(notification.id, opts.input.id), 186 - eq(notification.workspaceId, opts.ctx.workspace.id), 180 + eq(notification.workspaceId, opts.ctx.workspace.id) 187 181 ), 188 182 // FIXME: plural 189 183 with: { monitor: { with: { monitor: true } } }, ··· 193 187 monitor: z.array( 194 188 z.object({ 195 189 monitor: selectMonitorSchema, 196 - }), 190 + }) 197 191 ), 198 192 }); 199 193 ··· 213 207 monitor: z.array( 214 208 z.object({ 215 209 monitor: selectMonitorSchema, 216 - }), 210 + }) 217 211 ), 218 212 }); 219 213 ··· 221 215 }), 222 216 223 217 isNotificationLimitReached: protectedProcedure.query(async (opts) => { 224 - const notificationLimit = getLimit( 225 - opts.ctx.workspace.plan, 226 - "notification-channels", 227 - ); 218 + const notificationLimit = 219 + opts.ctx.workspace.limits["notification-channels"]; 228 220 const notificationNumbers = ( 229 221 await opts.ctx.db.query.notification.findMany({ 230 222 where: eq(notification.workspaceId, opts.ctx.workspace.id),
+20 -20
packages/api/src/router/page.ts
··· 25 25 statusReport, 26 26 workspace, 27 27 } from "@openstatus/db/src/schema"; 28 - import { allPlans } from "@openstatus/plans"; 28 + import { allPlans } from "@openstatus/db/src/schema/plan/config"; 29 29 30 30 import { trackNewPage } from "../analytics"; 31 31 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; ··· 75 75 where: and( 76 76 inArray(monitor.id, monitorIds), 77 77 eq(monitor.workspaceId, opts.ctx.workspace.id), 78 - isNull(monitor.deletedAt), 78 + isNull(monitor.deletedAt) 79 79 ), 80 80 }); 81 81 ··· 105 105 const firstPage = await opts.ctx.db.query.page.findFirst({ 106 106 where: and( 107 107 eq(page.id, opts.input.id), 108 - eq(page.workspaceId, opts.ctx.workspace.id), 108 + eq(page.workspaceId, opts.ctx.workspace.id) 109 109 ), 110 110 with: { 111 111 monitorsToPages: { ··· 142 142 .where( 143 143 and( 144 144 eq(page.id, pageInput.id), 145 - eq(page.workspaceId, opts.ctx.workspace.id), 146 - ), 145 + eq(page.workspaceId, opts.ctx.workspace.id) 146 + ) 147 147 ) 148 148 .returning() 149 149 .get(); ··· 154 154 where: and( 155 155 inArray(monitor.id, monitorIds), 156 156 eq(monitor.workspaceId, opts.ctx.workspace.id), 157 - isNull(monitor.deletedAt), 157 + isNull(monitor.deletedAt) 158 158 ), 159 159 }); 160 160 ··· 183 183 .where( 184 184 and( 185 185 inArray(monitorsToPages.monitorId, removedMonitors), 186 - eq(monitorsToPages.pageId, currentPage.id), 187 - ), 186 + eq(monitorsToPages.pageId, currentPage.id) 187 + ) 188 188 ); 189 189 } 190 190 ··· 212 212 .where( 213 213 and( 214 214 eq(page.id, opts.input.id), 215 - eq(page.workspaceId, opts.ctx.workspace.id), 216 - ), 215 + eq(page.workspaceId, opts.ctx.workspace.id) 216 + ) 217 217 ) 218 218 .run(); 219 219 }), ··· 225 225 maintenancesToPages: { 226 226 where: and( 227 227 lte(maintenance.from, new Date()), 228 - gte(maintenance.to, new Date()), 228 + gte(maintenance.to, new Date()) 229 229 ), 230 230 }, 231 231 }, ··· 261 261 .leftJoin(monitor, eq(monitorsToPages.monitorId, monitor.id)) 262 262 .where( 263 263 // make sur only active monitors are returned! 264 - and(eq(monitorsToPages.pageId, result.id), eq(monitor.active, true)), 264 + and(eq(monitorsToPages.pageId, result.id), eq(monitor.active, true)) 265 265 ) 266 266 .all(); 267 267 268 268 const monitorsId = monitorsToPagesResult.map( 269 - ({ monitors_to_pages }) => monitors_to_pages.monitorId, 269 + ({ monitors_to_pages }) => monitors_to_pages.monitorId 270 270 ); 271 271 272 272 const monitorsToStatusReportResult = ··· 279 279 : []; 280 280 281 281 const monitorStatusReportIds = monitorsToStatusReportResult.map( 282 - ({ statusReportId }) => statusReportId, 282 + ({ statusReportId }) => statusReportId 283 283 ); 284 284 285 285 const statusReportIds = Array.from(new Set([...monitorStatusReportIds])); ··· 307 307 and( 308 308 inArray(monitor.id, monitorsId), 309 309 eq(monitor.active, true), 310 - isNull(monitor.deletedAt), 311 - ), // REMINDER: this is hardcoded 310 + isNull(monitor.deletedAt) 311 + ) // REMINDER: this is hardcoded 312 312 ) 313 313 .all() 314 314 : []; ··· 321 321 .where( 322 322 inArray( 323 323 incidentTable.monitorId, 324 - monitors.map((m) => m.id), 325 - ), 324 + monitors.map((m) => m.id) 325 + ) 326 326 ) 327 327 .all() 328 328 : []; ··· 361 361 // had filter on some words we want to keep for us 362 362 if ( 363 363 ["api", "app", "www", "docs", "checker", "time", "help"].includes( 364 - opts.input.slug, 364 + opts.input.slug 365 365 ) 366 366 ) { 367 367 return false; ··· 374 374 375 375 addCustomDomain: protectedProcedure 376 376 .input( 377 - z.object({ customDomain: z.string().toLowerCase(), pageId: z.number() }), 377 + z.object({ customDomain: z.string().toLowerCase(), pageId: z.number() }) 378 378 ) 379 379 .mutation(async (opts) => { 380 380 // TODO Add some check ?
+3 -3
packages/api/src/router/stripe/utils.ts
··· 20 20 monthly: { 21 21 priceIds: { 22 22 test: "price_1NdurjBXJcTfzsyJdAzIxXnT", 23 - production: "price_1OUvJvBXJcTfzsyJMA07Uew7", 23 + production: "price_1PdJ3lBXJcTfzsyJHgqcfVG9", 24 24 }, 25 25 }, 26 26 }, ··· 31 31 monthly: { 32 32 priceIds: { 33 33 test: "price_1OVHQDBXJcTfzsyJjfiXl10Y", 34 - production: "price_1OWM4TBXJcTfzsyJBdu71AhE", 34 + production: "price_1PdJ5MBXJcTfzsyJqWXeMUEQ", 35 35 }, 36 36 }, 37 37 }, ··· 42 42 monthly: { 43 43 priceIds: { 44 44 test: "price_1OVHPlBXJcTfzsyJvPlB1kNb", 45 - production: "price_1OWM5kBXJcTfzsyJ6Z2bUWcQ", 45 + production: "price_1PdJ4eBXJcTfzsyJ0hsHMAxh", 46 46 }, 47 47 }, 48 48 },
+4 -2
packages/api/src/router/stripe/webhook.ts
··· 9 9 import { createTRPCRouter, publicProcedure } from "../../trpc"; 10 10 import { stripe } from "./shared"; 11 11 import { getPlanFromPriceId } from "./utils"; 12 + import { getLimits } from "@openstatus/db/src/schema/plan/utils"; 12 13 13 14 const webhookProcedure = publicProcedure.input( 14 15 z.object({ ··· 22 23 }), 23 24 type: z.string(), 24 25 }), 25 - }), 26 + }) 26 27 ); 27 28 28 29 export const webhookRouter = createTRPCRouter({ ··· 35 36 }); 36 37 } 37 38 const subscription = await stripe.subscriptions.retrieve( 38 - session.subscription, 39 + session.subscription 39 40 ); 40 41 const customerId = 41 42 typeof subscription.customer === "string" ··· 67 68 subscriptionId: subscription.id, 68 69 endsAt: new Date(subscription.current_period_end * 1000), 69 70 paidUntil: new Date(subscription.current_period_end * 1000), 71 + limits: JSON.stringify(getLimits(plan.plan)), 70 72 }) 71 73 .where(eq(workspace.id, result.id)) 72 74 .run();
+4 -4
packages/api/src/router/tinybird/index.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { flyRegions } from "@openstatus/db/src/schema"; 4 3 import { OSTinybird } from "@openstatus/tinybird"; 5 4 6 5 import { env } from "../../env"; 7 6 import { createTRPCRouter, protectedProcedure } from "../../trpc"; 7 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 8 8 9 9 const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 10 10 ··· 29 29 url: z.string().url().optional(), 30 30 region: z.enum(flyRegions).optional(), 31 31 cronTimestamp: z.number().int().optional(), 32 - }), 32 + }) 33 33 ) 34 34 .query(async (opts) => { 35 35 return await tb.endpointResponseDetails("7d")(opts.input); ··· 52 52 dsn: z.string(), 53 53 path: z.string(), 54 54 period: z.enum(["24h", "7d", "30d"]), 55 - }), 55 + }) 56 56 ) 57 57 .query(async (opts) => { 58 58 return await tb.applicationRUMMetricsForPath()(opts.input); ··· 63 63 dsn: z.string(), 64 64 path: z.string(), 65 65 period: z.enum(["24h", "7d", "30d"]), 66 - }), 66 + }) 67 67 ) 68 68 .query(async (opts) => { 69 69 return await tb.applicationSessionMetricsPerPath()(opts.input);
+7 -7
packages/api/src/router/workspace.ts
··· 16 16 workspace, 17 17 workspacePlanSchema, 18 18 } from "@openstatus/db/src/schema"; 19 - import type { Limits } from "@openstatus/plans"; 19 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 20 20 21 21 import { createTRPCRouter, protectedProcedure } from "../trpc"; 22 22 ··· 124 124 await opts.ctx.db.query.usersToWorkspaces.findFirst({ 125 125 where: and( 126 126 eq(usersToWorkspaces.userId, opts.ctx.user.id), 127 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 127 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 128 128 ), 129 129 }); 130 130 ··· 141 141 .where( 142 142 and( 143 143 eq(usersToWorkspaces.userId, opts.input.id), 144 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 145 - ), 144 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 145 + ) 146 146 ) 147 147 .run(); 148 148 }), ··· 154 154 await opts.ctx.db.query.usersToWorkspaces.findFirst({ 155 155 where: and( 156 156 eq(usersToWorkspaces.userId, opts.ctx.user.id), 157 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 157 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) 158 158 ), 159 159 }); 160 160 ··· 209 209 .where( 210 210 and( 211 211 eq(monitor.workspaceId, opts.ctx.workspace.id), 212 - isNotNull(monitor.deletedAt), 213 - ), 212 + isNotNull(monitor.deletedAt) 213 + ) 214 214 ); 215 215 const pages = await tx 216 216 .select({ count: sql<number>`count(*)` })
+1
packages/db/drizzle/0035_open_the_professor.sql
··· 1 + ALTER TABLE `workspace` ADD `limits` text DEFAULT '{}' NOT NULL;
+2194
packages/db/drizzle/meta/0035_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "d42f0027-40b6-4d43-9d76-160e0a6e35e7", 5 + "prevId": "145f2782-3cb0-4066-9842-881a17636999", 6 + "tables": { 7 + "status_report_to_monitors": { 8 + "name": "status_report_to_monitors", 9 + "columns": { 10 + "monitor_id": { 11 + "name": "monitor_id", 12 + "type": "integer", 13 + "primaryKey": false, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "status_report_id": { 18 + "name": "status_report_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "created_at": { 25 + "name": "created_at", 26 + "type": "integer", 27 + "primaryKey": false, 28 + "notNull": false, 29 + "autoincrement": false, 30 + "default": "(strftime('%s', 'now'))" 31 + } 32 + }, 33 + "indexes": {}, 34 + "foreignKeys": { 35 + "status_report_to_monitors_monitor_id_monitor_id_fk": { 36 + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", 37 + "tableFrom": "status_report_to_monitors", 38 + "tableTo": "monitor", 39 + "columnsFrom": [ 40 + "monitor_id" 41 + ], 42 + "columnsTo": [ 43 + "id" 44 + ], 45 + "onDelete": "cascade", 46 + "onUpdate": "no action" 47 + }, 48 + "status_report_to_monitors_status_report_id_status_report_id_fk": { 49 + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", 50 + "tableFrom": "status_report_to_monitors", 51 + "tableTo": "status_report", 52 + "columnsFrom": [ 53 + "status_report_id" 54 + ], 55 + "columnsTo": [ 56 + "id" 57 + ], 58 + "onDelete": "cascade", 59 + "onUpdate": "no action" 60 + } 61 + }, 62 + "compositePrimaryKeys": { 63 + "status_report_to_monitors_monitor_id_status_report_id_pk": { 64 + "columns": [ 65 + "monitor_id", 66 + "status_report_id" 67 + ], 68 + "name": "status_report_to_monitors_monitor_id_status_report_id_pk" 69 + } 70 + }, 71 + "uniqueConstraints": {} 72 + }, 73 + "status_report": { 74 + "name": "status_report", 75 + "columns": { 76 + "id": { 77 + "name": "id", 78 + "type": "integer", 79 + "primaryKey": true, 80 + "notNull": true, 81 + "autoincrement": false 82 + }, 83 + "status": { 84 + "name": "status", 85 + "type": "text", 86 + "primaryKey": false, 87 + "notNull": true, 88 + "autoincrement": false 89 + }, 90 + "title": { 91 + "name": "title", 92 + "type": "text(256)", 93 + "primaryKey": false, 94 + "notNull": true, 95 + "autoincrement": false 96 + }, 97 + "workspace_id": { 98 + "name": "workspace_id", 99 + "type": "integer", 100 + "primaryKey": false, 101 + "notNull": false, 102 + "autoincrement": false 103 + }, 104 + "page_id": { 105 + "name": "page_id", 106 + "type": "integer", 107 + "primaryKey": false, 108 + "notNull": false, 109 + "autoincrement": false 110 + }, 111 + "created_at": { 112 + "name": "created_at", 113 + "type": "integer", 114 + "primaryKey": false, 115 + "notNull": false, 116 + "autoincrement": false, 117 + "default": "(strftime('%s', 'now'))" 118 + }, 119 + "updated_at": { 120 + "name": "updated_at", 121 + "type": "integer", 122 + "primaryKey": false, 123 + "notNull": false, 124 + "autoincrement": false, 125 + "default": "(strftime('%s', 'now'))" 126 + } 127 + }, 128 + "indexes": {}, 129 + "foreignKeys": { 130 + "status_report_workspace_id_workspace_id_fk": { 131 + "name": "status_report_workspace_id_workspace_id_fk", 132 + "tableFrom": "status_report", 133 + "tableTo": "workspace", 134 + "columnsFrom": [ 135 + "workspace_id" 136 + ], 137 + "columnsTo": [ 138 + "id" 139 + ], 140 + "onDelete": "no action", 141 + "onUpdate": "no action" 142 + }, 143 + "status_report_page_id_page_id_fk": { 144 + "name": "status_report_page_id_page_id_fk", 145 + "tableFrom": "status_report", 146 + "tableTo": "page", 147 + "columnsFrom": [ 148 + "page_id" 149 + ], 150 + "columnsTo": [ 151 + "id" 152 + ], 153 + "onDelete": "no action", 154 + "onUpdate": "no action" 155 + } 156 + }, 157 + "compositePrimaryKeys": {}, 158 + "uniqueConstraints": {} 159 + }, 160 + "status_report_update": { 161 + "name": "status_report_update", 162 + "columns": { 163 + "id": { 164 + "name": "id", 165 + "type": "integer", 166 + "primaryKey": true, 167 + "notNull": true, 168 + "autoincrement": false 169 + }, 170 + "status": { 171 + "name": "status", 172 + "type": "text(4)", 173 + "primaryKey": false, 174 + "notNull": true, 175 + "autoincrement": false 176 + }, 177 + "date": { 178 + "name": "date", 179 + "type": "integer", 180 + "primaryKey": false, 181 + "notNull": true, 182 + "autoincrement": false 183 + }, 184 + "message": { 185 + "name": "message", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": true, 189 + "autoincrement": false 190 + }, 191 + "status_report_id": { 192 + "name": "status_report_id", 193 + "type": "integer", 194 + "primaryKey": false, 195 + "notNull": true, 196 + "autoincrement": false 197 + }, 198 + "created_at": { 199 + "name": "created_at", 200 + "type": "integer", 201 + "primaryKey": false, 202 + "notNull": false, 203 + "autoincrement": false, 204 + "default": "(strftime('%s', 'now'))" 205 + }, 206 + "updated_at": { 207 + "name": "updated_at", 208 + "type": "integer", 209 + "primaryKey": false, 210 + "notNull": false, 211 + "autoincrement": false, 212 + "default": "(strftime('%s', 'now'))" 213 + } 214 + }, 215 + "indexes": {}, 216 + "foreignKeys": { 217 + "status_report_update_status_report_id_status_report_id_fk": { 218 + "name": "status_report_update_status_report_id_status_report_id_fk", 219 + "tableFrom": "status_report_update", 220 + "tableTo": "status_report", 221 + "columnsFrom": [ 222 + "status_report_id" 223 + ], 224 + "columnsTo": [ 225 + "id" 226 + ], 227 + "onDelete": "cascade", 228 + "onUpdate": "no action" 229 + } 230 + }, 231 + "compositePrimaryKeys": {}, 232 + "uniqueConstraints": {} 233 + }, 234 + "integration": { 235 + "name": "integration", 236 + "columns": { 237 + "id": { 238 + "name": "id", 239 + "type": "integer", 240 + "primaryKey": true, 241 + "notNull": true, 242 + "autoincrement": false 243 + }, 244 + "name": { 245 + "name": "name", 246 + "type": "text(256)", 247 + "primaryKey": false, 248 + "notNull": true, 249 + "autoincrement": false 250 + }, 251 + "workspace_id": { 252 + "name": "workspace_id", 253 + "type": "integer", 254 + "primaryKey": false, 255 + "notNull": false, 256 + "autoincrement": false 257 + }, 258 + "credential": { 259 + "name": "credential", 260 + "type": "text", 261 + "primaryKey": false, 262 + "notNull": false, 263 + "autoincrement": false 264 + }, 265 + "external_id": { 266 + "name": "external_id", 267 + "type": "text", 268 + "primaryKey": false, 269 + "notNull": true, 270 + "autoincrement": false 271 + }, 272 + "created_at": { 273 + "name": "created_at", 274 + "type": "integer", 275 + "primaryKey": false, 276 + "notNull": false, 277 + "autoincrement": false, 278 + "default": "(strftime('%s', 'now'))" 279 + }, 280 + "updated_at": { 281 + "name": "updated_at", 282 + "type": "integer", 283 + "primaryKey": false, 284 + "notNull": false, 285 + "autoincrement": false, 286 + "default": "(strftime('%s', 'now'))" 287 + }, 288 + "data": { 289 + "name": "data", 290 + "type": "text", 291 + "primaryKey": false, 292 + "notNull": true, 293 + "autoincrement": false 294 + } 295 + }, 296 + "indexes": {}, 297 + "foreignKeys": { 298 + "integration_workspace_id_workspace_id_fk": { 299 + "name": "integration_workspace_id_workspace_id_fk", 300 + "tableFrom": "integration", 301 + "tableTo": "workspace", 302 + "columnsFrom": [ 303 + "workspace_id" 304 + ], 305 + "columnsTo": [ 306 + "id" 307 + ], 308 + "onDelete": "no action", 309 + "onUpdate": "no action" 310 + } 311 + }, 312 + "compositePrimaryKeys": {}, 313 + "uniqueConstraints": {} 314 + }, 315 + "page": { 316 + "name": "page", 317 + "columns": { 318 + "id": { 319 + "name": "id", 320 + "type": "integer", 321 + "primaryKey": true, 322 + "notNull": true, 323 + "autoincrement": false 324 + }, 325 + "workspace_id": { 326 + "name": "workspace_id", 327 + "type": "integer", 328 + "primaryKey": false, 329 + "notNull": true, 330 + "autoincrement": false 331 + }, 332 + "title": { 333 + "name": "title", 334 + "type": "text", 335 + "primaryKey": false, 336 + "notNull": true, 337 + "autoincrement": false 338 + }, 339 + "description": { 340 + "name": "description", 341 + "type": "text", 342 + "primaryKey": false, 343 + "notNull": true, 344 + "autoincrement": false 345 + }, 346 + "icon": { 347 + "name": "icon", 348 + "type": "text(256)", 349 + "primaryKey": false, 350 + "notNull": false, 351 + "autoincrement": false, 352 + "default": "''" 353 + }, 354 + "slug": { 355 + "name": "slug", 356 + "type": "text(256)", 357 + "primaryKey": false, 358 + "notNull": true, 359 + "autoincrement": false 360 + }, 361 + "custom_domain": { 362 + "name": "custom_domain", 363 + "type": "text(256)", 364 + "primaryKey": false, 365 + "notNull": true, 366 + "autoincrement": false 367 + }, 368 + "published": { 369 + "name": "published", 370 + "type": "integer", 371 + "primaryKey": false, 372 + "notNull": false, 373 + "autoincrement": false, 374 + "default": false 375 + }, 376 + "password": { 377 + "name": "password", 378 + "type": "text(256)", 379 + "primaryKey": false, 380 + "notNull": false, 381 + "autoincrement": false 382 + }, 383 + "password_protected": { 384 + "name": "password_protected", 385 + "type": "integer", 386 + "primaryKey": false, 387 + "notNull": false, 388 + "autoincrement": false, 389 + "default": false 390 + }, 391 + "created_at": { 392 + "name": "created_at", 393 + "type": "integer", 394 + "primaryKey": false, 395 + "notNull": false, 396 + "autoincrement": false, 397 + "default": "(strftime('%s', 'now'))" 398 + }, 399 + "updated_at": { 400 + "name": "updated_at", 401 + "type": "integer", 402 + "primaryKey": false, 403 + "notNull": false, 404 + "autoincrement": false, 405 + "default": "(strftime('%s', 'now'))" 406 + } 407 + }, 408 + "indexes": { 409 + "page_slug_unique": { 410 + "name": "page_slug_unique", 411 + "columns": [ 412 + "slug" 413 + ], 414 + "isUnique": true 415 + } 416 + }, 417 + "foreignKeys": { 418 + "page_workspace_id_workspace_id_fk": { 419 + "name": "page_workspace_id_workspace_id_fk", 420 + "tableFrom": "page", 421 + "tableTo": "workspace", 422 + "columnsFrom": [ 423 + "workspace_id" 424 + ], 425 + "columnsTo": [ 426 + "id" 427 + ], 428 + "onDelete": "cascade", 429 + "onUpdate": "no action" 430 + } 431 + }, 432 + "compositePrimaryKeys": {}, 433 + "uniqueConstraints": {} 434 + }, 435 + "monitor": { 436 + "name": "monitor", 437 + "columns": { 438 + "id": { 439 + "name": "id", 440 + "type": "integer", 441 + "primaryKey": true, 442 + "notNull": true, 443 + "autoincrement": false 444 + }, 445 + "job_type": { 446 + "name": "job_type", 447 + "type": "text", 448 + "primaryKey": false, 449 + "notNull": true, 450 + "autoincrement": false, 451 + "default": "'other'" 452 + }, 453 + "periodicity": { 454 + "name": "periodicity", 455 + "type": "text", 456 + "primaryKey": false, 457 + "notNull": true, 458 + "autoincrement": false, 459 + "default": "'other'" 460 + }, 461 + "status": { 462 + "name": "status", 463 + "type": "text", 464 + "primaryKey": false, 465 + "notNull": true, 466 + "autoincrement": false, 467 + "default": "'active'" 468 + }, 469 + "active": { 470 + "name": "active", 471 + "type": "integer", 472 + "primaryKey": false, 473 + "notNull": false, 474 + "autoincrement": false, 475 + "default": false 476 + }, 477 + "regions": { 478 + "name": "regions", 479 + "type": "text", 480 + "primaryKey": false, 481 + "notNull": true, 482 + "autoincrement": false, 483 + "default": "''" 484 + }, 485 + "url": { 486 + "name": "url", 487 + "type": "text(2048)", 488 + "primaryKey": false, 489 + "notNull": true, 490 + "autoincrement": false 491 + }, 492 + "name": { 493 + "name": "name", 494 + "type": "text(256)", 495 + "primaryKey": false, 496 + "notNull": true, 497 + "autoincrement": false, 498 + "default": "''" 499 + }, 500 + "description": { 501 + "name": "description", 502 + "type": "text", 503 + "primaryKey": false, 504 + "notNull": true, 505 + "autoincrement": false, 506 + "default": "''" 507 + }, 508 + "headers": { 509 + "name": "headers", 510 + "type": "text", 511 + "primaryKey": false, 512 + "notNull": false, 513 + "autoincrement": false, 514 + "default": "''" 515 + }, 516 + "body": { 517 + "name": "body", 518 + "type": "text", 519 + "primaryKey": false, 520 + "notNull": false, 521 + "autoincrement": false, 522 + "default": "''" 523 + }, 524 + "method": { 525 + "name": "method", 526 + "type": "text", 527 + "primaryKey": false, 528 + "notNull": false, 529 + "autoincrement": false, 530 + "default": "'GET'" 531 + }, 532 + "workspace_id": { 533 + "name": "workspace_id", 534 + "type": "integer", 535 + "primaryKey": false, 536 + "notNull": false, 537 + "autoincrement": false 538 + }, 539 + "timeout": { 540 + "name": "timeout", 541 + "type": "integer", 542 + "primaryKey": false, 543 + "notNull": true, 544 + "autoincrement": false, 545 + "default": 45000 546 + }, 547 + "degraded_after": { 548 + "name": "degraded_after", 549 + "type": "integer", 550 + "primaryKey": false, 551 + "notNull": false, 552 + "autoincrement": false 553 + }, 554 + "assertions": { 555 + "name": "assertions", 556 + "type": "text", 557 + "primaryKey": false, 558 + "notNull": false, 559 + "autoincrement": false 560 + }, 561 + "public": { 562 + "name": "public", 563 + "type": "integer", 564 + "primaryKey": false, 565 + "notNull": false, 566 + "autoincrement": false, 567 + "default": false 568 + }, 569 + "created_at": { 570 + "name": "created_at", 571 + "type": "integer", 572 + "primaryKey": false, 573 + "notNull": false, 574 + "autoincrement": false, 575 + "default": "(strftime('%s', 'now'))" 576 + }, 577 + "updated_at": { 578 + "name": "updated_at", 579 + "type": "integer", 580 + "primaryKey": false, 581 + "notNull": false, 582 + "autoincrement": false, 583 + "default": "(strftime('%s', 'now'))" 584 + }, 585 + "deleted_at": { 586 + "name": "deleted_at", 587 + "type": "integer", 588 + "primaryKey": false, 589 + "notNull": false, 590 + "autoincrement": false 591 + } 592 + }, 593 + "indexes": {}, 594 + "foreignKeys": { 595 + "monitor_workspace_id_workspace_id_fk": { 596 + "name": "monitor_workspace_id_workspace_id_fk", 597 + "tableFrom": "monitor", 598 + "tableTo": "workspace", 599 + "columnsFrom": [ 600 + "workspace_id" 601 + ], 602 + "columnsTo": [ 603 + "id" 604 + ], 605 + "onDelete": "no action", 606 + "onUpdate": "no action" 607 + } 608 + }, 609 + "compositePrimaryKeys": {}, 610 + "uniqueConstraints": {} 611 + }, 612 + "monitors_to_pages": { 613 + "name": "monitors_to_pages", 614 + "columns": { 615 + "monitor_id": { 616 + "name": "monitor_id", 617 + "type": "integer", 618 + "primaryKey": false, 619 + "notNull": true, 620 + "autoincrement": false 621 + }, 622 + "page_id": { 623 + "name": "page_id", 624 + "type": "integer", 625 + "primaryKey": false, 626 + "notNull": true, 627 + "autoincrement": false 628 + }, 629 + "created_at": { 630 + "name": "created_at", 631 + "type": "integer", 632 + "primaryKey": false, 633 + "notNull": false, 634 + "autoincrement": false, 635 + "default": "(strftime('%s', 'now'))" 636 + }, 637 + "order": { 638 + "name": "order", 639 + "type": "integer", 640 + "primaryKey": false, 641 + "notNull": false, 642 + "autoincrement": false, 643 + "default": 0 644 + } 645 + }, 646 + "indexes": {}, 647 + "foreignKeys": { 648 + "monitors_to_pages_monitor_id_monitor_id_fk": { 649 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 650 + "tableFrom": "monitors_to_pages", 651 + "tableTo": "monitor", 652 + "columnsFrom": [ 653 + "monitor_id" 654 + ], 655 + "columnsTo": [ 656 + "id" 657 + ], 658 + "onDelete": "cascade", 659 + "onUpdate": "no action" 660 + }, 661 + "monitors_to_pages_page_id_page_id_fk": { 662 + "name": "monitors_to_pages_page_id_page_id_fk", 663 + "tableFrom": "monitors_to_pages", 664 + "tableTo": "page", 665 + "columnsFrom": [ 666 + "page_id" 667 + ], 668 + "columnsTo": [ 669 + "id" 670 + ], 671 + "onDelete": "cascade", 672 + "onUpdate": "no action" 673 + } 674 + }, 675 + "compositePrimaryKeys": { 676 + "monitors_to_pages_monitor_id_page_id_pk": { 677 + "columns": [ 678 + "monitor_id", 679 + "page_id" 680 + ], 681 + "name": "monitors_to_pages_monitor_id_page_id_pk" 682 + } 683 + }, 684 + "uniqueConstraints": {} 685 + }, 686 + "workspace": { 687 + "name": "workspace", 688 + "columns": { 689 + "id": { 690 + "name": "id", 691 + "type": "integer", 692 + "primaryKey": true, 693 + "notNull": true, 694 + "autoincrement": false 695 + }, 696 + "slug": { 697 + "name": "slug", 698 + "type": "text", 699 + "primaryKey": false, 700 + "notNull": true, 701 + "autoincrement": false 702 + }, 703 + "name": { 704 + "name": "name", 705 + "type": "text", 706 + "primaryKey": false, 707 + "notNull": false, 708 + "autoincrement": false 709 + }, 710 + "stripe_id": { 711 + "name": "stripe_id", 712 + "type": "text(256)", 713 + "primaryKey": false, 714 + "notNull": false, 715 + "autoincrement": false 716 + }, 717 + "subscription_id": { 718 + "name": "subscription_id", 719 + "type": "text", 720 + "primaryKey": false, 721 + "notNull": false, 722 + "autoincrement": false 723 + }, 724 + "plan": { 725 + "name": "plan", 726 + "type": "text", 727 + "primaryKey": false, 728 + "notNull": false, 729 + "autoincrement": false 730 + }, 731 + "ends_at": { 732 + "name": "ends_at", 733 + "type": "integer", 734 + "primaryKey": false, 735 + "notNull": false, 736 + "autoincrement": false 737 + }, 738 + "paid_until": { 739 + "name": "paid_until", 740 + "type": "integer", 741 + "primaryKey": false, 742 + "notNull": false, 743 + "autoincrement": false 744 + }, 745 + "limits": { 746 + "name": "limits", 747 + "type": "text", 748 + "primaryKey": false, 749 + "notNull": true, 750 + "autoincrement": false, 751 + "default": "'{}'" 752 + }, 753 + "created_at": { 754 + "name": "created_at", 755 + "type": "integer", 756 + "primaryKey": false, 757 + "notNull": false, 758 + "autoincrement": false, 759 + "default": "(strftime('%s', 'now'))" 760 + }, 761 + "updated_at": { 762 + "name": "updated_at", 763 + "type": "integer", 764 + "primaryKey": false, 765 + "notNull": false, 766 + "autoincrement": false, 767 + "default": "(strftime('%s', 'now'))" 768 + }, 769 + "dsn": { 770 + "name": "dsn", 771 + "type": "text", 772 + "primaryKey": false, 773 + "notNull": false, 774 + "autoincrement": false 775 + } 776 + }, 777 + "indexes": { 778 + "workspace_slug_unique": { 779 + "name": "workspace_slug_unique", 780 + "columns": [ 781 + "slug" 782 + ], 783 + "isUnique": true 784 + }, 785 + "workspace_stripe_id_unique": { 786 + "name": "workspace_stripe_id_unique", 787 + "columns": [ 788 + "stripe_id" 789 + ], 790 + "isUnique": true 791 + }, 792 + "workspace_id_dsn_unique": { 793 + "name": "workspace_id_dsn_unique", 794 + "columns": [ 795 + "id", 796 + "dsn" 797 + ], 798 + "isUnique": true 799 + } 800 + }, 801 + "foreignKeys": {}, 802 + "compositePrimaryKeys": {}, 803 + "uniqueConstraints": {} 804 + }, 805 + "account": { 806 + "name": "account", 807 + "columns": { 808 + "user_id": { 809 + "name": "user_id", 810 + "type": "integer", 811 + "primaryKey": false, 812 + "notNull": true, 813 + "autoincrement": false 814 + }, 815 + "type": { 816 + "name": "type", 817 + "type": "text", 818 + "primaryKey": false, 819 + "notNull": true, 820 + "autoincrement": false 821 + }, 822 + "provider": { 823 + "name": "provider", 824 + "type": "text", 825 + "primaryKey": false, 826 + "notNull": true, 827 + "autoincrement": false 828 + }, 829 + "provider_account_id": { 830 + "name": "provider_account_id", 831 + "type": "text", 832 + "primaryKey": false, 833 + "notNull": true, 834 + "autoincrement": false 835 + }, 836 + "refresh_token": { 837 + "name": "refresh_token", 838 + "type": "text", 839 + "primaryKey": false, 840 + "notNull": false, 841 + "autoincrement": false 842 + }, 843 + "access_token": { 844 + "name": "access_token", 845 + "type": "text", 846 + "primaryKey": false, 847 + "notNull": false, 848 + "autoincrement": false 849 + }, 850 + "expires_at": { 851 + "name": "expires_at", 852 + "type": "integer", 853 + "primaryKey": false, 854 + "notNull": false, 855 + "autoincrement": false 856 + }, 857 + "token_type": { 858 + "name": "token_type", 859 + "type": "text", 860 + "primaryKey": false, 861 + "notNull": false, 862 + "autoincrement": false 863 + }, 864 + "scope": { 865 + "name": "scope", 866 + "type": "text", 867 + "primaryKey": false, 868 + "notNull": false, 869 + "autoincrement": false 870 + }, 871 + "id_token": { 872 + "name": "id_token", 873 + "type": "text", 874 + "primaryKey": false, 875 + "notNull": false, 876 + "autoincrement": false 877 + }, 878 + "session_state": { 879 + "name": "session_state", 880 + "type": "text", 881 + "primaryKey": false, 882 + "notNull": false, 883 + "autoincrement": false 884 + } 885 + }, 886 + "indexes": {}, 887 + "foreignKeys": { 888 + "account_user_id_user_id_fk": { 889 + "name": "account_user_id_user_id_fk", 890 + "tableFrom": "account", 891 + "tableTo": "user", 892 + "columnsFrom": [ 893 + "user_id" 894 + ], 895 + "columnsTo": [ 896 + "id" 897 + ], 898 + "onDelete": "cascade", 899 + "onUpdate": "no action" 900 + } 901 + }, 902 + "compositePrimaryKeys": { 903 + "account_provider_provider_account_id_pk": { 904 + "columns": [ 905 + "provider", 906 + "provider_account_id" 907 + ], 908 + "name": "account_provider_provider_account_id_pk" 909 + } 910 + }, 911 + "uniqueConstraints": {} 912 + }, 913 + "session": { 914 + "name": "session", 915 + "columns": { 916 + "session_token": { 917 + "name": "session_token", 918 + "type": "text", 919 + "primaryKey": true, 920 + "notNull": true, 921 + "autoincrement": false 922 + }, 923 + "user_id": { 924 + "name": "user_id", 925 + "type": "integer", 926 + "primaryKey": false, 927 + "notNull": true, 928 + "autoincrement": false 929 + }, 930 + "expires": { 931 + "name": "expires", 932 + "type": "integer", 933 + "primaryKey": false, 934 + "notNull": true, 935 + "autoincrement": false 936 + } 937 + }, 938 + "indexes": {}, 939 + "foreignKeys": { 940 + "session_user_id_user_id_fk": { 941 + "name": "session_user_id_user_id_fk", 942 + "tableFrom": "session", 943 + "tableTo": "user", 944 + "columnsFrom": [ 945 + "user_id" 946 + ], 947 + "columnsTo": [ 948 + "id" 949 + ], 950 + "onDelete": "cascade", 951 + "onUpdate": "no action" 952 + } 953 + }, 954 + "compositePrimaryKeys": {}, 955 + "uniqueConstraints": {} 956 + }, 957 + "user": { 958 + "name": "user", 959 + "columns": { 960 + "id": { 961 + "name": "id", 962 + "type": "integer", 963 + "primaryKey": true, 964 + "notNull": true, 965 + "autoincrement": false 966 + }, 967 + "tenant_id": { 968 + "name": "tenant_id", 969 + "type": "text(256)", 970 + "primaryKey": false, 971 + "notNull": false, 972 + "autoincrement": false 973 + }, 974 + "first_name": { 975 + "name": "first_name", 976 + "type": "text", 977 + "primaryKey": false, 978 + "notNull": false, 979 + "autoincrement": false, 980 + "default": "''" 981 + }, 982 + "last_name": { 983 + "name": "last_name", 984 + "type": "text", 985 + "primaryKey": false, 986 + "notNull": false, 987 + "autoincrement": false, 988 + "default": "''" 989 + }, 990 + "photo_url": { 991 + "name": "photo_url", 992 + "type": "text", 993 + "primaryKey": false, 994 + "notNull": false, 995 + "autoincrement": false, 996 + "default": "''" 997 + }, 998 + "name": { 999 + "name": "name", 1000 + "type": "text", 1001 + "primaryKey": false, 1002 + "notNull": false, 1003 + "autoincrement": false 1004 + }, 1005 + "email": { 1006 + "name": "email", 1007 + "type": "text", 1008 + "primaryKey": false, 1009 + "notNull": false, 1010 + "autoincrement": false, 1011 + "default": "''" 1012 + }, 1013 + "emailVerified": { 1014 + "name": "emailVerified", 1015 + "type": "integer", 1016 + "primaryKey": false, 1017 + "notNull": false, 1018 + "autoincrement": false 1019 + }, 1020 + "created_at": { 1021 + "name": "created_at", 1022 + "type": "integer", 1023 + "primaryKey": false, 1024 + "notNull": false, 1025 + "autoincrement": false, 1026 + "default": "(strftime('%s', 'now'))" 1027 + }, 1028 + "updated_at": { 1029 + "name": "updated_at", 1030 + "type": "integer", 1031 + "primaryKey": false, 1032 + "notNull": false, 1033 + "autoincrement": false, 1034 + "default": "(strftime('%s', 'now'))" 1035 + } 1036 + }, 1037 + "indexes": { 1038 + "user_tenant_id_unique": { 1039 + "name": "user_tenant_id_unique", 1040 + "columns": [ 1041 + "tenant_id" 1042 + ], 1043 + "isUnique": true 1044 + } 1045 + }, 1046 + "foreignKeys": {}, 1047 + "compositePrimaryKeys": {}, 1048 + "uniqueConstraints": {} 1049 + }, 1050 + "users_to_workspaces": { 1051 + "name": "users_to_workspaces", 1052 + "columns": { 1053 + "user_id": { 1054 + "name": "user_id", 1055 + "type": "integer", 1056 + "primaryKey": false, 1057 + "notNull": true, 1058 + "autoincrement": false 1059 + }, 1060 + "workspace_id": { 1061 + "name": "workspace_id", 1062 + "type": "integer", 1063 + "primaryKey": false, 1064 + "notNull": true, 1065 + "autoincrement": false 1066 + }, 1067 + "role": { 1068 + "name": "role", 1069 + "type": "text", 1070 + "primaryKey": false, 1071 + "notNull": true, 1072 + "autoincrement": false, 1073 + "default": "'member'" 1074 + }, 1075 + "created_at": { 1076 + "name": "created_at", 1077 + "type": "integer", 1078 + "primaryKey": false, 1079 + "notNull": false, 1080 + "autoincrement": false, 1081 + "default": "(strftime('%s', 'now'))" 1082 + } 1083 + }, 1084 + "indexes": {}, 1085 + "foreignKeys": { 1086 + "users_to_workspaces_user_id_user_id_fk": { 1087 + "name": "users_to_workspaces_user_id_user_id_fk", 1088 + "tableFrom": "users_to_workspaces", 1089 + "tableTo": "user", 1090 + "columnsFrom": [ 1091 + "user_id" 1092 + ], 1093 + "columnsTo": [ 1094 + "id" 1095 + ], 1096 + "onDelete": "no action", 1097 + "onUpdate": "no action" 1098 + }, 1099 + "users_to_workspaces_workspace_id_workspace_id_fk": { 1100 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 1101 + "tableFrom": "users_to_workspaces", 1102 + "tableTo": "workspace", 1103 + "columnsFrom": [ 1104 + "workspace_id" 1105 + ], 1106 + "columnsTo": [ 1107 + "id" 1108 + ], 1109 + "onDelete": "no action", 1110 + "onUpdate": "no action" 1111 + } 1112 + }, 1113 + "compositePrimaryKeys": { 1114 + "users_to_workspaces_user_id_workspace_id_pk": { 1115 + "columns": [ 1116 + "user_id", 1117 + "workspace_id" 1118 + ], 1119 + "name": "users_to_workspaces_user_id_workspace_id_pk" 1120 + } 1121 + }, 1122 + "uniqueConstraints": {} 1123 + }, 1124 + "verification_token": { 1125 + "name": "verification_token", 1126 + "columns": { 1127 + "identifier": { 1128 + "name": "identifier", 1129 + "type": "text", 1130 + "primaryKey": false, 1131 + "notNull": true, 1132 + "autoincrement": false 1133 + }, 1134 + "token": { 1135 + "name": "token", 1136 + "type": "text", 1137 + "primaryKey": false, 1138 + "notNull": true, 1139 + "autoincrement": false 1140 + }, 1141 + "expires": { 1142 + "name": "expires", 1143 + "type": "integer", 1144 + "primaryKey": false, 1145 + "notNull": true, 1146 + "autoincrement": false 1147 + } 1148 + }, 1149 + "indexes": {}, 1150 + "foreignKeys": {}, 1151 + "compositePrimaryKeys": { 1152 + "verification_token_identifier_token_pk": { 1153 + "columns": [ 1154 + "identifier", 1155 + "token" 1156 + ], 1157 + "name": "verification_token_identifier_token_pk" 1158 + } 1159 + }, 1160 + "uniqueConstraints": {} 1161 + }, 1162 + "page_subscriber": { 1163 + "name": "page_subscriber", 1164 + "columns": { 1165 + "id": { 1166 + "name": "id", 1167 + "type": "integer", 1168 + "primaryKey": true, 1169 + "notNull": true, 1170 + "autoincrement": false 1171 + }, 1172 + "email": { 1173 + "name": "email", 1174 + "type": "text", 1175 + "primaryKey": false, 1176 + "notNull": true, 1177 + "autoincrement": false 1178 + }, 1179 + "page_id": { 1180 + "name": "page_id", 1181 + "type": "integer", 1182 + "primaryKey": false, 1183 + "notNull": true, 1184 + "autoincrement": false 1185 + }, 1186 + "token": { 1187 + "name": "token", 1188 + "type": "text", 1189 + "primaryKey": false, 1190 + "notNull": false, 1191 + "autoincrement": false 1192 + }, 1193 + "accepted_at": { 1194 + "name": "accepted_at", 1195 + "type": "integer", 1196 + "primaryKey": false, 1197 + "notNull": false, 1198 + "autoincrement": false 1199 + }, 1200 + "expires_at": { 1201 + "name": "expires_at", 1202 + "type": "integer", 1203 + "primaryKey": false, 1204 + "notNull": false, 1205 + "autoincrement": false 1206 + }, 1207 + "created_at": { 1208 + "name": "created_at", 1209 + "type": "integer", 1210 + "primaryKey": false, 1211 + "notNull": false, 1212 + "autoincrement": false, 1213 + "default": "(strftime('%s', 'now'))" 1214 + }, 1215 + "updated_at": { 1216 + "name": "updated_at", 1217 + "type": "integer", 1218 + "primaryKey": false, 1219 + "notNull": false, 1220 + "autoincrement": false, 1221 + "default": "(strftime('%s', 'now'))" 1222 + } 1223 + }, 1224 + "indexes": {}, 1225 + "foreignKeys": { 1226 + "page_subscriber_page_id_page_id_fk": { 1227 + "name": "page_subscriber_page_id_page_id_fk", 1228 + "tableFrom": "page_subscriber", 1229 + "tableTo": "page", 1230 + "columnsFrom": [ 1231 + "page_id" 1232 + ], 1233 + "columnsTo": [ 1234 + "id" 1235 + ], 1236 + "onDelete": "no action", 1237 + "onUpdate": "no action" 1238 + } 1239 + }, 1240 + "compositePrimaryKeys": {}, 1241 + "uniqueConstraints": {} 1242 + }, 1243 + "notification": { 1244 + "name": "notification", 1245 + "columns": { 1246 + "id": { 1247 + "name": "id", 1248 + "type": "integer", 1249 + "primaryKey": true, 1250 + "notNull": true, 1251 + "autoincrement": false 1252 + }, 1253 + "name": { 1254 + "name": "name", 1255 + "type": "text", 1256 + "primaryKey": false, 1257 + "notNull": true, 1258 + "autoincrement": false 1259 + }, 1260 + "provider": { 1261 + "name": "provider", 1262 + "type": "text", 1263 + "primaryKey": false, 1264 + "notNull": true, 1265 + "autoincrement": false 1266 + }, 1267 + "data": { 1268 + "name": "data", 1269 + "type": "text", 1270 + "primaryKey": false, 1271 + "notNull": false, 1272 + "autoincrement": false, 1273 + "default": "'{}'" 1274 + }, 1275 + "workspace_id": { 1276 + "name": "workspace_id", 1277 + "type": "integer", 1278 + "primaryKey": false, 1279 + "notNull": false, 1280 + "autoincrement": false 1281 + }, 1282 + "created_at": { 1283 + "name": "created_at", 1284 + "type": "integer", 1285 + "primaryKey": false, 1286 + "notNull": false, 1287 + "autoincrement": false, 1288 + "default": "(strftime('%s', 'now'))" 1289 + }, 1290 + "updated_at": { 1291 + "name": "updated_at", 1292 + "type": "integer", 1293 + "primaryKey": false, 1294 + "notNull": false, 1295 + "autoincrement": false, 1296 + "default": "(strftime('%s', 'now'))" 1297 + } 1298 + }, 1299 + "indexes": {}, 1300 + "foreignKeys": { 1301 + "notification_workspace_id_workspace_id_fk": { 1302 + "name": "notification_workspace_id_workspace_id_fk", 1303 + "tableFrom": "notification", 1304 + "tableTo": "workspace", 1305 + "columnsFrom": [ 1306 + "workspace_id" 1307 + ], 1308 + "columnsTo": [ 1309 + "id" 1310 + ], 1311 + "onDelete": "no action", 1312 + "onUpdate": "no action" 1313 + } 1314 + }, 1315 + "compositePrimaryKeys": {}, 1316 + "uniqueConstraints": {} 1317 + }, 1318 + "notifications_to_monitors": { 1319 + "name": "notifications_to_monitors", 1320 + "columns": { 1321 + "monitor_id": { 1322 + "name": "monitor_id", 1323 + "type": "integer", 1324 + "primaryKey": false, 1325 + "notNull": true, 1326 + "autoincrement": false 1327 + }, 1328 + "notification_id": { 1329 + "name": "notification_id", 1330 + "type": "integer", 1331 + "primaryKey": false, 1332 + "notNull": true, 1333 + "autoincrement": false 1334 + }, 1335 + "created_at": { 1336 + "name": "created_at", 1337 + "type": "integer", 1338 + "primaryKey": false, 1339 + "notNull": false, 1340 + "autoincrement": false, 1341 + "default": "(strftime('%s', 'now'))" 1342 + } 1343 + }, 1344 + "indexes": {}, 1345 + "foreignKeys": { 1346 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 1347 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 1348 + "tableFrom": "notifications_to_monitors", 1349 + "tableTo": "monitor", 1350 + "columnsFrom": [ 1351 + "monitor_id" 1352 + ], 1353 + "columnsTo": [ 1354 + "id" 1355 + ], 1356 + "onDelete": "cascade", 1357 + "onUpdate": "no action" 1358 + }, 1359 + "notifications_to_monitors_notification_id_notification_id_fk": { 1360 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 1361 + "tableFrom": "notifications_to_monitors", 1362 + "tableTo": "notification", 1363 + "columnsFrom": [ 1364 + "notification_id" 1365 + ], 1366 + "columnsTo": [ 1367 + "id" 1368 + ], 1369 + "onDelete": "cascade", 1370 + "onUpdate": "no action" 1371 + } 1372 + }, 1373 + "compositePrimaryKeys": { 1374 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1375 + "columns": [ 1376 + "monitor_id", 1377 + "notification_id" 1378 + ], 1379 + "name": "notifications_to_monitors_monitor_id_notification_id_pk" 1380 + } 1381 + }, 1382 + "uniqueConstraints": {} 1383 + }, 1384 + "monitor_status": { 1385 + "name": "monitor_status", 1386 + "columns": { 1387 + "monitor_id": { 1388 + "name": "monitor_id", 1389 + "type": "integer", 1390 + "primaryKey": false, 1391 + "notNull": true, 1392 + "autoincrement": false 1393 + }, 1394 + "region": { 1395 + "name": "region", 1396 + "type": "text", 1397 + "primaryKey": false, 1398 + "notNull": true, 1399 + "autoincrement": false, 1400 + "default": "''" 1401 + }, 1402 + "status": { 1403 + "name": "status", 1404 + "type": "text", 1405 + "primaryKey": false, 1406 + "notNull": true, 1407 + "autoincrement": false, 1408 + "default": "'active'" 1409 + }, 1410 + "created_at": { 1411 + "name": "created_at", 1412 + "type": "integer", 1413 + "primaryKey": false, 1414 + "notNull": false, 1415 + "autoincrement": false, 1416 + "default": "(strftime('%s', 'now'))" 1417 + }, 1418 + "updated_at": { 1419 + "name": "updated_at", 1420 + "type": "integer", 1421 + "primaryKey": false, 1422 + "notNull": false, 1423 + "autoincrement": false, 1424 + "default": "(strftime('%s', 'now'))" 1425 + } 1426 + }, 1427 + "indexes": { 1428 + "monitor_status_idx": { 1429 + "name": "monitor_status_idx", 1430 + "columns": [ 1431 + "monitor_id", 1432 + "region" 1433 + ], 1434 + "isUnique": false 1435 + } 1436 + }, 1437 + "foreignKeys": { 1438 + "monitor_status_monitor_id_monitor_id_fk": { 1439 + "name": "monitor_status_monitor_id_monitor_id_fk", 1440 + "tableFrom": "monitor_status", 1441 + "tableTo": "monitor", 1442 + "columnsFrom": [ 1443 + "monitor_id" 1444 + ], 1445 + "columnsTo": [ 1446 + "id" 1447 + ], 1448 + "onDelete": "cascade", 1449 + "onUpdate": "no action" 1450 + } 1451 + }, 1452 + "compositePrimaryKeys": { 1453 + "monitor_status_monitor_id_region_pk": { 1454 + "columns": [ 1455 + "monitor_id", 1456 + "region" 1457 + ], 1458 + "name": "monitor_status_monitor_id_region_pk" 1459 + } 1460 + }, 1461 + "uniqueConstraints": {} 1462 + }, 1463 + "invitation": { 1464 + "name": "invitation", 1465 + "columns": { 1466 + "id": { 1467 + "name": "id", 1468 + "type": "integer", 1469 + "primaryKey": true, 1470 + "notNull": true, 1471 + "autoincrement": false 1472 + }, 1473 + "email": { 1474 + "name": "email", 1475 + "type": "text", 1476 + "primaryKey": false, 1477 + "notNull": true, 1478 + "autoincrement": false 1479 + }, 1480 + "role": { 1481 + "name": "role", 1482 + "type": "text", 1483 + "primaryKey": false, 1484 + "notNull": true, 1485 + "autoincrement": false, 1486 + "default": "'member'" 1487 + }, 1488 + "workspace_id": { 1489 + "name": "workspace_id", 1490 + "type": "integer", 1491 + "primaryKey": false, 1492 + "notNull": true, 1493 + "autoincrement": false 1494 + }, 1495 + "token": { 1496 + "name": "token", 1497 + "type": "text", 1498 + "primaryKey": false, 1499 + "notNull": true, 1500 + "autoincrement": false 1501 + }, 1502 + "expires_at": { 1503 + "name": "expires_at", 1504 + "type": "integer", 1505 + "primaryKey": false, 1506 + "notNull": true, 1507 + "autoincrement": false 1508 + }, 1509 + "created_at": { 1510 + "name": "created_at", 1511 + "type": "integer", 1512 + "primaryKey": false, 1513 + "notNull": false, 1514 + "autoincrement": false, 1515 + "default": "(strftime('%s', 'now'))" 1516 + }, 1517 + "accepted_at": { 1518 + "name": "accepted_at", 1519 + "type": "integer", 1520 + "primaryKey": false, 1521 + "notNull": false, 1522 + "autoincrement": false 1523 + } 1524 + }, 1525 + "indexes": {}, 1526 + "foreignKeys": {}, 1527 + "compositePrimaryKeys": {}, 1528 + "uniqueConstraints": {} 1529 + }, 1530 + "incident": { 1531 + "name": "incident", 1532 + "columns": { 1533 + "id": { 1534 + "name": "id", 1535 + "type": "integer", 1536 + "primaryKey": true, 1537 + "notNull": true, 1538 + "autoincrement": false 1539 + }, 1540 + "title": { 1541 + "name": "title", 1542 + "type": "text", 1543 + "primaryKey": false, 1544 + "notNull": true, 1545 + "autoincrement": false, 1546 + "default": "''" 1547 + }, 1548 + "summary": { 1549 + "name": "summary", 1550 + "type": "text", 1551 + "primaryKey": false, 1552 + "notNull": true, 1553 + "autoincrement": false, 1554 + "default": "''" 1555 + }, 1556 + "status": { 1557 + "name": "status", 1558 + "type": "text", 1559 + "primaryKey": false, 1560 + "notNull": true, 1561 + "autoincrement": false, 1562 + "default": "'triage'" 1563 + }, 1564 + "monitor_id": { 1565 + "name": "monitor_id", 1566 + "type": "integer", 1567 + "primaryKey": false, 1568 + "notNull": false, 1569 + "autoincrement": false 1570 + }, 1571 + "workspace_id": { 1572 + "name": "workspace_id", 1573 + "type": "integer", 1574 + "primaryKey": false, 1575 + "notNull": false, 1576 + "autoincrement": false 1577 + }, 1578 + "started_at": { 1579 + "name": "started_at", 1580 + "type": "integer", 1581 + "primaryKey": false, 1582 + "notNull": true, 1583 + "autoincrement": false, 1584 + "default": "(strftime('%s', 'now'))" 1585 + }, 1586 + "acknowledged_at": { 1587 + "name": "acknowledged_at", 1588 + "type": "integer", 1589 + "primaryKey": false, 1590 + "notNull": false, 1591 + "autoincrement": false 1592 + }, 1593 + "acknowledged_by": { 1594 + "name": "acknowledged_by", 1595 + "type": "integer", 1596 + "primaryKey": false, 1597 + "notNull": false, 1598 + "autoincrement": false 1599 + }, 1600 + "resolved_at": { 1601 + "name": "resolved_at", 1602 + "type": "integer", 1603 + "primaryKey": false, 1604 + "notNull": false, 1605 + "autoincrement": false 1606 + }, 1607 + "resolved_by": { 1608 + "name": "resolved_by", 1609 + "type": "integer", 1610 + "primaryKey": false, 1611 + "notNull": false, 1612 + "autoincrement": false 1613 + }, 1614 + "incident_screenshot_url": { 1615 + "name": "incident_screenshot_url", 1616 + "type": "text", 1617 + "primaryKey": false, 1618 + "notNull": false, 1619 + "autoincrement": false 1620 + }, 1621 + "recovery_screenshot_url": { 1622 + "name": "recovery_screenshot_url", 1623 + "type": "text", 1624 + "primaryKey": false, 1625 + "notNull": false, 1626 + "autoincrement": false 1627 + }, 1628 + "auto_resolved": { 1629 + "name": "auto_resolved", 1630 + "type": "integer", 1631 + "primaryKey": false, 1632 + "notNull": false, 1633 + "autoincrement": false, 1634 + "default": false 1635 + }, 1636 + "created_at": { 1637 + "name": "created_at", 1638 + "type": "integer", 1639 + "primaryKey": false, 1640 + "notNull": false, 1641 + "autoincrement": false, 1642 + "default": "(strftime('%s', 'now'))" 1643 + }, 1644 + "updated_at": { 1645 + "name": "updated_at", 1646 + "type": "integer", 1647 + "primaryKey": false, 1648 + "notNull": false, 1649 + "autoincrement": false, 1650 + "default": "(strftime('%s', 'now'))" 1651 + } 1652 + }, 1653 + "indexes": { 1654 + "incident_monitor_id_started_at_unique": { 1655 + "name": "incident_monitor_id_started_at_unique", 1656 + "columns": [ 1657 + "monitor_id", 1658 + "started_at" 1659 + ], 1660 + "isUnique": true 1661 + } 1662 + }, 1663 + "foreignKeys": { 1664 + "incident_monitor_id_monitor_id_fk": { 1665 + "name": "incident_monitor_id_monitor_id_fk", 1666 + "tableFrom": "incident", 1667 + "tableTo": "monitor", 1668 + "columnsFrom": [ 1669 + "monitor_id" 1670 + ], 1671 + "columnsTo": [ 1672 + "id" 1673 + ], 1674 + "onDelete": "set default", 1675 + "onUpdate": "no action" 1676 + }, 1677 + "incident_workspace_id_workspace_id_fk": { 1678 + "name": "incident_workspace_id_workspace_id_fk", 1679 + "tableFrom": "incident", 1680 + "tableTo": "workspace", 1681 + "columnsFrom": [ 1682 + "workspace_id" 1683 + ], 1684 + "columnsTo": [ 1685 + "id" 1686 + ], 1687 + "onDelete": "no action", 1688 + "onUpdate": "no action" 1689 + }, 1690 + "incident_acknowledged_by_user_id_fk": { 1691 + "name": "incident_acknowledged_by_user_id_fk", 1692 + "tableFrom": "incident", 1693 + "tableTo": "user", 1694 + "columnsFrom": [ 1695 + "acknowledged_by" 1696 + ], 1697 + "columnsTo": [ 1698 + "id" 1699 + ], 1700 + "onDelete": "no action", 1701 + "onUpdate": "no action" 1702 + }, 1703 + "incident_resolved_by_user_id_fk": { 1704 + "name": "incident_resolved_by_user_id_fk", 1705 + "tableFrom": "incident", 1706 + "tableTo": "user", 1707 + "columnsFrom": [ 1708 + "resolved_by" 1709 + ], 1710 + "columnsTo": [ 1711 + "id" 1712 + ], 1713 + "onDelete": "no action", 1714 + "onUpdate": "no action" 1715 + } 1716 + }, 1717 + "compositePrimaryKeys": {}, 1718 + "uniqueConstraints": {} 1719 + }, 1720 + "monitor_tag": { 1721 + "name": "monitor_tag", 1722 + "columns": { 1723 + "id": { 1724 + "name": "id", 1725 + "type": "integer", 1726 + "primaryKey": true, 1727 + "notNull": true, 1728 + "autoincrement": false 1729 + }, 1730 + "workspace_id": { 1731 + "name": "workspace_id", 1732 + "type": "integer", 1733 + "primaryKey": false, 1734 + "notNull": true, 1735 + "autoincrement": false 1736 + }, 1737 + "name": { 1738 + "name": "name", 1739 + "type": "text", 1740 + "primaryKey": false, 1741 + "notNull": true, 1742 + "autoincrement": false 1743 + }, 1744 + "color": { 1745 + "name": "color", 1746 + "type": "text", 1747 + "primaryKey": false, 1748 + "notNull": true, 1749 + "autoincrement": false 1750 + }, 1751 + "created_at": { 1752 + "name": "created_at", 1753 + "type": "integer", 1754 + "primaryKey": false, 1755 + "notNull": false, 1756 + "autoincrement": false, 1757 + "default": "(strftime('%s', 'now'))" 1758 + }, 1759 + "updated_at": { 1760 + "name": "updated_at", 1761 + "type": "integer", 1762 + "primaryKey": false, 1763 + "notNull": false, 1764 + "autoincrement": false, 1765 + "default": "(strftime('%s', 'now'))" 1766 + } 1767 + }, 1768 + "indexes": {}, 1769 + "foreignKeys": { 1770 + "monitor_tag_workspace_id_workspace_id_fk": { 1771 + "name": "monitor_tag_workspace_id_workspace_id_fk", 1772 + "tableFrom": "monitor_tag", 1773 + "tableTo": "workspace", 1774 + "columnsFrom": [ 1775 + "workspace_id" 1776 + ], 1777 + "columnsTo": [ 1778 + "id" 1779 + ], 1780 + "onDelete": "cascade", 1781 + "onUpdate": "no action" 1782 + } 1783 + }, 1784 + "compositePrimaryKeys": {}, 1785 + "uniqueConstraints": {} 1786 + }, 1787 + "monitor_tag_to_monitor": { 1788 + "name": "monitor_tag_to_monitor", 1789 + "columns": { 1790 + "monitor_id": { 1791 + "name": "monitor_id", 1792 + "type": "integer", 1793 + "primaryKey": false, 1794 + "notNull": true, 1795 + "autoincrement": false 1796 + }, 1797 + "monitor_tag_id": { 1798 + "name": "monitor_tag_id", 1799 + "type": "integer", 1800 + "primaryKey": false, 1801 + "notNull": true, 1802 + "autoincrement": false 1803 + }, 1804 + "created_at": { 1805 + "name": "created_at", 1806 + "type": "integer", 1807 + "primaryKey": false, 1808 + "notNull": false, 1809 + "autoincrement": false, 1810 + "default": "(strftime('%s', 'now'))" 1811 + } 1812 + }, 1813 + "indexes": {}, 1814 + "foreignKeys": { 1815 + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { 1816 + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", 1817 + "tableFrom": "monitor_tag_to_monitor", 1818 + "tableTo": "monitor", 1819 + "columnsFrom": [ 1820 + "monitor_id" 1821 + ], 1822 + "columnsTo": [ 1823 + "id" 1824 + ], 1825 + "onDelete": "cascade", 1826 + "onUpdate": "no action" 1827 + }, 1828 + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { 1829 + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", 1830 + "tableFrom": "monitor_tag_to_monitor", 1831 + "tableTo": "monitor_tag", 1832 + "columnsFrom": [ 1833 + "monitor_tag_id" 1834 + ], 1835 + "columnsTo": [ 1836 + "id" 1837 + ], 1838 + "onDelete": "cascade", 1839 + "onUpdate": "no action" 1840 + } 1841 + }, 1842 + "compositePrimaryKeys": { 1843 + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { 1844 + "columns": [ 1845 + "monitor_id", 1846 + "monitor_tag_id" 1847 + ], 1848 + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" 1849 + } 1850 + }, 1851 + "uniqueConstraints": {} 1852 + }, 1853 + "application": { 1854 + "name": "application", 1855 + "columns": { 1856 + "id": { 1857 + "name": "id", 1858 + "type": "integer", 1859 + "primaryKey": true, 1860 + "notNull": true, 1861 + "autoincrement": false 1862 + }, 1863 + "name": { 1864 + "name": "name", 1865 + "type": "text", 1866 + "primaryKey": false, 1867 + "notNull": false, 1868 + "autoincrement": false 1869 + }, 1870 + "dsn": { 1871 + "name": "dsn", 1872 + "type": "text", 1873 + "primaryKey": false, 1874 + "notNull": false, 1875 + "autoincrement": false 1876 + }, 1877 + "workspace_id": { 1878 + "name": "workspace_id", 1879 + "type": "integer", 1880 + "primaryKey": false, 1881 + "notNull": false, 1882 + "autoincrement": false 1883 + }, 1884 + "created_at": { 1885 + "name": "created_at", 1886 + "type": "integer", 1887 + "primaryKey": false, 1888 + "notNull": false, 1889 + "autoincrement": false, 1890 + "default": "(strftime('%s', 'now'))" 1891 + }, 1892 + "updated_at": { 1893 + "name": "updated_at", 1894 + "type": "integer", 1895 + "primaryKey": false, 1896 + "notNull": false, 1897 + "autoincrement": false, 1898 + "default": "(strftime('%s', 'now'))" 1899 + } 1900 + }, 1901 + "indexes": { 1902 + "application_dsn_unique": { 1903 + "name": "application_dsn_unique", 1904 + "columns": [ 1905 + "dsn" 1906 + ], 1907 + "isUnique": true 1908 + } 1909 + }, 1910 + "foreignKeys": { 1911 + "application_workspace_id_workspace_id_fk": { 1912 + "name": "application_workspace_id_workspace_id_fk", 1913 + "tableFrom": "application", 1914 + "tableTo": "workspace", 1915 + "columnsFrom": [ 1916 + "workspace_id" 1917 + ], 1918 + "columnsTo": [ 1919 + "id" 1920 + ], 1921 + "onDelete": "no action", 1922 + "onUpdate": "no action" 1923 + } 1924 + }, 1925 + "compositePrimaryKeys": {}, 1926 + "uniqueConstraints": {} 1927 + }, 1928 + "maintenance": { 1929 + "name": "maintenance", 1930 + "columns": { 1931 + "id": { 1932 + "name": "id", 1933 + "type": "integer", 1934 + "primaryKey": true, 1935 + "notNull": true, 1936 + "autoincrement": false 1937 + }, 1938 + "title": { 1939 + "name": "title", 1940 + "type": "text(256)", 1941 + "primaryKey": false, 1942 + "notNull": true, 1943 + "autoincrement": false 1944 + }, 1945 + "message": { 1946 + "name": "message", 1947 + "type": "text", 1948 + "primaryKey": false, 1949 + "notNull": true, 1950 + "autoincrement": false 1951 + }, 1952 + "from": { 1953 + "name": "from", 1954 + "type": "integer", 1955 + "primaryKey": false, 1956 + "notNull": true, 1957 + "autoincrement": false 1958 + }, 1959 + "to": { 1960 + "name": "to", 1961 + "type": "integer", 1962 + "primaryKey": false, 1963 + "notNull": true, 1964 + "autoincrement": false 1965 + }, 1966 + "workspace_id": { 1967 + "name": "workspace_id", 1968 + "type": "integer", 1969 + "primaryKey": false, 1970 + "notNull": false, 1971 + "autoincrement": false 1972 + }, 1973 + "page_id": { 1974 + "name": "page_id", 1975 + "type": "integer", 1976 + "primaryKey": false, 1977 + "notNull": false, 1978 + "autoincrement": false 1979 + }, 1980 + "created_at": { 1981 + "name": "created_at", 1982 + "type": "integer", 1983 + "primaryKey": false, 1984 + "notNull": false, 1985 + "autoincrement": false, 1986 + "default": "(strftime('%s', 'now'))" 1987 + }, 1988 + "updated_at": { 1989 + "name": "updated_at", 1990 + "type": "integer", 1991 + "primaryKey": false, 1992 + "notNull": false, 1993 + "autoincrement": false, 1994 + "default": "(strftime('%s', 'now'))" 1995 + } 1996 + }, 1997 + "indexes": {}, 1998 + "foreignKeys": { 1999 + "maintenance_workspace_id_workspace_id_fk": { 2000 + "name": "maintenance_workspace_id_workspace_id_fk", 2001 + "tableFrom": "maintenance", 2002 + "tableTo": "workspace", 2003 + "columnsFrom": [ 2004 + "workspace_id" 2005 + ], 2006 + "columnsTo": [ 2007 + "id" 2008 + ], 2009 + "onDelete": "no action", 2010 + "onUpdate": "no action" 2011 + }, 2012 + "maintenance_page_id_page_id_fk": { 2013 + "name": "maintenance_page_id_page_id_fk", 2014 + "tableFrom": "maintenance", 2015 + "tableTo": "page", 2016 + "columnsFrom": [ 2017 + "page_id" 2018 + ], 2019 + "columnsTo": [ 2020 + "id" 2021 + ], 2022 + "onDelete": "no action", 2023 + "onUpdate": "no action" 2024 + } 2025 + }, 2026 + "compositePrimaryKeys": {}, 2027 + "uniqueConstraints": {} 2028 + }, 2029 + "maintenance_to_monitor": { 2030 + "name": "maintenance_to_monitor", 2031 + "columns": { 2032 + "monitor_id": { 2033 + "name": "monitor_id", 2034 + "type": "integer", 2035 + "primaryKey": false, 2036 + "notNull": true, 2037 + "autoincrement": false 2038 + }, 2039 + "maintenance_id": { 2040 + "name": "maintenance_id", 2041 + "type": "integer", 2042 + "primaryKey": false, 2043 + "notNull": true, 2044 + "autoincrement": false 2045 + }, 2046 + "created_at": { 2047 + "name": "created_at", 2048 + "type": "integer", 2049 + "primaryKey": false, 2050 + "notNull": false, 2051 + "autoincrement": false, 2052 + "default": "(strftime('%s', 'now'))" 2053 + } 2054 + }, 2055 + "indexes": {}, 2056 + "foreignKeys": { 2057 + "maintenance_to_monitor_monitor_id_monitor_id_fk": { 2058 + "name": "maintenance_to_monitor_monitor_id_monitor_id_fk", 2059 + "tableFrom": "maintenance_to_monitor", 2060 + "tableTo": "monitor", 2061 + "columnsFrom": [ 2062 + "monitor_id" 2063 + ], 2064 + "columnsTo": [ 2065 + "id" 2066 + ], 2067 + "onDelete": "cascade", 2068 + "onUpdate": "no action" 2069 + }, 2070 + "maintenance_to_monitor_maintenance_id_maintenance_id_fk": { 2071 + "name": "maintenance_to_monitor_maintenance_id_maintenance_id_fk", 2072 + "tableFrom": "maintenance_to_monitor", 2073 + "tableTo": "maintenance", 2074 + "columnsFrom": [ 2075 + "maintenance_id" 2076 + ], 2077 + "columnsTo": [ 2078 + "id" 2079 + ], 2080 + "onDelete": "cascade", 2081 + "onUpdate": "no action" 2082 + } 2083 + }, 2084 + "compositePrimaryKeys": { 2085 + "maintenance_to_monitor_monitor_id_maintenance_id_pk": { 2086 + "columns": [ 2087 + "maintenance_id", 2088 + "monitor_id" 2089 + ], 2090 + "name": "maintenance_to_monitor_monitor_id_maintenance_id_pk" 2091 + } 2092 + }, 2093 + "uniqueConstraints": {} 2094 + }, 2095 + "check": { 2096 + "name": "check", 2097 + "columns": { 2098 + "id": { 2099 + "name": "id", 2100 + "type": "integer", 2101 + "primaryKey": true, 2102 + "notNull": true, 2103 + "autoincrement": true 2104 + }, 2105 + "regions": { 2106 + "name": "regions", 2107 + "type": "text", 2108 + "primaryKey": false, 2109 + "notNull": true, 2110 + "autoincrement": false, 2111 + "default": "''" 2112 + }, 2113 + "url": { 2114 + "name": "url", 2115 + "type": "text(4096)", 2116 + "primaryKey": false, 2117 + "notNull": true, 2118 + "autoincrement": false 2119 + }, 2120 + "headers": { 2121 + "name": "headers", 2122 + "type": "text", 2123 + "primaryKey": false, 2124 + "notNull": false, 2125 + "autoincrement": false, 2126 + "default": "''" 2127 + }, 2128 + "body": { 2129 + "name": "body", 2130 + "type": "text", 2131 + "primaryKey": false, 2132 + "notNull": false, 2133 + "autoincrement": false, 2134 + "default": "''" 2135 + }, 2136 + "method": { 2137 + "name": "method", 2138 + "type": "text", 2139 + "primaryKey": false, 2140 + "notNull": false, 2141 + "autoincrement": false, 2142 + "default": "'GET'" 2143 + }, 2144 + "count_requests": { 2145 + "name": "count_requests", 2146 + "type": "integer", 2147 + "primaryKey": false, 2148 + "notNull": false, 2149 + "autoincrement": false, 2150 + "default": 1 2151 + }, 2152 + "workspace_id": { 2153 + "name": "workspace_id", 2154 + "type": "integer", 2155 + "primaryKey": false, 2156 + "notNull": false, 2157 + "autoincrement": false 2158 + }, 2159 + "created_at": { 2160 + "name": "created_at", 2161 + "type": "integer", 2162 + "primaryKey": false, 2163 + "notNull": false, 2164 + "autoincrement": false, 2165 + "default": "(strftime('%s', 'now'))" 2166 + } 2167 + }, 2168 + "indexes": {}, 2169 + "foreignKeys": { 2170 + "check_workspace_id_workspace_id_fk": { 2171 + "name": "check_workspace_id_workspace_id_fk", 2172 + "tableFrom": "check", 2173 + "tableTo": "workspace", 2174 + "columnsFrom": [ 2175 + "workspace_id" 2176 + ], 2177 + "columnsTo": [ 2178 + "id" 2179 + ], 2180 + "onDelete": "no action", 2181 + "onUpdate": "no action" 2182 + } 2183 + }, 2184 + "compositePrimaryKeys": {}, 2185 + "uniqueConstraints": {} 2186 + } 2187 + }, 2188 + "enums": {}, 2189 + "_meta": { 2190 + "schemas": {}, 2191 + "tables": {}, 2192 + "columns": {} 2193 + } 2194 + }
+7
packages/db/drizzle/meta/_journal.json
··· 246 246 "when": 1720727898360, 247 247 "tag": "0034_serious_shard", 248 248 "breakpoints": true 249 + }, 250 + { 251 + "idx": 35, 252 + "version": "6", 253 + "when": 1721159796428, 254 + "tag": "0035_open_the_professor", 255 + "breakpoints": true 249 256 } 250 257 ] 251 258 }
+55
packages/db/src/schema/constants.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const flyRegions = [ 4 + "ams", 5 + "arn", 6 + "atl", 7 + "bog", 8 + "bom", 9 + "bos", 10 + "cdg", 11 + "den", 12 + "dfw", 13 + "ewr", 14 + "eze", 15 + "fra", 16 + "gdl", 17 + "gig", 18 + "gru", 19 + "hkg", 20 + "iad", 21 + "jnb", 22 + "lax", 23 + "lhr", 24 + "mad", 25 + "mia", 26 + "nrt", 27 + "ord", 28 + "otp", 29 + "phx", 30 + "qro", 31 + "scl", 32 + "sjc", 33 + "sea", 34 + "sin", 35 + "syd", 36 + "waw", 37 + "yul", 38 + "yyz", 39 + ] as const; 40 + 41 + export const monitorPeriodicity = [ 42 + "30s", 43 + "1m", 44 + "5m", 45 + "10m", 46 + "30m", 47 + "1h", 48 + "other", 49 + ] as const; 50 + 51 + export const monitorRegions = [...flyRegions] as const; 52 + export const monitorPeriodicitySchema = z.enum(monitorPeriodicity); 53 + export const monitorRegionSchema = z.enum(monitorRegions); 54 + export const monitorFlyRegionSchema = z.enum(flyRegions); 55 + export type MonitorFlyRegion = z.infer<typeof monitorFlyRegionSchema>;
+1 -1
packages/db/src/schema/index.ts
··· 2 2 export * from "./integration"; 3 3 export * from "./pages"; 4 4 export * from "./monitors"; 5 + export * from "./workspaces"; 5 6 export * from "./users"; 6 7 export * from "./page_subscribers"; 7 - export * from "./workspaces"; 8 8 export * from "./shared"; 9 9 export * from "./notifications"; 10 10 export * from "./monitor_status";
+2 -1
packages/db/src/schema/monitor_status/validation.ts
··· 1 1 import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 2 2 import type { z } from "zod"; 3 3 4 - import { monitorRegionSchema, monitorStatusSchema } from "../monitors"; 4 + import { monitorStatusSchema } from "../monitors"; 5 5 import { monitorStatusTable } from "./monitor_status"; 6 + import { monitorRegionSchema } from "../constants"; 6 7 7 8 export const selectMonitorStatusSchema = createSelectSchema( 8 9 monitorStatusTable,
-48
packages/db/src/schema/monitors/constants.ts
··· 1 - export const flyRegions = [ 2 - "ams", 3 - "arn", 4 - "atl", 5 - "bog", 6 - "bom", 7 - "bos", 8 - "cdg", 9 - "den", 10 - "dfw", 11 - "ewr", 12 - "eze", 13 - "fra", 14 - "gdl", 15 - "gig", 16 - "gru", 17 - "hkg", 18 - "iad", 19 - "jnb", 20 - "lax", 21 - "lhr", 22 - "mad", 23 - "mia", 24 - "nrt", 25 - "ord", 26 - "otp", 27 - "phx", 28 - "qro", 29 - "scl", 30 - "sjc", 31 - "sea", 32 - "sin", 33 - "syd", 34 - "waw", 35 - "yul", 36 - "yyz", 37 - ] as const; 38 - 39 - export const monitorPeriodicity = [ 40 - "30s", 41 - "1m", 42 - "5m", 43 - "10m", 44 - "30m", 45 - "1h", 46 - "other", 47 - ] as const; 48 1 export const monitorMethods = ["GET", "POST", "HEAD"] as const; 49 2 export const monitorStatus = ["active", "error", "degraded"] as const; 50 - export const monitorRegions = [...flyRegions] as const; 51 3 52 4 export const monitorJobTypes = ["website", "cron", "other"] as const;
+3 -7
packages/db/src/schema/monitors/monitor.ts
··· 11 11 import { notificationsToMonitors } from "../notifications"; 12 12 import { page } from "../pages"; 13 13 import { monitorsToStatusReport } from "../status_reports"; 14 - import { workspace } from "../workspaces"; 15 - import { 16 - monitorJobTypes, 17 - monitorMethods, 18 - monitorPeriodicity, 19 - monitorStatus, 20 - } from "./constants"; 14 + import { workspace } from "../workspaces/workspace"; 15 + import { monitorJobTypes, monitorMethods, monitorStatus } from "./constants"; 16 + import { monitorPeriodicity } from "../constants"; 21 17 22 18 export const monitor = sqliteTable("monitor", { 23 19 id: integer("id").primaryKey(),
+2 -12
packages/db/src/schema/monitors/validation.ts
··· 3 3 4 4 import * as assertions from "@openstatus/assertions"; 5 5 6 - import { 7 - flyRegions, 8 - monitorJobTypes, 9 - monitorMethods, 10 - monitorPeriodicity, 11 - monitorRegions, 12 - monitorStatus, 13 - } from "./constants"; 6 + import { monitorJobTypes, monitorMethods, monitorStatus } from "./constants"; 14 7 import { monitor, monitorsToPages } from "./monitor"; 8 + import { monitorPeriodicitySchema, monitorRegionSchema } from "../constants"; 15 9 16 - export const monitorPeriodicitySchema = z.enum(monitorPeriodicity); 17 10 export const monitorMethodsSchema = z.enum(monitorMethods); 18 11 export const monitorStatusSchema = z.enum(monitorStatus); 19 - export const monitorRegionSchema = z.enum(monitorRegions); 20 12 export const monitorJobTypesSchema = z.enum(monitorJobTypes); 21 - export const monitorFlyRegionSchema = z.enum(flyRegions); 22 13 23 14 // TODO: shared function 24 15 // biome-ignore lint/correctness/noUnusedVariables: <explanation> ··· 90 81 export type MonitorPeriodicity = z.infer<typeof monitorPeriodicitySchema>; 91 82 export type MonitorMethod = z.infer<typeof monitorMethodsSchema>; 92 83 export type MonitorRegion = z.infer<typeof monitorRegionSchema>; 93 - export type MonitorFlyRegion = z.infer<typeof monitorFlyRegionSchema>; 94 84 export type MonitorJobType = z.infer<typeof monitorJobTypesSchema>;
+41
packages/db/src/schema/plan/schema.ts
··· 1 + import { z } from "zod"; 2 + import { monitorFlyRegionSchema, monitorPeriodicitySchema } from "../constants"; 3 + 4 + // This is not a database table but just a schema for the limits of the plan 5 + 6 + export const limitsV1 = z.object({ 7 + version: z.undefined(), 8 + monitors: z.number(), 9 + "synthetic-checks": z.number(), 10 + periodicity: monitorPeriodicitySchema.array(), 11 + "multi-region": z.boolean(), 12 + "max-regions": z.number(), 13 + "data-retention": z.enum(["14 days", "3 months", "12 months", "24 months"]), 14 + // status pages 15 + "status-pages": z.number(), 16 + maintenance: z.boolean(), 17 + "status-subscribers": z.boolean(), 18 + "custom-domain": z.boolean(), 19 + "password-protection": z.boolean(), 20 + "white-label": z.boolean(), 21 + // alerts 22 + notifications: z.boolean(), 23 + pagerduty: z.boolean(), 24 + sms: z.boolean(), 25 + "notification-channels": z.number(), 26 + // collaboration 27 + members: z.literal("Unlimited").or(z.number()), 28 + "audit-log": z.boolean(), 29 + regions: monitorFlyRegionSchema.array(), 30 + }); 31 + 32 + const unknownLimit = z.discriminatedUnion("version", [limitsV1]); 33 + 34 + // export const limitSchema = unknownLimit.transform((val) => { 35 + // if (!val.version) { 36 + // return migrateFromV1ToV2(val); 37 + // } 38 + // return val; 39 + // }); 40 + 41 + export type Limits = z.infer<typeof unknownLimit>;
+15
packages/db/src/schema/plan/utils.ts
··· 1 + import type { Limits } from "./schema"; 2 + import { allPlans } from "./config"; 3 + import type { WorkspacePlan } from "../workspaces/validation"; 4 + 5 + export function getLimit<T extends keyof Limits>(limits: Limits, limit: T) { 6 + return limits[limit] || allPlans.free.limits[limit]; 7 + } 8 + 9 + export function getLimits(plan: WorkspacePlan | null) { 10 + return allPlans[plan || "free"].limits; 11 + } 12 + 13 + export function getPlanConfig(plan: WorkspacePlan | null) { 14 + return allPlans[plan || "free"]; 15 + }
+4 -4
packages/db/src/schema/shared.ts
··· 26 26 monitorId: z.number(), 27 27 statusReportId: z.number(), 28 28 monitor: selectPublicMonitorSchema, 29 - }), 29 + }) 30 30 ) 31 31 .default([]), 32 32 }); ··· 37 37 z.object({ 38 38 monitorId: z.number(), 39 39 maintenanceId: z.number(), 40 - }), 40 + }) 41 41 ) 42 42 .default([]), 43 43 }); ··· 56 56 pageId: z.number(), 57 57 order: z.number().default(0).optional(), 58 58 monitor: selectMonitorSchema, 59 - }), 59 + }) 60 60 ), 61 61 maintenancesToPages: selectMaintenanceSchema.array().default([]), 62 62 }); ··· 85 85 monitorId: z.number(), 86 86 statusReportId: z.number(), 87 87 monitor: selectPublicMonitorSchema, 88 - }), 88 + }) 89 89 ) 90 90 .default([]), 91 91 statusReportUpdates: z.array(selectStatusReportUpdateSchema),
+14
packages/db/src/schema/workspaces/validation.ts
··· 3 3 4 4 import { workspacePlans, workspaceRole } from "./constants"; 5 5 import { workspace } from "./workspace"; 6 + import { limitsV1 } from "../plan/schema"; 7 + import { allPlans } from "../plan/config"; 6 8 7 9 export const workspacePlanSchema = z.enum(workspacePlans); 8 10 export const workspaceRoleSchema = z.enum(workspaceRole); 9 11 10 12 export const selectWorkspaceSchema = createSelectSchema(workspace).extend({ 13 + limits: z.string().transform((val) => { 14 + const parsed = JSON.parse(val); 15 + const result = limitsV1.safeParse(parsed); 16 + if (result.error) { 17 + // Fallback to default limits 18 + return limitsV1.parse({ 19 + ...allPlans.free.limits, 20 + }); 21 + } 22 + 23 + return result.data; 24 + }), 11 25 plan: z 12 26 .enum(workspacePlans) 13 27 .nullable()
+4 -4
packages/db/src/schema/workspaces/workspace.ts
··· 17 17 plan: text("plan", { enum: workspacePlans }), 18 18 endsAt: integer("ends_at", { mode: "timestamp" }), 19 19 paidUntil: integer("paid_until", { mode: "timestamp" }), 20 - 20 + limits: text("limits").default("{}").notNull(), 21 21 createdAt: integer("created_at", { mode: "timestamp" }).default( 22 - sql`(strftime('%s', 'now'))`, 22 + sql`(strftime('%s', 'now'))` 23 23 ), 24 24 updatedAt: integer("updated_at", { mode: "timestamp" }).default( 25 - sql`(strftime('%s', 'now'))`, 25 + sql`(strftime('%s', 'now'))` 26 26 ), 27 27 28 28 dsn: text("dsn"), // should be removed soon 29 29 }, 30 30 (t) => ({ 31 31 unique: unique().on(t.id, t.dsn), 32 - }), 32 + }) 33 33 ); 34 34 35 35 export const workspaceRelations = relations(workspace, ({ many }) => ({
+4 -2
packages/db/src/seed.mts
··· 35 35 plan: "pro", 36 36 endsAt: null, 37 37 paidUntil: null, 38 + limits: 39 + '{"monitors":50,"synthetic-checks":150000,"periodicity":["30s","1m","5m","10m","30m","1h"],"multi-region":true,"max-regions":35,"data-retention":"24 months","status-pages":20,"maintenance":true,"status-subscribers":true,"custom-domain":true,"password-protection":true,"white-label":true,"notifications":true,"sms":true,"pagerduty":true,"notification-channels":50,"members":"Unlimited","audit-log":true,"regions":["ams","arn","atl","bog","bom","bos","cdg","den","dfw","ewr","eze","fra","gdl","gig","gru","hkg","iad","jnb","lax","lhr","mad","mia","nrt","ord","otp","phx","qro","scl","sea","sin","sjc","syd","waw","yul","yyz"]}', 38 40 }, 39 41 { 40 42 id: 2, ··· 142 144 .values({ 143 145 id: 1, 144 146 workspaceId: 1, 145 - pageId:1, 147 + pageId: 1, 146 148 title: "Test Status Report", 147 149 status: "investigating", 148 150 updatedAt: new Date(), ··· 165 167 .values({ 166 168 id: 2, 167 169 workspaceId: 1, 168 - pageId:1, 170 + pageId: 1, 169 171 title: "Test Status Report", 170 172 status: "investigating", 171 173 updatedAt: new Date(),
-18
packages/plans/package.json
··· 1 - { 2 - "name": "@openstatus/plans", 3 - "version": "1.0.0", 4 - "description": "", 5 - "main": "src/index.ts", 6 - "scripts": {}, 7 - "dependencies": { 8 - "@openstatus/db": "workspace:*", 9 - "zod": "3.23.8" 10 - }, 11 - "devDependencies": { 12 - "@openstatus/tsconfig": "workspace:*", 13 - "typescript": "5.5.2" 14 - }, 15 - "keywords": [], 16 - "author": "", 17 - "license": "ISC" 18 - }
+13 -10
packages/plans/src/config.ts packages/db/src/schema/plan/config.ts
··· 1 - import type { WorkspacePlan } from "@openstatus/db/src/schema"; 2 - 3 - import type { Limits } from "./types"; 1 + import type { WorkspacePlan } from "../workspaces/validation"; 2 + import type { Limits } from "./schema"; 4 3 5 4 // TODO: rename to `planConfig` 6 5 export const allPlans: Record< ··· 17 16 description: "For personal projects", 18 17 price: 0, 19 18 limits: { 20 - monitors: 3, 19 + monitors: 1, 20 + "synthetic-checks": 1000, 21 21 periodicity: ["10m", "30m", "1h"], 22 22 "multi-region": true, 23 23 "max-regions": 6, ··· 40 40 starter: { 41 41 title: "Starter", 42 42 description: "For small projects", 43 - price: 29, 43 + price: 30, 44 44 limits: { 45 - monitors: 30, 45 + monitors: 5, 46 + "synthetic-checks": 10000, 46 47 periodicity: ["1m", "5m", "10m", "30m", "1h"], 47 48 "multi-region": true, 48 49 "max-regions": 35, ··· 101 102 team: { 102 103 title: "Growth", 103 104 description: "For small teams", 104 - price: 79, 105 + price: 100, 105 106 limits: { 106 - monitors: 100, 107 + monitors: 15, 108 + "synthetic-checks": 50000, 107 109 periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"], 108 110 "multi-region": true, 109 111 "max-regions": 35, ··· 162 164 pro: { 163 165 title: "Pro", 164 166 description: "For bigger teams", 165 - price: 149, 167 + price: 300, 166 168 limits: { 167 - monitors: 500, 169 + monitors: 50, 170 + "synthetic-checks": 150000, 168 171 periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"], 169 172 "multi-region": true, 170 173 "max-regions": 35,
-7
packages/plans/src/index.ts
··· 1 - export * from "./config"; 2 - export * from "./utils"; 3 - export * from "./pricing-table"; 4 - export * from "./types"; 5 - 6 - export { workspacePlans as plans } from "@openstatus/db/src/schema"; 7 - export type { WorkspacePlan } from "@openstatus/db/src/schema";
+10 -1
packages/plans/src/pricing-table.ts apps/web/src/config/pricing-table.ts
··· 1 - import type { Limits } from "./types"; 1 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 2 2 3 3 export const pricingTableConfig: Record< 4 4 string, ··· 24 24 }, 25 25 { value: "max-regions", label: "Number of Regions" }, 26 26 { value: "data-retention", label: "Data retention" }, 27 + ], 28 + }, 29 + "synthetic-checks": { 30 + label: "Synthetic API Checks", 31 + features: [ 32 + { 33 + value: "synthetic-checks", 34 + label: "Number of synthetic API checks", 35 + }, 27 36 ], 28 37 }, 29 38 "status-pages": {
-29
packages/plans/src/types.ts
··· 1 - import type { 2 - MonitorFlyRegion, 3 - MonitorPeriodicity, 4 - } from "@openstatus/db/src/schema"; 5 - 6 - export type Limits = { 7 - // monitors 8 - monitors: number; 9 - periodicity: Partial<MonitorPeriodicity>[]; 10 - "multi-region": boolean; 11 - "max-regions": number; 12 - "data-retention": string; 13 - // status pages 14 - "status-pages": number; 15 - maintenance: boolean; 16 - "status-subscribers": boolean; 17 - "custom-domain": boolean; 18 - "password-protection": boolean; 19 - "white-label": boolean; 20 - // alerts 21 - notifications: boolean; 22 - pagerduty: boolean; 23 - sms: boolean; 24 - "notification-channels": number; 25 - // collaboration 26 - members: "Unlimited" | number; 27 - "audit-log": boolean; 28 - regions: Partial<MonitorFlyRegion>[]; 29 - };
-20
packages/plans/src/utils.ts
··· 1 - import type { WorkspacePlan } from "@openstatus/db/src/schema"; 2 - 3 - import { allPlans } from "./index"; 4 - import type { Limits } from "./types"; 5 - 6 - // TODO: use getLimit utils function 7 - export function getLimit<T extends keyof Limits>( 8 - plan: WorkspacePlan, 9 - limit: T, 10 - ) { 11 - return allPlans[plan].limits[limit]; 12 - } 13 - 14 - export function getLimits(plan: WorkspacePlan | null) { 15 - return allPlans[plan || "free"].limits; 16 - } 17 - 18 - export function getPlanConfig(plan: WorkspacePlan | null) { 19 - return allPlans[plan || "free"]; 20 - }
-4
packages/plans/tsconfig.json
··· 1 - { 2 - "extends": "@openstatus/tsconfig/base.json", 3 - "include": ["src", "*.ts"] 4 - }
+1 -1
packages/tinybird/src/os-client.ts
··· 1 1 import { NoopTinybird, Tinybird } from "@chronark/zod-bird"; 2 2 import { z } from "zod"; 3 3 4 - import { flyRegions } from "../../db/src/schema/monitors/constants"; 4 + import { flyRegions } from "../../db/src/schema/constants"; 5 5 6 6 import type { tbIngestWebVitalsArray } from "./validation"; 7 7 import {
+2 -2
packages/tinybird/src/validation.ts
··· 1 1 import * as z from "zod"; 2 - import { monitorFlyRegionSchema } from "../../db/src/schema/monitors/validation"; 3 - import type { flyRegions } from "../../db/src/schema/monitors/constants"; 2 + import { monitorFlyRegionSchema } from "../../db/src/schema/constants"; 3 + import type { flyRegions } from "../../db/src/schema/constants"; 4 4 5 5 export const tbIngestWebVitals = z.object({ 6 6 dsn: z.string(),
+1
packages/ui/package.json
··· 38 38 "@radix-ui/react-radio-group": "1.1.3", 39 39 "@radix-ui/react-select": "2.0.0", 40 40 "@radix-ui/react-separator": "1.0.3", 41 + "@radix-ui/react-slider": "1.2.0", 41 42 "@radix-ui/react-slot": "1.0.2", 42 43 "@radix-ui/react-switch": "1.0.3", 43 44 "@radix-ui/react-tabs": "1.0.4",
+28
packages/ui/src/components/slider.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as SliderPrimitive from "@radix-ui/react-slider"; 5 + 6 + import { cn } from "../lib/utils"; 7 + 8 + const Slider = React.forwardRef< 9 + React.ElementRef<typeof SliderPrimitive.Root>, 10 + React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> 11 + >(({ className, ...props }, ref) => ( 12 + <SliderPrimitive.Root 13 + ref={ref} 14 + className={cn( 15 + "relative flex w-full touch-none select-none items-center", 16 + className 17 + )} 18 + {...props} 19 + > 20 + <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> 21 + <SliderPrimitive.Range className="absolute h-full bg-primary" /> 22 + </SliderPrimitive.Track> 23 + <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" /> 24 + </SliderPrimitive.Root> 25 + )); 26 + Slider.displayName = SliderPrimitive.Root.displayName; 27 + 28 + export { Slider };
+1
packages/ui/src/index.tsx
··· 38 38 export * from "./components/sonner"; 39 39 export * from "./components/sortable"; 40 40 export * from "./components/navigation-menu"; 41 + export * from "./components/slider";
+2 -2
packages/utils/index.ts
··· 3 3 * https://vercel.com/docs/concepts/edge-network/regions#region-list 4 4 */ 5 5 6 - import type { MonitorFlyRegion } from "@openstatus/db/src/schema"; 6 + import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 7 7 8 8 // export const vercelRegionsDict = { 9 9 // /** ··· 392 392 Oceania: [], 393 393 Asia: [], 394 394 Africa: [], 395 - }, 395 + } 396 396 ); 397 397 398 398 export const vercelRegions = [
+56 -25
pnpm-lock.yaml
··· 178 178 '@openstatus/notification-twillio-sms': 179 179 specifier: workspace:* 180 180 version: link:../../packages/notifications/twillio-sms 181 - '@openstatus/plans': 182 - specifier: workspace:* 183 - version: link:../../packages/plans 184 181 '@openstatus/tinybird': 185 182 specifier: workspace:* 186 183 version: link:../../packages/tinybird ··· 290 287 '@openstatus/notification-slack': 291 288 specifier: workspace:* 292 289 version: link:../../packages/notifications/slack 293 - '@openstatus/plans': 294 - specifier: workspace:* 295 - version: link:../../packages/plans 296 290 '@openstatus/react': 297 291 specifier: workspace:* 298 292 version: link:../../packages/react ··· 559 553 '@openstatus/error': 560 554 specifier: workspace:* 561 555 version: link:../error 562 - '@openstatus/plans': 563 - specifier: workspace:* 564 - version: link:../plans 565 556 '@openstatus/tinybird': 566 557 specifier: workspace:* 567 558 version: link:../tinybird ··· 908 899 specifier: 5.5.2 909 900 version: 5.5.2 910 901 911 - packages/plans: 912 - dependencies: 913 - '@openstatus/db': 914 - specifier: workspace:* 915 - version: link:../db 916 - zod: 917 - specifier: 3.23.8 918 - version: 3.23.8 919 - devDependencies: 920 - '@openstatus/tsconfig': 921 - specifier: workspace:* 922 - version: link:../tsconfig 923 - typescript: 924 - specifier: 5.5.2 925 - version: 5.5.2 926 - 927 902 packages/react: 928 903 dependencies: 929 904 react: ··· 1070 1045 '@radix-ui/react-separator': 1071 1046 specifier: 1.0.3 1072 1047 version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 1048 + '@radix-ui/react-slider': 1049 + specifier: 1.2.0 1050 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 1073 1051 '@radix-ui/react-slot': 1074 1052 specifier: 1.0.2 1075 1053 version: 1.0.2(@types/react@18.3.3)(react@18.3.1) ··· 3593 3571 '@radix-ui/number@1.0.1': 3594 3572 resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} 3595 3573 3574 + '@radix-ui/number@1.1.0': 3575 + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} 3576 + 3596 3577 '@radix-ui/primitive@1.0.1': 3597 3578 resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} 3598 3579 ··· 4127 4108 '@types/react-dom': 4128 4109 optional: true 4129 4110 4111 + '@radix-ui/react-slider@1.2.0': 4112 + resolution: {integrity: sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA==} 4113 + peerDependencies: 4114 + '@types/react': '*' 4115 + '@types/react-dom': '*' 4116 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 4117 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 4118 + peerDependenciesMeta: 4119 + '@types/react': 4120 + optional: true 4121 + '@types/react-dom': 4122 + optional: true 4123 + 4130 4124 '@radix-ui/react-slot@1.0.0': 4131 4125 resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} 4132 4126 peerDependencies: ··· 4306 4300 peerDependencies: 4307 4301 '@types/react': '*' 4308 4302 react: ^16.8 || ^17.0 || ^18.0 4303 + peerDependenciesMeta: 4304 + '@types/react': 4305 + optional: true 4306 + 4307 + '@radix-ui/react-use-size@1.1.0': 4308 + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} 4309 + peerDependencies: 4310 + '@types/react': '*' 4311 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 4309 4312 peerDependenciesMeta: 4310 4313 '@types/react': 4311 4314 optional: true ··· 13195 13198 dependencies: 13196 13199 '@babel/runtime': 7.23.2 13197 13200 13201 + '@radix-ui/number@1.1.0': {} 13202 + 13198 13203 '@radix-ui/primitive@1.0.1': 13199 13204 dependencies: 13200 13205 '@babel/runtime': 7.23.2 ··· 13792 13797 '@types/react': 18.3.3 13793 13798 '@types/react-dom': 18.3.0 13794 13799 13800 + '@radix-ui/react-slider@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': 13801 + dependencies: 13802 + '@radix-ui/number': 1.1.0 13803 + '@radix-ui/primitive': 1.1.0 13804 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 13805 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13806 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13807 + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13808 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 13809 + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13810 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13811 + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13812 + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13813 + react: 18.3.1 13814 + react-dom: 18.3.1(react@18.3.1) 13815 + optionalDependencies: 13816 + '@types/react': 18.3.3 13817 + '@types/react-dom': 18.3.0 13818 + 13795 13819 '@radix-ui/react-slot@1.0.0(react@18.2.0)': 13796 13820 dependencies: 13797 13821 '@babel/runtime': 7.23.2 ··· 13960 13984 dependencies: 13961 13985 '@babel/runtime': 7.23.2 13962 13986 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) 13987 + react: 18.3.1 13988 + optionalDependencies: 13989 + '@types/react': 18.3.3 13990 + 13991 + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.3)(react@18.3.1)': 13992 + dependencies: 13993 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) 13963 13994 react: 18.3.1 13964 13995 optionalDependencies: 13965 13996 '@types/react': 18.3.3