Openstatus www.openstatus.dev

feat: public monitor (#765)

* feat: public monitor

* fix: import

* fix: layout

* wip: update test, create changelog, update home monitor card

* fix: unrelated monitors on status pages

* refactor: pathname

* fix: unknown gap

* chore: update home cta

* feat: public monitor loading page

authored by

Maximilian Kaske and committed by
GitHub
ac438dc3 ae6068c5

+2855 -326
+11 -1
apps/server/src/v1/monitor.test.ts
··· 1 1 import { expect, test } from "bun:test"; 2 2 3 3 import { api } from "."; 4 - import { iso8601Regex } from "./test-utils"; 5 4 6 5 test("GET one monitor", async () => { 7 6 const res = await api.request("/monitor/1", { ··· 22 21 body: '{"hello":"world"}', 23 22 headers: [{ key: "key", value: "value" }], 24 23 active: true, 24 + public: false, 25 25 }); 26 26 }); 27 27 ··· 43 43 body: '{"hello":"world"}', 44 44 headers: [{ key: "key", value: "value" }], 45 45 active: true, 46 + public: false, 46 47 }); 47 48 }); 48 49 ··· 57 58 body: '{"hello":"world"}', 58 59 headers: [{ key: "key", value: "value" }], 59 60 active: true, 61 + public: true, 60 62 }; 61 63 const res = await api.request("/monitor", { 62 64 method: "POST", ··· 79 81 body: '{"hello":"world"}', 80 82 headers: [{ key: "key", value: "value" }], 81 83 active: true, 84 + public: true, 82 85 }); 83 86 }); 84 87 ··· 93 96 body: '{"hello":"world"}', 94 97 headers: [{ key: "key", value: "value" }], 95 98 active: true, 99 + public: false, 96 100 }; 97 101 const res = await api.request("/monitor", { 98 102 method: "POST", ··· 115 119 body: '{"hello":"world"}', 116 120 headers: [{ key: "key", value: "value" }], 117 121 active: true, 122 + public: false, 118 123 }; 119 124 const res = await api.request("/monitor", { 120 125 method: "POST", ··· 145 150 body: '{"hello":"world"}', 146 151 headers: [{ key: "key", value: "value" }], 147 152 active: true, 153 + public: true, 148 154 }; 149 155 150 156 const res = await api.request("/monitor/1", { ··· 168 174 body: '{"hello":"world"}', 169 175 headers: [{ key: "key", value: "value" }], 170 176 active: true, 177 + public: true, 171 178 }); 172 179 }); 173 180 ··· 182 189 body: '{"hello":"world"}', 183 190 headers: [{ key: "key", value: "value" }], 184 191 active: true, 192 + public: false, 185 193 }; 186 194 187 195 const res = await api.request("/monitor/404", { ··· 212 220 body: '{"hello":"world"}', 213 221 headers: [{ key: "key", value: "value" }], 214 222 active: true, 223 + public: false, 215 224 }; 216 225 const res = await api.request("/monitor/2", { 217 226 method: "PUT", ··· 233 242 body: '{"hello":"world"}', 234 243 headers: [{ key: "key", value: "value" }], 235 244 active: true, 245 + public: false, 236 246 }; 237 247 const res = await api.request("/monitor/2", { 238 248 method: "PUT",
+7
apps/server/src/v1/monitor.ts
··· 110 110 .boolean() 111 111 .default(false) 112 112 .openapi({ description: "If the monitor is active" }), 113 + public: z 114 + .boolean() 115 + .default(false) 116 + .openapi({ description: "If the monitor is public" }), 113 117 }) 114 118 .openapi({ 115 119 description: "The monitor", ··· 145 149 }), 146 150 active: z.boolean().default(false).openapi({ 147 151 description: "If the monitor is active", 152 + }), 153 + public: z.boolean().default(false).openapi({ 154 + description: "If the monitor is public", 148 155 }), 149 156 headers: z 150 157 .preprocess(
+1
apps/web/package.json
··· 33 33 "@sentry/nextjs": "7.100.1", 34 34 "@stripe/stripe-js": "2.1.6", 35 35 "@t3-oss/env-nextjs": "0.7.0", 36 + "@tailwindcss/container-queries": "0.1.1", 36 37 "@tailwindcss/typography": "0.5.10", 37 38 "@tanstack/react-table": "8.10.3", 38 39 "@tremor/react": "3.13.3",
apps/web/public/assets/changelog/public-monitors.png

This is a binary file and will not be displayed.

+2 -2
apps/web/src/app/api/og/_components/status-check.tsx
··· 23 23 const Icon = getVariant(); 24 24 25 25 return ( 26 - <div tw="flex flex-col justify-center items-center gap-2 w-full"> 27 - <div tw={cn("flex text-white rounded-full p-3 border-2", className)}> 26 + <div tw="flex flex-col justify-center items-center w-full"> 27 + <div tw={cn("flex text-white rounded-full p-3 border-2 mb-2", className)}> 28 28 <Icon /> 29 29 </div> 30 30 <p style={{ fontFamily: "Cal" }} tw="text-4xl">
+10 -6
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/layout.tsx
··· 35 35 active={monitor.active} 36 36 status={monitor.status} 37 37 /> 38 - <span className="text-muted-foreground/50 text-xs">•</span> 39 - <TagBadgeWithTooltip 40 - tags={monitor.monitorTagsToMonitors.map( 41 - ({ monitorTag }) => monitorTag, 42 - )} 43 - /> 38 + {monitor.monitorTagsToMonitors.length > 0 ? ( 39 + <> 40 + <span className="text-muted-foreground/50 text-xs">•</span> 41 + <TagBadgeWithTooltip 42 + tags={monitor.monitorTagsToMonitors.map( 43 + ({ monitorTag }) => monitorTag, 44 + )} 45 + /> 46 + </> 47 + ) : null} 44 48 <span className="text-muted-foreground/50 text-xs">•</span> 45 49 <span className="text-sm"> 46 50 every <code>{monitor.periodicity}</code>
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 110 110 <DatePickerPreset defaultValue={period} values={periods} /> 111 111 {isDirty ? <ButtonReset /> : null} 112 112 </div> 113 - <Metrics metrics={metrics} period={period} /> 113 + <Metrics metrics={metrics} period={period} showErrorLink /> 114 114 <Separator className="my-8" /> 115 115 <CombinedChartWrapper 116 116 data={data}
+2 -1
apps/web/src/app/page.tsx
··· 1 1 import { MarketingLayout } from "@/components/layout/marketing-layout"; 2 2 import { AlertCard } from "@/components/marketing/alert/card"; 3 - import { BottomCTA } from "@/components/marketing/bottom-cta"; 4 3 import { Hero } from "@/components/marketing/hero"; 4 + import { BottomCTA, MiddleCTA } from "@/components/marketing/in-between-cta"; 5 5 import { LatestChangelogs } from "@/components/marketing/lastest-changelogs"; 6 6 import { MonitoringCard } from "@/components/marketing/monitor/card"; 7 7 import { Partners } from "@/components/marketing/partners"; ··· 18 18 <Partners /> 19 19 <MonitoringCard /> 20 20 <Stats /> 21 + <MiddleCTA /> 21 22 <StatusPageCard /> 22 23 <AlertCard /> 23 24 <BottomCTA />
+15 -3
apps/web/src/app/play/page.tsx
··· 1 1 import type { Metadata } from "next"; 2 - import { Clock, FileCode, Gauge, Palette, PanelTop } from "lucide-react"; 2 + import { 3 + Activity, 4 + Clock, 5 + FileCode, 6 + Gauge, 7 + Palette, 8 + PanelTop, 9 + } from "lucide-react"; 3 10 4 11 import { BackButton } from "@/components/layout/back-button"; 5 12 import type { CardProps } from "@/components/play/card"; ··· 54 61 variant: "primary", 55 62 }, 56 63 { 64 + href: "/public/monitors/1", 65 + title: "Public Dashboard", 66 + description: "Get a demo of wha data we collect for your monitor.", 67 + icon: Activity, 68 + }, 69 + { 57 70 href: "/play/status", 58 71 title: "Status Page", 59 - description: 60 - "Get a status page for your website or api, supporting timezones.", 72 + description: "Get a status page for your website or api.", 61 73 icon: PanelTop, 62 74 }, 63 75 {
+29
apps/web/src/app/public/layout.tsx
··· 1 + import * as React from "react"; 2 + import type { Metadata } from "next"; 3 + 4 + import { 5 + defaultMetadata, 6 + ogMetadata, 7 + twitterMetadata, 8 + } from "@/app/shared-metadata"; 9 + import { PublicLayout } from "@/components/layout/public-layout"; 10 + 11 + export const metadata: Metadata = { 12 + ...defaultMetadata, 13 + twitter: { 14 + ...twitterMetadata, 15 + }, 16 + openGraph: { 17 + ...ogMetadata, 18 + }, 19 + }; 20 + 21 + export default function DemoLayout({ 22 + children, 23 + }: { 24 + children: React.ReactNode; 25 + }) { 26 + // TODO: move first sticky Shell into here 27 + // that will allow to use a loading state and have it already 28 + return <PublicLayout>{children}</PublicLayout>; 29 + }
+54
apps/web/src/app/public/monitors/[id]/loading.tsx
··· 1 + import { Separator, Skeleton } from "@openstatus/ui"; 2 + 3 + import { Shell } from "@/components/dashboard/shell"; 4 + 5 + export default function Loading() { 6 + return ( 7 + <div className="relative flex w-full flex-col gap-6"> 8 + <Shell className="flex items-center justify-between gap-2"> 9 + <div className="grid gap-2"> 10 + <Skeleton className="h-6 w-24 md:w-40" /> 11 + <Skeleton className="h-4 w-32 md:w-60" /> 12 + </div> 13 + <div> 14 + <Skeleton className="h-10 w-[150px]" /> 15 + </div> 16 + </Shell> 17 + <Shell className="grid gap-4"> 18 + <div className="grid gap-6"> 19 + <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 md:gap-6"> 20 + {new Array(4).fill(0).map((_, i) => ( 21 + <Skeleton key={i} className="h-16 w-full" /> 22 + ))} 23 + </div> 24 + <div className="grid gap-4"> 25 + <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 md:gap-6"> 26 + {new Array(5).fill(0).map((_, i) => ( 27 + <Skeleton key={i} className="h-16 w-full" /> 28 + ))} 29 + </div> 30 + <Skeleton className="h-3 w-40" /> 31 + </div> 32 + </div> 33 + <Separator className="my-8" /> 34 + <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between"> 35 + <div className="flex w-full gap-2 sm:flex-row sm:justify-between"> 36 + <Skeleton className="h-10 w-[150px]" /> 37 + <Skeleton className="h-10 w-[150px]" /> 38 + </div> 39 + <div className="flex gap-2"> 40 + <div className="grid gap-1"> 41 + <Skeleton className="h-4 w-12" /> 42 + <Skeleton className="h-10 w-[150px]" /> 43 + </div> 44 + <div className="grid gap-1"> 45 + <Skeleton className="h-4 w-12" /> 46 + <Skeleton className="h-10 w-[150px]" /> 47 + </div> 48 + </div> 49 + </div> 50 + <Skeleton className="h-[396px] w-full" /> 51 + </Shell> 52 + </div> 53 + ); 54 + }
+130
apps/web/src/app/public/monitors/[id]/page.tsx
··· 1 + import * as React from "react"; 2 + import { notFound } from "next/navigation"; 3 + import * as z from "zod"; 4 + 5 + import { flyRegions } from "@openstatus/db/src/schema"; 6 + import type { Region } from "@openstatus/tinybird"; 7 + import { OSTinybird } from "@openstatus/tinybird"; 8 + import { Separator } from "@openstatus/ui"; 9 + 10 + import { Shell } from "@/components/dashboard/shell"; 11 + import { CombinedChartWrapper } from "@/components/monitor-charts/combined-chart-wrapper"; 12 + import { ButtonReset } from "@/components/monitor-dashboard/button-reset"; 13 + import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 14 + import { Metrics } from "@/components/monitor-dashboard/metrics"; 15 + import { env } from "@/env"; 16 + import { 17 + getMinutesByInterval, 18 + intervals, 19 + periods, 20 + quantiles, 21 + } from "@/lib/monitor/utils"; 22 + import { getPreferredSettings } from "@/lib/preferred-settings/server"; 23 + import { api } from "@/trpc/server"; 24 + 25 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 26 + 27 + const DEFAULT_QUANTILE = "p95"; 28 + const DEFAULT_INTERVAL = "30m"; 29 + const DEFAULT_PERIOD = "1d"; 30 + 31 + /** 32 + * allowed URL search params 33 + */ 34 + const searchParamsSchema = z.object({ 35 + statusCode: z.coerce.number().optional(), 36 + cronTimestamp: z.coerce.number().optional(), 37 + quantile: z.enum(quantiles).optional().default(DEFAULT_QUANTILE), 38 + interval: z.enum(intervals).optional().default(DEFAULT_INTERVAL), 39 + period: z.enum(periods).optional().default(DEFAULT_PERIOD), 40 + regions: z 41 + .string() 42 + .optional() 43 + .transform( 44 + (value) => 45 + value 46 + ?.trim() 47 + ?.split(",") 48 + .filter((i) => flyRegions.includes(i as Region)) ?? flyRegions, 49 + ), 50 + }); 51 + 52 + export default async function Page({ 53 + params, 54 + searchParams, 55 + }: { 56 + params: { workspaceSlug: string; id: string }; 57 + searchParams: { [key: string]: string | string[] | undefined }; 58 + }) { 59 + const id = params.id; 60 + const search = searchParamsSchema.safeParse(searchParams); 61 + const preferredSettings = getPreferredSettings(); 62 + 63 + const monitor = await api.monitor.getPublicMonitorById.query({ 64 + id: Number(id), 65 + }); 66 + 67 + if (!monitor || !search.success) { 68 + return notFound(); 69 + } 70 + 71 + const { period, quantile, interval, regions } = search.data; 72 + 73 + // TODO: work it out easier 74 + const intervalMinutes = getMinutesByInterval(interval); 75 + const periodicityMinutes = getMinutesByInterval(monitor.periodicity); 76 + 77 + const isQuantileDisabled = intervalMinutes <= periodicityMinutes; 78 + const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 79 + 80 + const metrics = await tb.endpointMetrics(period)({ monitorId: id }); 81 + 82 + const data = await tb.endpointChart(period)({ 83 + monitorId: id, 84 + interval: minutes, 85 + }); 86 + 87 + const metricsByRegion = await tb.endpointMetricsByRegion(period)({ 88 + monitorId: id, 89 + }); 90 + 91 + if (!data || !metrics || !metricsByRegion) return null; 92 + 93 + const isDirty = 94 + period !== DEFAULT_PERIOD || 95 + quantile !== DEFAULT_QUANTILE || 96 + interval !== DEFAULT_INTERVAL || 97 + flyRegions.length !== regions.length; 98 + 99 + return ( 100 + <div className="relative flex w-full flex-col gap-6"> 101 + <Shell className="bg-background/80 sticky top-2 z-10 flex items-center justify-between gap-2 backdrop-blur-sm"> 102 + <div className="min-w-0"> 103 + <p className="text-sm font-semibold">{monitor.name}</p> 104 + <p className="text-muted-foreground truncate text-base"> 105 + {monitor.url} 106 + </p> 107 + </div> 108 + <div className="flex items-center gap-2"> 109 + {isDirty ? <ButtonReset /> : null} 110 + <DatePickerPreset defaultValue={period} values={periods} /> 111 + </div> 112 + </Shell> 113 + <Shell className="grid gap-4"> 114 + <Metrics metrics={metrics} period={period} /> 115 + <Separator className="my-8" /> 116 + <CombinedChartWrapper 117 + data={data} 118 + period={period} 119 + quantile={quantile} 120 + interval={interval} 121 + regions={regions as Region[]} // FIXME: not properly reseted after filtered 122 + monitor={monitor} 123 + isQuantileDisabled={isQuantileDisabled} 124 + metricsByRegion={metricsByRegion} 125 + preferredSettings={preferredSettings} 126 + /> 127 + </Shell> 128 + </div> 129 + ); 130 + }
+31
apps/web/src/app/status-page/[domain]/_components/footer.tsx
··· 1 + import { allPlans } from "@openstatus/plans"; 2 + import type { WorkspacePlan } from "@openstatus/plans"; 3 + 4 + import { ThemeToggle } from "@/components/theme-toggle"; 5 + 6 + interface Props { 7 + plan: WorkspacePlan; 8 + } 9 + 10 + export function Footer({ plan }: Props) { 11 + const isWhiteLabel = allPlans[plan].limits["white-label"]; 12 + return ( 13 + <footer className="z-10 mx-auto flex w-full items-center justify-between"> 14 + <div /> 15 + {!isWhiteLabel ? ( 16 + <p className="text-muted-foreground text-center text-sm"> 17 + powered by{" "} 18 + <a 19 + href="https://www.openstatus.dev" 20 + target="_blank" 21 + rel="noreferrer" 22 + className="text-foreground underline underline-offset-4 hover:no-underline" 23 + > 24 + openstatus.dev 25 + </a> 26 + </p> 27 + ) : null} 28 + <ThemeToggle /> 29 + </footer> 30 + ); 31 + }
+67
apps/web/src/app/status-page/[domain]/_components/header.tsx
··· 1 + "use client"; 2 + 3 + import Image from "next/image"; 4 + import { useSelectedLayoutSegment } from "next/navigation"; 5 + 6 + import type { PublicPage } from "@openstatus/db/src/schema"; 7 + import { allPlans } from "@openstatus/plans"; 8 + import type { WorkspacePlan } from "@openstatus/plans"; 9 + 10 + import { Shell } from "@/components/dashboard/shell"; 11 + import { TabsContainer, TabsLink } from "@/components/dashboard/tabs-link"; 12 + import { Menu } from "./menu"; 13 + import { SubscribeButton } from "./subscribe-button"; 14 + 15 + type Props = { 16 + navigation: { 17 + label: string; 18 + href: string; 19 + segment: string | null; 20 + disabled?: boolean; 21 + }[]; 22 + plan: WorkspacePlan; 23 + page: PublicPage; 24 + }; 25 + 26 + export function Header({ navigation, plan, page }: Props) { 27 + const selectedSegment = useSelectedLayoutSegment(); 28 + const isSubscribers = allPlans[plan].limits["status-subscribers"]; 29 + 30 + return ( 31 + <header className="w-full"> 32 + <Shell className="flex items-center justify-between gap-4 px-3 py-3 md:px-6 md:py-3"> 33 + <div className="relative sm:w-[100px]"> 34 + {page?.icon ? ( 35 + <div className="bg-muted border-border flex h-7 w-7 items-center justify-center overflow-hidden rounded-full border"> 36 + <Image 37 + height={36} 38 + width={36} 39 + src={page.icon} 40 + alt={page.title} 41 + className="object-cover" 42 + /> 43 + </div> 44 + ) : null} 45 + </div> 46 + <TabsContainer className="-mb-[13px] hidden sm:block" hideSeparator> 47 + {navigation.map(({ label, href, disabled, segment }) => { 48 + const active = segment === selectedSegment; 49 + return ( 50 + <TabsLink key={segment} {...{ active, href, label, disabled }}> 51 + {label} 52 + </TabsLink> 53 + ); 54 + })} 55 + </TabsContainer> 56 + <div className="flex items-center gap-4"> 57 + <div className="text-end sm:w-[100px]"> 58 + {isSubscribers ? <SubscribeButton slug={page.slug} /> : null} 59 + </div> 60 + <div className="block sm:hidden"> 61 + <Menu navigation={navigation} /> 62 + </div> 63 + </div> 64 + </Shell> 65 + </header> 66 + ); 67 + }
+65
apps/web/src/app/status-page/[domain]/_components/menu.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { usePathname, useSelectedLayoutSegment } from "next/navigation"; 5 + import { MenuIcon } from "lucide-react"; 6 + 7 + import { 8 + Button, 9 + Sheet, 10 + SheetContent, 11 + SheetHeader, 12 + SheetTitle, 13 + SheetTrigger, 14 + } from "@openstatus/ui"; 15 + 16 + import { AppLink } from "@/components/layout/app-link"; 17 + 18 + interface Props { 19 + navigation: { 20 + label: string; 21 + href: string; 22 + segment: string | null; 23 + disabled?: boolean; 24 + }[]; 25 + } 26 + 27 + export function Menu({ navigation }: Props) { 28 + const selectedSegment = useSelectedLayoutSegment(); 29 + const [open, setOpen] = React.useState(false); 30 + const pathname = usePathname(); 31 + 32 + React.useEffect(() => { 33 + setOpen(false); 34 + }, [pathname]); 35 + 36 + return ( 37 + <Sheet open={open} onOpenChange={(value) => setOpen(value)}> 38 + <SheetTrigger asChild> 39 + <Button 40 + size="icon" 41 + variant="outline" 42 + className="rounded-full" 43 + aria-label="menu" 44 + > 45 + <MenuIcon className="h-6 w-6" /> 46 + </Button> 47 + </SheetTrigger> 48 + <SheetContent side="top" className="flex flex-col"> 49 + <SheetHeader> 50 + <SheetTitle className="ml-2 text-left">Menu</SheetTitle> 51 + </SheetHeader> 52 + <ul className="grid gap-1"> 53 + {navigation.map(({ href, label, segment, disabled }) => { 54 + const active = segment === selectedSegment; 55 + return ( 56 + <li key={href} className="w-full"> 57 + <AppLink {...{ href, label, active, disabled }} /> 58 + </li> 59 + ); 60 + })} 61 + </ul> 62 + </SheetContent> 63 + </Sheet> 64 + ); 65 + }
-33
apps/web/src/app/status-page/[domain]/_components/navbar.tsx
··· 1 - "use client"; 2 - 3 - import Link from "next/link"; 4 - import { useSelectedLayoutSegment } from "next/navigation"; 5 - 6 - import { Button } from "@openstatus/ui"; 7 - 8 - type Props = { 9 - navigation: { label: string; href: string; segment: string | null }[]; 10 - }; 11 - 12 - export function Navbar({ navigation }: Props) { 13 - const selectedSegment = useSelectedLayoutSegment(); 14 - 15 - return ( 16 - <ul className="flex items-center gap-2"> 17 - {navigation.map(({ label, href, segment }) => { 18 - const isActive = segment === selectedSegment; 19 - return ( 20 - <li key={segment}> 21 - <Button 22 - variant={isActive ? "secondary" : "ghost"} 23 - size="sm" 24 - asChild 25 - > 26 - <Link href={href}>{label}</Link> 27 - </Button> 28 - </li> 29 - ); 30 - })} 31 - </ul> 32 - ); 33 - }
+17 -22
apps/web/src/app/status-page/[domain]/incidents/[id]/page.tsx
··· 1 - import Link from "next/link"; 2 1 import { notFound } from "next/navigation"; 3 - import { ChevronLeft } from "lucide-react"; 4 2 5 - import { Button, Separator } from "@openstatus/ui"; 6 - 7 - import { Events } from "@/components/status-update/events"; 8 - import { Summary } from "@/components/status-update/summary"; 3 + import { Header } from "@/components/dashboard/header"; 4 + import { 5 + StatusReportDescription, 6 + StatusReportUpdates, 7 + } from "@/components/status-page/status-report"; 9 8 import { api } from "@/trpc/server"; 10 - import { setPrefixUrl } from "../../utils"; 11 9 import { CopyLinkButton } from "./_components/copy-link-button"; 12 10 13 11 export default async function IncidentPage({ ··· 27 25 ); 28 26 29 27 return ( 30 - <div className="grid gap-4 text-left"> 31 - <div> 32 - <Button variant="link" size="sm" className="px-0" asChild> 33 - <Link href={setPrefixUrl("/", params)}> 34 - <ChevronLeft className="h-4 w-4" /> Back 35 - </Link> 36 - </Button> 37 - </div> 38 - <div className="flex items-center gap-1"> 39 - <h3 className="text-xl font-semibold">{report.title}</h3> 40 - <CopyLinkButton /> 41 - </div> 42 - <Summary report={report} monitors={affectedMonitors} /> 43 - <Separator /> 44 - <Events statusReportUpdates={report.statusReportUpdates} /> 28 + <div className="grid gap-8 text-left"> 29 + <Header 30 + title={report.title} 31 + description={ 32 + <StatusReportDescription 33 + report={report} 34 + monitors={affectedMonitors} 35 + /> 36 + } 37 + actions={<CopyLinkButton />} 38 + /> 39 + <StatusReportUpdates report={report} /> 45 40 </div> 46 41 ); 47 42 }
+1 -1
apps/web/src/app/status-page/[domain]/incidents/page.tsx
··· 16 16 if (!page) return notFound(); 17 17 18 18 return ( 19 - <div className="grid gap-6"> 19 + <div className="grid gap-8"> 20 20 <Header 21 21 title={page.title} 22 22 description={page.description}
+13 -50
apps/web/src/app/status-page/[domain]/layout.tsx
··· 1 1 import type { Metadata } from "next"; 2 - import Image from "next/image"; 3 2 import { notFound } from "next/navigation"; 4 - 5 - import { allPlans } from "@openstatus/plans"; 6 3 7 4 import { 8 5 defaultMetadata, ··· 10 7 twitterMetadata, 11 8 } from "@/app/shared-metadata"; 12 9 import { Shell } from "@/components/dashboard/shell"; 13 - import { ThemeToggle } from "@/components/theme-toggle"; 14 10 import { api } from "@/trpc/server"; 15 - import { Navbar } from "./_components/navbar"; 16 - import { SubscribeButton } from "./_components/subscribe-button"; 11 + import { Footer } from "./_components/footer"; 12 + import { Header } from "./_components/header"; 17 13 import { setPrefixUrl } from "./utils"; 18 14 19 15 type Props = { ··· 26 22 if (!page) return notFound(); 27 23 28 24 const plan = page.workspacePlan; 29 - const isSubscribers = allPlans[plan].limits["status-subscribers"]; 30 - const isWhiteLabel = allPlans[plan].limits["white-label"]; 31 25 32 26 const navigation = [ 33 27 { ··· 40 34 segment: "incidents", 41 35 href: setPrefixUrl("/incidents", params), 42 36 }, 37 + { 38 + label: "Monitors", 39 + segment: "monitors", 40 + href: setPrefixUrl("/monitors", params), 41 + disabled: 42 + page.monitors.filter((monitor) => Boolean(monitor.public)).length === 0, 43 + }, 43 44 ]; 44 45 45 46 return ( 46 - <div className="flex min-h-screen w-full flex-col space-y-6 p-4 md:p-8"> 47 - <header className="mx-auto w-full max-w-xl"> 48 - <Shell className="mx-auto flex items-center justify-between gap-4 p-2 px-2 md:p-3"> 49 - <div className="relative sm:w-[100px]"> 50 - {page?.icon ? ( 51 - <div className="bg-muted border-border flex h-7 w-7 items-center justify-center overflow-hidden rounded-full border"> 52 - <Image 53 - height={28} 54 - width={28} 55 - src={page.icon} 56 - alt={page.title} 57 - className="object-cover" 58 - /> 59 - </div> 60 - ) : null} 61 - </div> 62 - <Navbar navigation={navigation} /> 63 - <div className="text-end sm:w-[100px]"> 64 - {isSubscribers ? <SubscribeButton slug={params.domain} /> : null} 65 - </div> 66 - </Shell> 67 - </header> 47 + <div className="mx-auto flex min-h-screen w-full max-w-3xl flex-col space-y-6 p-4 md:p-8"> 48 + <Header navigation={navigation} plan={plan} page={page} /> 68 49 <main className="flex h-full w-full flex-1 flex-col"> 69 - <Shell className="mx-auto h-full max-w-xl flex-1 px-4 py-4"> 70 - {children} 71 - </Shell> 50 + <Shell className="mx-auto h-full flex-1 px-4 py-4">{children}</Shell> 72 51 </main> 73 - <footer className="z-10 mx-auto flex w-full max-w-xl items-center justify-between"> 74 - <div /> 75 - {!isWhiteLabel ? ( 76 - <p className="text-muted-foreground text-center text-sm"> 77 - powered by{" "} 78 - <a 79 - href="https://www.openstatus.dev" 80 - target="_blank" 81 - rel="noreferrer" 82 - className="text-foreground underline underline-offset-4 hover:no-underline" 83 - > 84 - openstatus.dev 85 - </a> 86 - </p> 87 - ) : null} 88 - <ThemeToggle /> 89 - </footer> 52 + <Footer plan={plan} /> 90 53 </div> 91 54 ); 92 55 }
+126
apps/web/src/app/status-page/[domain]/monitors/[id]/page.tsx
··· 1 + import * as React from "react"; 2 + import { notFound } from "next/navigation"; 3 + import * as z from "zod"; 4 + 5 + import { flyRegions } from "@openstatus/db/src/schema"; 6 + import type { Region } from "@openstatus/tinybird"; 7 + import { OSTinybird } from "@openstatus/tinybird"; 8 + import { Separator } from "@openstatus/ui"; 9 + 10 + import { Header } from "@/components/dashboard/header"; 11 + import { CombinedChartWrapper } from "@/components/monitor-charts/combined-chart-wrapper"; 12 + import { ButtonReset } from "@/components/monitor-dashboard/button-reset"; 13 + import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 14 + import { Metrics } from "@/components/monitor-dashboard/metrics"; 15 + import { env } from "@/env"; 16 + import { 17 + getMinutesByInterval, 18 + intervals, 19 + quantiles, 20 + } from "@/lib/monitor/utils"; 21 + import { getPreferredSettings } from "@/lib/preferred-settings/server"; 22 + import { api } from "@/trpc/server"; 23 + 24 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 25 + 26 + const periods = ["1d", "7d"] as const; // satisfies Period[] 27 + 28 + const DEFAULT_QUANTILE = "p95"; 29 + const DEFAULT_INTERVAL = "30m"; 30 + const DEFAULT_PERIOD = "7d"; 31 + 32 + /** 33 + * allowed URL search params 34 + */ 35 + const searchParamsSchema = z.object({ 36 + statusCode: z.coerce.number().optional(), 37 + cronTimestamp: z.coerce.number().optional(), 38 + quantile: z.enum(quantiles).optional().default(DEFAULT_QUANTILE), 39 + interval: z.enum(intervals).optional().default(DEFAULT_INTERVAL), 40 + period: z.enum(periods).optional().default(DEFAULT_PERIOD), 41 + regions: z 42 + .string() 43 + .optional() 44 + .transform( 45 + (value) => 46 + value 47 + ?.trim() 48 + ?.split(",") 49 + .filter((i) => flyRegions.includes(i as Region)) ?? flyRegions, 50 + ), 51 + }); 52 + 53 + export default async function Page({ 54 + params, 55 + searchParams, 56 + }: { 57 + params: { domain: string; id: string }; 58 + searchParams: { [key: string]: string | string[] | undefined }; 59 + }) { 60 + const id = params.id; 61 + const search = searchParamsSchema.safeParse(searchParams); 62 + const preferredSettings = getPreferredSettings(); 63 + 64 + const monitor = await api.monitor.getPublicMonitorById.query({ 65 + id: Number(id), 66 + slug: params.domain, 67 + }); 68 + 69 + if (!monitor || !search.success) { 70 + return notFound(); 71 + } 72 + 73 + const { period, quantile, interval, regions } = search.data; 74 + 75 + // TODO: work it out easier 76 + const intervalMinutes = getMinutesByInterval(interval); 77 + const periodicityMinutes = getMinutesByInterval(monitor.periodicity); 78 + 79 + const isQuantileDisabled = intervalMinutes <= periodicityMinutes; 80 + const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 81 + 82 + const metrics = await tb.endpointMetrics(period)({ monitorId: id }); 83 + 84 + const data = await tb.endpointChart(period)({ 85 + monitorId: id, 86 + interval: minutes, 87 + }); 88 + 89 + const metricsByRegion = await tb.endpointMetricsByRegion(period)({ 90 + monitorId: id, 91 + }); 92 + 93 + if (!data || !metrics || !metricsByRegion) return null; 94 + 95 + const isDirty = 96 + period !== DEFAULT_PERIOD || 97 + quantile !== DEFAULT_QUANTILE || 98 + interval !== DEFAULT_INTERVAL || 99 + flyRegions.length !== regions.length; 100 + 101 + return ( 102 + <div className="relative flex w-full flex-col gap-6"> 103 + <Header 104 + title={monitor.name} 105 + description={monitor.description || monitor.url} 106 + /> 107 + <div className="flex items-center justify-between gap-2"> 108 + <DatePickerPreset defaultValue={period} values={periods} /> 109 + {isDirty ? <ButtonReset /> : null} 110 + </div> 111 + <Metrics metrics={metrics} period={period} /> 112 + <Separator className="my-8" /> 113 + <CombinedChartWrapper 114 + data={data} 115 + period={period} 116 + quantile={quantile} 117 + interval={interval} 118 + regions={regions as Region[]} // FIXME: not properly reseted after filtered 119 + monitor={monitor} 120 + isQuantileDisabled={isQuantileDisabled} 121 + metricsByRegion={metricsByRegion} 122 + preferredSettings={preferredSettings} 123 + /> 124 + </div> 125 + ); 126 + }
+118
apps/web/src/app/status-page/[domain]/monitors/page.tsx
··· 1 + import Link from "next/link"; 2 + import { notFound } from "next/navigation"; 3 + import { ChevronRight } from "lucide-react"; 4 + import { z } from "zod"; 5 + 6 + import { OSTinybird } from "@openstatus/tinybird"; 7 + import { Button } from "@openstatus/ui"; 8 + 9 + import { Header } from "@/components/dashboard/header"; 10 + import { SimpleChart } from "@/components/monitor-charts/simple-chart"; 11 + import { groupDataByTimestamp } from "@/components/monitor-charts/utils"; 12 + import { env } from "@/env"; 13 + import { quantiles } from "@/lib/monitor/utils"; 14 + import { api } from "@/trpc/server"; 15 + 16 + // Add loading page 17 + 18 + /** 19 + * allowed URL search params 20 + */ 21 + const searchParamsSchema = z.object({ 22 + quantile: z.enum(quantiles).optional().default("p95"), 23 + period: z.enum(["7d"]).optional().default("7d"), 24 + }); 25 + 26 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 27 + 28 + export default async function Page({ 29 + params, 30 + searchParams, 31 + }: { 32 + params: { domain: string }; 33 + searchParams: { [key: string]: string | string[] | undefined }; 34 + }) { 35 + const page = await api.page.getPageBySlug.query({ slug: params.domain }); 36 + const search = searchParamsSchema.safeParse(searchParams); 37 + if (!page || !search.success) notFound(); 38 + 39 + const { quantile, period } = search.data; 40 + 41 + // filter monitor by public or not 42 + 43 + const publicMonitors = page.monitors.filter((monitor) => monitor.public); 44 + 45 + const monitorsWithData = 46 + publicMonitors.length > 0 47 + ? await Promise.all( 48 + publicMonitors?.map(async (monitor) => { 49 + const data = await tb.endpointChartAllRegions(period)({ 50 + monitorId: String(monitor.id), 51 + }); 52 + 53 + return { monitor, data }; 54 + }), 55 + ) 56 + : undefined; 57 + 58 + return ( 59 + <div className="grid gap-8"> 60 + <Header 61 + title={page.title} 62 + description={page.description} 63 + className="text-left" 64 + /> 65 + {monitorsWithData ? ( 66 + <div className="grid gap-6"> 67 + <p className="text-muted-foreground"> 68 + Response time over the{" "} 69 + <span className="text-foreground font-medium">last {period}</span>{" "} 70 + across{" "} 71 + <span className="text-foreground font-medium"> 72 + all selected regions 73 + </span>{" "} 74 + within a{" "} 75 + <span className="text-foreground font-medium">p95 quantile</span>. 76 + </p> 77 + <ul className="grid gap-6"> 78 + {monitorsWithData?.map(({ monitor, data }) => { 79 + const group = 80 + data && 81 + groupDataByTimestamp( 82 + data.map((data) => ({ ...data, region: "ams" })), 83 + period, 84 + quantile, 85 + ); 86 + return ( 87 + <li key={monitor.id} className="grid gap-2"> 88 + <div className="flex w-full min-w-0 items-center justify-between gap-3"> 89 + <div className="w-full min-w-0"> 90 + <p className="text-sm font-semibold">{monitor.name}</p> 91 + <p className="text-muted-foreground truncate text-sm"> 92 + {monitor.url} 93 + </p> 94 + </div> 95 + <Button variant="link" size="sm" asChild> 96 + <Link href={`./monitors/${monitor.id}`}> 97 + Details <ChevronRight className="h-4 w-4" /> 98 + </Link> 99 + </Button> 100 + </div> 101 + {group ? ( 102 + <SimpleChart data={group.data} region="ams" /> 103 + ) : ( 104 + <p>missing data</p> 105 + )} 106 + </li> 107 + ); 108 + })} 109 + </ul> 110 + </div> 111 + ) : ( 112 + <p className="text-muted-foreground text-center text-sm font-light"> 113 + No public monitor. 114 + </p> 115 + )} 116 + </div> 117 + ); 118 + }
+25 -40
apps/web/src/app/status-page/[domain]/page.tsx
··· 1 - import Link from "next/link"; 2 1 import { notFound } from "next/navigation"; 2 + import { subDays } from "date-fns"; 3 3 4 - import { Button } from "@openstatus/ui"; 4 + import { Separator } from "@openstatus/ui"; 5 5 6 - import { EmptyState } from "@/components/dashboard/empty-state"; 7 6 import { Header } from "@/components/dashboard/header"; 8 7 import { MonitorList } from "@/components/status-page/monitor-list"; 9 8 import { StatusCheck } from "@/components/status-page/status-check"; 10 9 import { StatusReportList } from "@/components/status-page/status-report-list"; 11 10 import { api } from "@/trpc/server"; 12 11 13 - const url = 14 - process.env.NODE_ENV === "development" 15 - ? "http://localhost:3000" 16 - : "https://www.openstatus.dev"; 17 - 18 12 type Props = { 19 13 params: { domain: string }; 20 14 searchParams: { [key: string]: string | string[] | undefined }; ··· 25 19 export default async function Page({ params }: Props) { 26 20 const page = await api.page.getPageBySlug.query({ slug: params.domain }); 27 21 if (!page) return notFound(); 28 - const isEmptyState = !( 29 - Boolean(page.monitors.length) || Boolean(page.statusReports.length) 30 - ); 31 22 32 23 return ( 33 - <div className="mx-auto flex w-full flex-col gap-6"> 24 + <div className="mx-auto flex w-full flex-col gap-8"> 34 25 <Header 35 26 title={page.title} 36 27 description={page.description} 37 28 className="text-left" 38 29 /> 39 - {isEmptyState ? ( 40 - <EmptyState 41 - icon="activity" 42 - title="Missing Monitors" 43 - description="Fill your status page with monitors." 44 - action={ 45 - <Button asChild> 46 - <Link href={`${url}/app`}>Go to Dashboard</Link> 47 - </Button> 48 - } 30 + <StatusCheck 31 + statusReports={page.statusReports} 32 + incidents={page.incidents} 33 + /> 34 + <MonitorList 35 + monitors={page.monitors} 36 + statusReports={page.statusReports} 37 + incidents={page.incidents} 38 + /> 39 + <Separator /> 40 + <div className="grid gap-6"> 41 + <div> 42 + <h2 className="text-xl font-semibold">Latest Incidents</h2> 43 + <p className="text-muted-foreground text-sm"> 44 + Incidents of the last 7 days or that have not been resolved yet. 45 + </p> 46 + </div> 47 + <StatusReportList 48 + statusReports={page.statusReports} 49 + monitors={page.monitors} 50 + filter={{ date: subDays(Date.now(), 7), open: true }} 49 51 /> 50 - ) : ( 51 - <> 52 - <StatusCheck 53 - statusReports={page.statusReports} 54 - incidents={page.incidents} 55 - /> 56 - <MonitorList 57 - monitors={page.monitors} 58 - statusReports={page.statusReports} 59 - incidents={page.incidents} 60 - /> 61 - <StatusReportList 62 - statusReports={page.statusReports} 63 - monitors={page.monitors} 64 - context="latest" 65 - /> 66 - </> 67 - )} 52 + </div> 68 53 </div> 69 54 ); 70 55 }
+2 -1
apps/web/src/components/forms/monitor/form.tsx
··· 82 82 notifications: defaultValues?.notifications ?? [], 83 83 pages: defaultValues?.pages ?? [], 84 84 tags: defaultValues?.tags ?? [], 85 + public: defaultValues?.public ?? false, 85 86 statusAssertions: _assertions.filter((a) => a.type === "status") as any, // TS considers a.type === "header" 86 87 headerAssertions: _assertions.filter((a) => a.type === "header") as any, // TS considers a.type === "status" 87 88 }, ··· 286 287 </TabsContent> 287 288 {defaultValues?.id ? ( 288 289 <TabsContent value="danger"> 289 - <SectionDanger monitorId={defaultValues.id} /> 290 + <SectionDanger monitorId={defaultValues.id} {...{ form }} /> 290 291 </TabsContent> 291 292 ) : null} 292 293 </Tabs>
+73 -32
apps/web/src/components/forms/monitor/section-danger.tsx
··· 2 2 3 3 import React from "react"; 4 4 import { useRouter } from "next/navigation"; 5 + import type { UseFormReturn } from "react-hook-form"; 5 6 7 + import type { InsertMonitor } from "@openstatus/db/src/schema"; 6 8 import { 7 9 AlertDialog, 8 10 AlertDialogAction, ··· 14 16 AlertDialogTitle, 15 17 AlertDialogTrigger, 16 18 Button, 19 + Checkbox, 20 + FormControl, 21 + FormDescription, 22 + FormField, 23 + FormItem, 24 + FormLabel, 17 25 } from "@openstatus/ui"; 18 26 19 27 import { LoadingAnimation } from "@/components/loading-animation"; ··· 23 31 24 32 interface Props { 25 33 monitorId: number; 34 + form: UseFormReturn<InsertMonitor>; 26 35 } 27 36 28 - export function SectionDanger({ monitorId }: Props) { 37 + export function SectionDanger({ monitorId, form }: Props) { 29 38 const router = useRouter(); 30 39 const [open, setOpen] = React.useState(false); 31 40 const [isPending, startTransition] = React.useTransition(); ··· 47 56 <div className="grid w-full gap-4"> 48 57 <SectionHeader 49 58 title="Danger Zone" 50 - description="This action cannot be undone. This will permanently delete the monitor." 59 + description="Be aware of the changes you are about to make." 51 60 /> 52 - <div> 53 - <AlertDialog open={open} onOpenChange={setOpen}> 54 - <AlertDialogTrigger asChild> 55 - <Button variant="destructive" className="w-full sm:w-auto"> 56 - Delete 57 - </Button> 58 - </AlertDialogTrigger> 59 - <AlertDialogContent> 60 - <AlertDialogHeader> 61 - <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 62 - <AlertDialogDescription> 63 - This action cannot be undone. This will permanently delete the 64 - monitor. 65 - </AlertDialogDescription> 66 - </AlertDialogHeader> 67 - <AlertDialogFooter> 68 - <AlertDialogCancel>Cancel</AlertDialogCancel> 69 - <AlertDialogAction 70 - onClick={(e) => { 71 - e.preventDefault(); 72 - onDelete(); 73 - }} 74 - disabled={isPending} 75 - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 76 - > 77 - {!isPending ? "Delete" : <LoadingAnimation />} 78 - </AlertDialogAction> 79 - </AlertDialogFooter> 80 - </AlertDialogContent> 81 - </AlertDialog> 61 + <div className="grid gap-4 sm:grid-cols-3"> 62 + <FormField 63 + control={form.control} 64 + name="public" 65 + render={({ field }) => ( 66 + <FormItem className="flex flex-row items-start space-x-3 space-y-0 sm:col-span-2"> 67 + <FormControl> 68 + <Checkbox 69 + checked={field.value ?? false} 70 + onCheckedChange={field.onChange} 71 + /> 72 + </FormControl> 73 + <div className="space-y-1 leading-none"> 74 + <FormLabel>Allow public monitor</FormLabel> 75 + <FormDescription> 76 + Change monitor visibility. When checked, the monitor stats 77 + from the overview page will be public. You will be able to 78 + share it via a connected status page or{" "} 79 + <code className="underline underline-offset-4"> 80 + openstatus.dev/public/monitors/{form.getValues("id")} 81 + </code> 82 + . 83 + </FormDescription> 84 + </div> 85 + </FormItem> 86 + )} 87 + /> 88 + <div className="col-start-1 flex flex-col items-center gap-4 sm:col-span-2 sm:flex-row"> 89 + <AlertDialog open={open} onOpenChange={setOpen}> 90 + <AlertDialogTrigger asChild> 91 + <Button variant="destructive" className="w-full sm:w-auto"> 92 + Delete 93 + </Button> 94 + </AlertDialogTrigger> 95 + <AlertDialogContent> 96 + <AlertDialogHeader> 97 + <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 98 + <AlertDialogDescription> 99 + This action cannot be undone. This will permanently delete the 100 + monitor. 101 + </AlertDialogDescription> 102 + </AlertDialogHeader> 103 + <AlertDialogFooter> 104 + <AlertDialogCancel>Cancel</AlertDialogCancel> 105 + <AlertDialogAction 106 + onClick={(e) => { 107 + e.preventDefault(); 108 + onDelete(); 109 + }} 110 + disabled={isPending} 111 + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 112 + > 113 + {!isPending ? "Delete" : <LoadingAnimation />} 114 + </AlertDialogAction> 115 + </AlertDialogFooter> 116 + </AlertDialogContent> 117 + </AlertDialog> 118 + <FormDescription className="order-1 text-red-500 sm:order-2"> 119 + This action cannot be undone. This will permanently delete the 120 + monitor. 121 + </FormDescription> 122 + </div> 82 123 </div> 83 124 </div> 84 125 );
+18
apps/web/src/components/layout/public-header.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + import { BrandName } from "./brand-name"; 3 + import { LoginButton } from "./login-button"; 4 + 5 + interface Props { 6 + className?: string; 7 + } 8 + 9 + export function PublicHeader({ className }: Props) { 10 + return ( 11 + <header 12 + className={cn("flex w-full items-center justify-between", className)} 13 + > 14 + <BrandName /> 15 + <LoginButton /> 16 + </header> 17 + ); 18 + }
+18
apps/web/src/components/layout/public-layout.tsx
··· 1 + import { ClerkProvider } from "@clerk/nextjs"; 2 + 3 + import { MarketingFooter } from "./marketing-footer"; 4 + import { PublicHeader } from "./public-header"; 5 + 6 + export function PublicLayout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <ClerkProvider> 9 + <main className="flex min-h-screen w-full flex-col items-center justify-center gap-8 p-4 md:p-8"> 10 + <PublicHeader className="mx-auto w-full max-w-4xl" /> 11 + <div className="mx-auto flex w-full max-w-4xl flex-1 flex-col items-start justify-center"> 12 + {children} 13 + </div> 14 + <MarketingFooter className="mx-auto w-full max-w-4xl" /> 15 + </main> 16 + </ClerkProvider> 17 + ); 18 + }
-24
apps/web/src/components/marketing/bottom-cta.tsx
··· 1 - import Link from "next/link"; 2 - 3 - import { Button } from "@openstatus/ui"; 4 - 5 - export function BottomCTA() { 6 - return ( 7 - <div className="my-8 flex flex-col items-center justify-between gap-6"> 8 - <p className="text-muted-foreground max-w-lg text-center text-lg"> 9 - Learn over time how your services are performing, and inform your users 10 - when there are issues. 11 - </p> 12 - <div className="flex gap-2"> 13 - <Button className="rounded-full" asChild> 14 - <Link href="/app/sign-up">Start for Free</Link> 15 - </Button> 16 - <Button className="rounded-full" variant="outline" asChild> 17 - <Link href="/cal" target="_blank"> 18 - Schedule a Demo 19 - </Link> 20 - </Button> 21 - </div> 22 - </div> 23 - ); 24 - }
+59
apps/web/src/components/marketing/in-between-cta.tsx
··· 1 + import type { ReactNode } from "react"; 2 + import Link from "next/link"; 3 + 4 + import { Button } from "@openstatus/ui"; 5 + 6 + interface InBetweenCTAProps { 7 + description: string; 8 + actions: Record< 9 + "primary" | "secondary", 10 + { label: string; href: string; target?: string } 11 + >; 12 + } 13 + 14 + export function InBetweenCTA({ description, actions }: InBetweenCTAProps) { 15 + const { primary, secondary } = actions; 16 + return ( 17 + <div className="my-8 flex flex-col items-center justify-between gap-6"> 18 + <p className="text-muted-foreground max-w-lg text-center text-lg"> 19 + {description} 20 + </p> 21 + <div className="flex gap-2"> 22 + <Button className="rounded-full" asChild> 23 + <Link href={primary.href} target={primary.target}> 24 + {primary.label} 25 + </Link> 26 + </Button> 27 + <Button className="rounded-full" variant="outline" asChild> 28 + <Link href={secondary.href} target={secondary.target}> 29 + {secondary.label} 30 + </Link> 31 + </Button> 32 + </div> 33 + </div> 34 + ); 35 + } 36 + 37 + export function MiddleCTA() { 38 + return ( 39 + <InBetweenCTA 40 + description="Sick of booking a demo to know more? Test your endpoint or check our public dashboard right away." 41 + actions={{ 42 + primary: { label: "Public Dashboard", href: "/public/monitors/1" }, 43 + secondary: { label: "Speed Checker", href: "/play/check" }, 44 + }} 45 + /> 46 + ); 47 + } 48 + 49 + export function BottomCTA() { 50 + return ( 51 + <InBetweenCTA 52 + description="Learn over time how your services are performing, and inform your users when there are issues." 53 + actions={{ 54 + primary: { label: "Start for Free", href: "/app/sign-up" }, 55 + secondary: { label: "Schedule a Demo", href: "/cal", target: "_blank" }, 56 + }} 57 + /> 58 + ); 59 + }
+1 -1
apps/web/src/components/marketing/monitor/card.tsx
··· 26 26 <Globe /> 27 27 <CardFeatureContainer> 28 28 {features?.map((feature, i) => <CardFeature key={i} {...feature} />)} 29 - <div className="text-center"> 29 + <div className="order-first text-center md:order-none"> 30 30 <Button asChild variant="outline" className="rounded-full"> 31 31 <Link href="/play/checker">Playground</Link> 32 32 </Button>
+2 -2
apps/web/src/components/monitor-charts/combined-chart-wrapper.tsx
··· 3 3 import { useMemo } from "react"; 4 4 import { LineChart } from "lucide-react"; 5 5 6 - import type { Monitor } from "@openstatus/db/src/schema"; 6 + import type { Monitor, PublicMonitor } from "@openstatus/db/src/schema"; 7 7 import type { 8 8 Region, 9 9 ResponseGraph, ··· 37 37 quantile: Quantile; 38 38 interval: Interval; 39 39 regions: Region[]; 40 - monitor: Monitor; 40 + monitor: Monitor | PublicMonitor; 41 41 isQuantileDisabled: boolean; 42 42 metricsByRegion: ResponseTimeMetricsByRegion[]; 43 43 preferredSettings: PreferredSettings;
+1 -1
apps/web/src/components/monitor-charts/region-table.tsx
··· 27 27 regions, 28 28 data, 29 29 metricsByRegion, 30 - caption = "A list of your regions.", 30 + caption = "A list of all the selected regions.", 31 31 }: RegionTableProps) { 32 32 // console.log(JSON.stringify({ regions, data, metricsByRegion }, null, 2)); 33 33 return (
+1 -1
apps/web/src/components/monitor-charts/simple-chart.tsx
··· 30 30 valueFormatter={dataFormatter} 31 31 curveType="monotone" 32 32 autoMinValue 33 + showAnimation 33 34 noDataText="" 34 35 showXAxis={false} 35 36 showYAxis={false} ··· 38 39 customTooltip={customTooltip} 39 40 // FEATURE: it would be nice, if on click, the tooltip would be open 40 41 // onValueChange={(v) => setValue(v)} 41 - showAnimation={true} 42 42 /> 43 43 ); 44 44 }
+8 -4
apps/web/src/components/monitor-dashboard/metrics.tsx
··· 18 18 export function Metrics({ 19 19 metrics, 20 20 period, 21 + showErrorLink, 21 22 }: { 22 23 metrics?: ResponseTimeMetrics[]; 23 24 period: Period; 25 + showErrorLink?: boolean; 24 26 }) { 25 27 if (!metrics) return null; 26 28 ··· 41 43 : undefined; 42 44 43 45 return ( 44 - <div className="grid gap-6"> 45 - <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 md:gap-6"> 46 + <div className="@container grid gap-6"> 47 + <div className="@xl:grid-cols-4 @3xl:grid-cols-5 @3xl:gap-6 grid grid-cols-2 gap-4"> 46 48 <MetricsCard 47 49 title="uptime" 48 50 value={uptime * 100} ··· 74 76 <MetricsCard title="total pings" value={current.count} suffix="#" /> 75 77 </div> 76 78 <div className="grid gap-4"> 77 - <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 md:gap-6"> 79 + <div className="@xl:grid-cols-4 @3xl:grid-cols-5 @3xl:gap-6 grid grid-cols-2 gap-4"> 78 80 {metricsOrder.map((key) => { 79 81 const value = current[key]; 80 82 const lastValue = last[key]; ··· 100 102 over all the regions and compared with the previous period. 101 103 </p> 102 104 {/* restricted to max 3d as we only support it in the list -> TODO: add more periods */} 103 - {failures > 0 && ["1h", "1d", "3d", "7d"].includes(period) ? ( 105 + {showErrorLink && 106 + failures > 0 && 107 + ["1h", "1d", "3d", "7d"].includes(period) ? ( 104 108 <p className="text-destructive text-xs"> 105 109 The monitor had {failures} failed ping(s). See more in the{" "} 106 110 <Link
+22
apps/web/src/components/status-page/process-message.tsx
··· 1 + import type { AnchorHTMLAttributes } from "react"; 2 + import { createElement, Fragment } from "react"; 3 + import rehypeReact from "rehype-react"; 4 + import remarkParse from "remark-parse"; 5 + import remarkRehype from "remark-rehype"; 6 + import { unified } from "unified"; 7 + 8 + export function ProcessMessage({ value }: { value: string }) { 9 + return unified() 10 + .use(remarkParse) 11 + .use(remarkRehype) 12 + .use(rehypeReact, { 13 + createElement, 14 + Fragment, 15 + components: { 16 + a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => { 17 + return <a target="_blank" rel="noreferrer" {...props} />; 18 + }, 19 + }, 20 + }) 21 + .processSync(value).result; 22 + }
+1 -1
apps/web/src/components/status-page/status-check.tsx
··· 22 22 return ( 23 23 <div className="flex flex-col items-center gap-3"> 24 24 <div className="flex items-center gap-3"> 25 - <p className="text-lg font-semibold">{details.long}</p> 25 + <h2 className="text-xl font-semibold">{details.long}</h2> 26 26 <span className={cn("rounded-full border p-1.5", className)}> 27 27 <StatusIcon variant={details.variant} /> 28 28 </span>
+37 -72
apps/web/src/components/status-page/status-report-list.tsx
··· 1 1 "use client"; 2 2 3 - import Link from "next/link"; 4 - import { useParams } from "next/navigation"; 5 - import { ChevronRight } from "lucide-react"; 6 - 7 3 import type { 8 4 PublicMonitor, 9 5 StatusReportWithUpdates, 10 6 } from "@openstatus/db/src/schema"; 11 - import { Button, Separator } from "@openstatus/ui"; 7 + import { Separator } from "@openstatus/ui"; 12 8 13 - import { setPrefixUrl } from "@/app/status-page/[domain]/utils"; 14 9 import { notEmpty } from "@/lib/utils"; 15 - import { Events } from "../status-update/events"; 16 - import { Summary } from "../status-update/summary"; 17 - 18 - // TODO: change layout - it is too packed with data rn 10 + import { StatusReport } from "./status-report"; 19 11 20 12 export const StatusReportList = ({ 21 13 statusReports, 22 14 monitors, 23 - context = "all", 15 + filter, 24 16 }: { 25 17 statusReports: StatusReportWithUpdates[]; 26 18 monitors: PublicMonitor[]; 27 - context?: "all" | "latest"; // latest 7 days 19 + filter?: { date: Date; open?: boolean }; 28 20 }) => { 29 - const params = useParams<{ domain: string }>(); 30 - const lastWeek = Date.now() - 1000 * 60 * 60 * 24 * 7; 31 - 32 - function getLastWeekOrOpenIncidents() { 33 - return statusReports.filter((incident) => { 34 - const hasLastWeekReports = incident.statusReportUpdates.some( 35 - (update) => update.date.getTime() > lastWeek, 36 - ); 37 - const hasOpenIncident = ["identified", "investigating"].includes( 38 - incident.status, 21 + function getFilteredReports() { 22 + if (!filter?.date) return statusReports; 23 + return statusReports.filter((report) => { 24 + if (filter.open && report.status !== "resolved") return true; 25 + return report.statusReportUpdates.some( 26 + (update) => update.date.getTime() > filter?.date?.getTime(), 39 27 ); 40 - 41 - return hasLastWeekReports || hasOpenIncident; 42 28 }); 43 29 } 44 30 45 - const reports = 46 - context === "all" ? statusReports : getLastWeekOrOpenIncidents(); 47 - 48 - reports.sort((a, b) => { 31 + const reports = getFilteredReports().sort((a, b) => { 49 32 if (a.updatedAt == undefined) return 1; 50 33 if (b.updatedAt == undefined) return -1; 51 34 return b.updatedAt.getTime() - a.updatedAt.getTime(); ··· 54 37 return ( 55 38 <> 56 39 {reports?.length > 0 ? ( 57 - <div className="grid gap-3"> 58 - <p className="text-muted-foreground text-sm font-light"> 59 - {context === "all" ? "All incidents" : "Latest incidents"} 60 - </p> 61 - <div className="grid gap-8"> 62 - {reports.map((report) => { 63 - const affectedMonitors = report.monitorsToStatusReports 64 - .map(({ monitorId }) => { 65 - const monitor = monitors.find(({ id }) => monitorId === id); 66 - return monitor || undefined; 67 - }) 68 - .filter(notEmpty); 69 - return ( 70 - <div key={report.id} className="group grid gap-4 text-left"> 71 - <div className="flex items-center gap-1"> 72 - <h3 className="text-xl font-semibold">{report.title}</h3> 73 - <Button 74 - variant="ghost" 75 - size="icon" 76 - className="invisible h-7 w-7 group-hover:visible" 77 - asChild 78 - > 79 - <Link 80 - href={setPrefixUrl(`/incidents/${report.id}`, params)} 81 - > 82 - <ChevronRight className="h-4 w-4" /> 83 - </Link> 84 - </Button> 85 - </div> 86 - <Summary report={report} monitors={affectedMonitors} /> 87 - <Separator /> 88 - <Events 89 - statusReportUpdates={report.statusReportUpdates} 90 - collabsible 91 - /> 92 - </div> 93 - ); 94 - })} 95 - </div> 40 + <div className="grid gap-8"> 41 + {reports.map((report, i) => { 42 + const affectedMonitors = report.monitorsToStatusReports 43 + .map(({ monitorId }) => { 44 + const monitor = monitors.find(({ id }) => monitorId === id); 45 + return monitor || undefined; 46 + }) 47 + .filter(notEmpty); 48 + const isLast = reports.length - 1 === i; 49 + 50 + return ( 51 + <div key={report.id} className="grid gap-6"> 52 + <StatusReport monitors={affectedMonitors} report={report} /> 53 + {!isLast ? <Separator /> : null} 54 + </div> 55 + ); 56 + })} 96 57 </div> 97 58 ) : ( 98 - <p className="text-muted-foreground text-center text-sm font-light"> 99 - {context === "all" 100 - ? "No incidents." 101 - : "No incidents in the last week."} 102 - </p> 59 + <EmptyState /> 103 60 )} 104 61 </> 105 62 ); 106 63 }; 64 + 65 + function EmptyState() { 66 + return ( 67 + <p className="text-muted-foreground text-center text-sm font-light"> 68 + No incident reported. 69 + </p> 70 + ); 71 + }
+118
apps/web/src/components/status-page/status-report.tsx
··· 1 + "use client"; 2 + 3 + import { Fragment } from "react"; 4 + import Link from "next/link"; 5 + import { useParams } from "next/navigation"; 6 + import { format } from "date-fns"; 7 + import { ChevronRight } from "lucide-react"; 8 + 9 + import type { 10 + PublicMonitor, 11 + StatusReportWithUpdates, 12 + } from "@openstatus/db/src/schema"; 13 + import { Badge, Button } from "@openstatus/ui"; 14 + 15 + import { setPrefixUrl } from "@/app/status-page/[domain]/utils"; 16 + import { StatusBadge } from "../status-update/status-badge"; 17 + import { ProcessMessage } from "./process-message"; 18 + 19 + function StatusReport({ 20 + report, 21 + monitors, 22 + }: { 23 + report: StatusReportWithUpdates; 24 + monitors: PublicMonitor[]; 25 + }) { 26 + return ( 27 + <div className="group grid gap-4"> 28 + <div className="grid gap-1"> 29 + <StatusReportHeader {...{ report }} /> 30 + <StatusReportDescription {...{ report, monitors }} /> 31 + </div> 32 + <StatusReportUpdates {...{ report }} /> 33 + </div> 34 + ); 35 + } 36 + 37 + function StatusReportHeader({ report }: { report: StatusReportWithUpdates }) { 38 + const params = useParams<{ domain: string }>(); 39 + return ( 40 + <div className="flex items-center gap-2"> 41 + <h3 className="text-2xl font-semibold">{report.title}</h3> 42 + <Button 43 + variant="ghost" 44 + size="icon" 45 + className="text-muted-foreground/50 group-hover:text-foreground" 46 + asChild 47 + > 48 + <Link href={setPrefixUrl(`/incidents/${report.id}`, params)}> 49 + <ChevronRight className="h-4 w-4" /> 50 + </Link> 51 + </Button> 52 + </div> 53 + ); 54 + } 55 + 56 + function StatusReportDescription({ 57 + report, 58 + monitors, 59 + }: { 60 + report: StatusReportWithUpdates; 61 + monitors: PublicMonitor[]; 62 + }) { 63 + const firstReport = 64 + report.statusReportUpdates[report.statusReportUpdates.length - 1]; 65 + return ( 66 + <div className="flex flex-wrap items-center gap-2"> 67 + <p className="text-muted-foreground"> 68 + {format(firstReport.date || new Date(), "LLL dd, y HH:mm")} 69 + </p> 70 + <span className="text-muted-foreground/50 text-xs">•</span> 71 + <StatusBadge status={report.status} /> 72 + {monitors.length > 0 ? ( 73 + <> 74 + <span className="text-muted-foreground/50 text-xs">•</span> 75 + {monitors.map((monitor) => ( 76 + <Badge key={monitor.id} variant="secondary"> 77 + {monitor.name} 78 + </Badge> 79 + ))} 80 + </> 81 + ) : null} 82 + </div> 83 + ); 84 + } 85 + 86 + // reports are already `orderBy: desc(report.date)` within the query itself 87 + function StatusReportUpdates({ report }: { report: StatusReportWithUpdates }) { 88 + return ( 89 + <div className="grid gap-4 md:grid-cols-4"> 90 + {report.statusReportUpdates.map((update) => { 91 + return ( 92 + <Fragment key={update.id}> 93 + <div className="flex items-center gap-2 md:col-span-1 md:flex-col md:items-start md:gap-1"> 94 + <p className="font-medium capitalize">{update.status}</p> 95 + <p className="text-muted-foreground font-mono text-sm md:text-xs"> 96 + {format(update.date, "LLL dd, y HH:mm")} 97 + </p> 98 + </div> 99 + <div className="prose dark:prose-invert md:col-span-3"> 100 + <ProcessMessage value={update.message} /> 101 + </div> 102 + </Fragment> 103 + ); 104 + })} 105 + </div> 106 + ); 107 + } 108 + 109 + StatusReport.Header = StatusReportHeader; 110 + StatusReport.Description = StatusReportDescription; 111 + StatusReport.Updates = StatusReportUpdates; 112 + 113 + export { 114 + StatusReport, 115 + StatusReportDescription, 116 + StatusReportHeader, 117 + StatusReportUpdates, 118 + };
+10 -15
apps/web/src/components/status-update/status-badge.tsx
··· 1 + import type { StatusReport } from "@openstatus/db/src/schema"; 1 2 import { Badge } from "@openstatus/ui"; 2 3 3 4 import { statusDict } from "@/data/incidents-dictionary"; 4 5 import { cn } from "@/lib/utils"; 5 6 import { Icons } from "../icons"; 6 7 8 + const variant = { 9 + investigating: "border-rose-500/20 bg-rose-500/10 text-rose-500", 10 + identified: "border-amber-500/20 bg-amber-500/10 text-amber-500", 11 + monitoring: "border-blue-500/20 bg-blue-500/10 text-blue-500", 12 + resolved: "border-green-500/20 bg-green-500/10 text-green-500", 13 + } satisfies Record<StatusReport["status"], string>; 14 + 7 15 export function StatusBadge({ 8 16 status, 9 17 className, 10 18 }: { 11 - status: keyof typeof statusDict; 19 + status: StatusReport["status"]; 12 20 className?: string; 13 21 }) { 14 22 const { label, icon } = statusDict[status]; ··· 16 24 return ( 17 25 <Badge 18 26 variant="outline" 19 - className={cn( 20 - "font-normal", 21 - { 22 - "border-rose-500/20 bg-rose-500/10 text-rose-500": 23 - status === "investigating", 24 - "border-amber-500/20 bg-amber-500/10 text-amber-500": 25 - status === "identified", 26 - "border-blue-500/20 bg-blue-500/10 text-blue-500": 27 - status === "monitoring", 28 - "border-green-500/20 bg-green-500/10 text-green-500": 29 - status === "resolved", 30 - }, 31 - className, 32 - )} 27 + className={cn("font-normal", variant[status], className)} 33 28 > 34 29 <Icon className="mr-1 h-3 w-3" /> 35 30 {label}
+2 -6
apps/web/src/components/status-update/summary.tsx
··· 16 16 report: StatusReport & { statusReportUpdates: StatusReportUpdate[] }; 17 17 monitors: Pick<Monitor, "name">[]; 18 18 }) { 19 - const sortedStatusReportUpdates = report.statusReportUpdates.sort( 20 - (a, b) => a.date.getTime() - b.date.getTime(), 21 - ); 22 - 23 - const firstUpdate = sortedStatusReportUpdates?.[0]; 19 + const firstUpdate = report.statusReportUpdates?.[0]; 24 20 const lastUpdate = 25 - sortedStatusReportUpdates?.[sortedStatusReportUpdates.length - 1]; 21 + report.statusReportUpdates?.[report.statusReportUpdates.length - 1]; 26 22 27 23 return ( 28 24 <div className="grid grid-cols-5 gap-3 text-sm">
+1 -1
apps/web/src/config/features.ts
··· 37 37 icon: "globe", 38 38 catchline: "Latency Monitoring.", 39 39 description: 40 - "Monitor the latency of your endpoints from all over the world. We currently support all the continents.", 40 + "Monitor the latency of your endpoints from all over the world. We support all the continents.", 41 41 }, 42 42 { 43 43 icon: "play",
+20
apps/web/src/content/changelog/public-monitors.mdx
··· 1 + --- 2 + title: Public Monitors 3 + description: Make your monitors public and share more metrics with your users. 4 + image: /assets/changelog/public-monitors.png 5 + publishedAt: 2024-04-14 6 + --- 7 + 8 + You can now change the monitors visibility to public. This will allow you to 9 + share the monitor's metrics _(the overview page)_ with your users. The period is 10 + restricted to **1d** and **7d** for now. 11 + 12 + The monitor can be accessed either by the **public URL** or/and by embedding the 13 + monitor within a **status page**. 14 + 15 + - Public URL: 16 + [openstatus.dev/public/monitors/1](https://openstatus.dev/public/monitors/1) 17 + - Status Page URL: 18 + [status.openstatus.dev/monitors/1](https://status.openstatus.dev/monitors/1) 19 + 20 + You can enable public mode from the monitor _Danger_ section setting.
+1
apps/web/src/middleware.ts
··· 87 87 "/incidents", // used when trying subdomain slug via status.documenso.com/incidents 88 88 "/incidents/(.*)", // used when trying subdomain slug via status.documenso.com/incidents/123 89 89 "/verify/(.*)", // used when trying subdomain slug via status.documenso.com/incidents 90 + "/public/(.*)", 90 91 ], 91 92 ignoredRoutes: ["/api/og", "/discord", "/github", "/status-page/(.*)"], // FIXME: we should check the `publicRoutes` 92 93 beforeAuth: before,
+1
apps/web/tailwind.config.ts
··· 216 216 /* */ 217 217 plugins: [ 218 218 require("tailwindcss-animate"), 219 + require("@tailwindcss/container-queries"), 219 220 require("@tailwindcss/typography"), 220 221 require("@headlessui/tailwindcss"), 221 222 ],
+31
packages/api/src/router/monitor.ts
··· 21 21 selectMonitorSchema, 22 22 selectMonitorTagSchema, 23 23 selectNotificationSchema, 24 + selectPublicMonitorSchema, 24 25 } from "@openstatus/db/src/schema"; 25 26 import { allPlans } from "@openstatus/plans"; 26 27 ··· 187 188 }); 188 189 } 189 190 return parsedMonitor.data; 191 + }), 192 + 193 + getPublicMonitorById: publicProcedure 194 + // REMINDER: if on status page, we should check if the monitor is associated with the page 195 + // otherwise, using `/public` we don't need to check 196 + .input(z.object({ id: z.number(), slug: z.string().optional() })) 197 + .query(async (opts) => { 198 + const _monitor = await opts.ctx.db.query.monitor.findFirst({ 199 + where: and( 200 + eq(monitor.id, opts.input.id), 201 + isNull(monitor.deletedAt), 202 + eq(monitor.public, true), 203 + ), 204 + }); 205 + if (!_monitor) return undefined; 206 + 207 + if (opts.input.slug) { 208 + const _page = await opts.ctx.db.query.page.findFirst({ 209 + where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 210 + with: { monitorsToPages: true }, 211 + }); 212 + 213 + const hasPageRelation = _page?.monitorsToPages.find( 214 + ({ monitorId }) => _monitor.id === monitorId, 215 + ); 216 + 217 + if (!hasPageRelation) return undefined; 218 + } 219 + 220 + return selectPublicMonitorSchema.parse(_monitor); 190 221 }), 191 222 192 223 update: protectedProcedure
+3 -1
packages/api/src/router/page.ts
··· 222 222 ? await opts.ctx.db.query.statusReport.findMany({ 223 223 where: or(inArray(statusReport.id, statusReportIds)), 224 224 with: { 225 - statusReportUpdates: true, 225 + statusReportUpdates: { 226 + orderBy: (reports, { desc }) => desc(reports.date), 227 + }, 226 228 monitorsToStatusReports: { with: { monitor: true } }, 227 229 pagesToStatusReports: true, 228 230 },
+3 -1
packages/api/src/router/statusReport.ts
··· 393 393 ), 394 394 with: { 395 395 monitorsToStatusReports: { with: { monitor: true } }, 396 - statusReportUpdates: true, 396 + statusReportUpdates: { 397 + orderBy: (reports, { desc }) => desc(reports.date), 398 + }, 397 399 }, 398 400 }); 399 401
+1
packages/db/drizzle/0025_strong_thunderball.sql
··· 1 + ALTER TABLE monitor ADD `public` integer DEFAULT false;
+1641
packages/db/drizzle/meta/0025_snapshot.json
··· 1 + { 2 + "version": "5", 3 + "dialect": "sqlite", 4 + "id": "40ddc769-9652-470e-87af-71f269131bc3", 5 + "prevId": "96f82403-94f3-49fd-871b-946bc0a687f2", 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_reports_to_pages": { 74 + "name": "status_reports_to_pages", 75 + "columns": { 76 + "page_id": { 77 + "name": "page_id", 78 + "type": "integer", 79 + "primaryKey": false, 80 + "notNull": true, 81 + "autoincrement": false 82 + }, 83 + "status_report_id": { 84 + "name": "status_report_id", 85 + "type": "integer", 86 + "primaryKey": false, 87 + "notNull": true, 88 + "autoincrement": false 89 + }, 90 + "created_at": { 91 + "name": "created_at", 92 + "type": "integer", 93 + "primaryKey": false, 94 + "notNull": false, 95 + "autoincrement": false, 96 + "default": "(strftime('%s', 'now'))" 97 + } 98 + }, 99 + "indexes": {}, 100 + "foreignKeys": { 101 + "status_reports_to_pages_page_id_page_id_fk": { 102 + "name": "status_reports_to_pages_page_id_page_id_fk", 103 + "tableFrom": "status_reports_to_pages", 104 + "tableTo": "page", 105 + "columnsFrom": [ 106 + "page_id" 107 + ], 108 + "columnsTo": [ 109 + "id" 110 + ], 111 + "onDelete": "cascade", 112 + "onUpdate": "no action" 113 + }, 114 + "status_reports_to_pages_status_report_id_status_report_id_fk": { 115 + "name": "status_reports_to_pages_status_report_id_status_report_id_fk", 116 + "tableFrom": "status_reports_to_pages", 117 + "tableTo": "status_report", 118 + "columnsFrom": [ 119 + "status_report_id" 120 + ], 121 + "columnsTo": [ 122 + "id" 123 + ], 124 + "onDelete": "cascade", 125 + "onUpdate": "no action" 126 + } 127 + }, 128 + "compositePrimaryKeys": { 129 + "status_reports_to_pages_page_id_status_report_id_pk": { 130 + "columns": [ 131 + "page_id", 132 + "status_report_id" 133 + ], 134 + "name": "status_reports_to_pages_page_id_status_report_id_pk" 135 + } 136 + }, 137 + "uniqueConstraints": {} 138 + }, 139 + "status_report": { 140 + "name": "status_report", 141 + "columns": { 142 + "id": { 143 + "name": "id", 144 + "type": "integer", 145 + "primaryKey": true, 146 + "notNull": true, 147 + "autoincrement": false 148 + }, 149 + "status": { 150 + "name": "status", 151 + "type": "text", 152 + "primaryKey": false, 153 + "notNull": true, 154 + "autoincrement": false 155 + }, 156 + "title": { 157 + "name": "title", 158 + "type": "text(256)", 159 + "primaryKey": false, 160 + "notNull": true, 161 + "autoincrement": false 162 + }, 163 + "workspace_id": { 164 + "name": "workspace_id", 165 + "type": "integer", 166 + "primaryKey": false, 167 + "notNull": false, 168 + "autoincrement": false 169 + }, 170 + "created_at": { 171 + "name": "created_at", 172 + "type": "integer", 173 + "primaryKey": false, 174 + "notNull": false, 175 + "autoincrement": false, 176 + "default": "(strftime('%s', 'now'))" 177 + }, 178 + "updated_at": { 179 + "name": "updated_at", 180 + "type": "integer", 181 + "primaryKey": false, 182 + "notNull": false, 183 + "autoincrement": false, 184 + "default": "(strftime('%s', 'now'))" 185 + } 186 + }, 187 + "indexes": {}, 188 + "foreignKeys": { 189 + "status_report_workspace_id_workspace_id_fk": { 190 + "name": "status_report_workspace_id_workspace_id_fk", 191 + "tableFrom": "status_report", 192 + "tableTo": "workspace", 193 + "columnsFrom": [ 194 + "workspace_id" 195 + ], 196 + "columnsTo": [ 197 + "id" 198 + ], 199 + "onDelete": "no action", 200 + "onUpdate": "no action" 201 + } 202 + }, 203 + "compositePrimaryKeys": {}, 204 + "uniqueConstraints": {} 205 + }, 206 + "status_report_update": { 207 + "name": "status_report_update", 208 + "columns": { 209 + "id": { 210 + "name": "id", 211 + "type": "integer", 212 + "primaryKey": true, 213 + "notNull": true, 214 + "autoincrement": false 215 + }, 216 + "status": { 217 + "name": "status", 218 + "type": "text(4)", 219 + "primaryKey": false, 220 + "notNull": true, 221 + "autoincrement": false 222 + }, 223 + "date": { 224 + "name": "date", 225 + "type": "integer", 226 + "primaryKey": false, 227 + "notNull": true, 228 + "autoincrement": false 229 + }, 230 + "message": { 231 + "name": "message", 232 + "type": "text", 233 + "primaryKey": false, 234 + "notNull": true, 235 + "autoincrement": false 236 + }, 237 + "status_report_id": { 238 + "name": "status_report_id", 239 + "type": "integer", 240 + "primaryKey": false, 241 + "notNull": true, 242 + "autoincrement": false 243 + }, 244 + "created_at": { 245 + "name": "created_at", 246 + "type": "integer", 247 + "primaryKey": false, 248 + "notNull": false, 249 + "autoincrement": false, 250 + "default": "(strftime('%s', 'now'))" 251 + }, 252 + "updated_at": { 253 + "name": "updated_at", 254 + "type": "integer", 255 + "primaryKey": false, 256 + "notNull": false, 257 + "autoincrement": false, 258 + "default": "(strftime('%s', 'now'))" 259 + } 260 + }, 261 + "indexes": {}, 262 + "foreignKeys": { 263 + "status_report_update_status_report_id_status_report_id_fk": { 264 + "name": "status_report_update_status_report_id_status_report_id_fk", 265 + "tableFrom": "status_report_update", 266 + "tableTo": "status_report", 267 + "columnsFrom": [ 268 + "status_report_id" 269 + ], 270 + "columnsTo": [ 271 + "id" 272 + ], 273 + "onDelete": "cascade", 274 + "onUpdate": "no action" 275 + } 276 + }, 277 + "compositePrimaryKeys": {}, 278 + "uniqueConstraints": {} 279 + }, 280 + "integration": { 281 + "name": "integration", 282 + "columns": { 283 + "id": { 284 + "name": "id", 285 + "type": "integer", 286 + "primaryKey": true, 287 + "notNull": true, 288 + "autoincrement": false 289 + }, 290 + "name": { 291 + "name": "name", 292 + "type": "text(256)", 293 + "primaryKey": false, 294 + "notNull": true, 295 + "autoincrement": false 296 + }, 297 + "workspace_id": { 298 + "name": "workspace_id", 299 + "type": "integer", 300 + "primaryKey": false, 301 + "notNull": false, 302 + "autoincrement": false 303 + }, 304 + "credential": { 305 + "name": "credential", 306 + "type": "text", 307 + "primaryKey": false, 308 + "notNull": false, 309 + "autoincrement": false 310 + }, 311 + "external_id": { 312 + "name": "external_id", 313 + "type": "text", 314 + "primaryKey": false, 315 + "notNull": true, 316 + "autoincrement": false 317 + }, 318 + "created_at": { 319 + "name": "created_at", 320 + "type": "integer", 321 + "primaryKey": false, 322 + "notNull": false, 323 + "autoincrement": false, 324 + "default": "(strftime('%s', 'now'))" 325 + }, 326 + "updated_at": { 327 + "name": "updated_at", 328 + "type": "integer", 329 + "primaryKey": false, 330 + "notNull": false, 331 + "autoincrement": false, 332 + "default": "(strftime('%s', 'now'))" 333 + }, 334 + "data": { 335 + "name": "data", 336 + "type": "text", 337 + "primaryKey": false, 338 + "notNull": true, 339 + "autoincrement": false 340 + } 341 + }, 342 + "indexes": {}, 343 + "foreignKeys": { 344 + "integration_workspace_id_workspace_id_fk": { 345 + "name": "integration_workspace_id_workspace_id_fk", 346 + "tableFrom": "integration", 347 + "tableTo": "workspace", 348 + "columnsFrom": [ 349 + "workspace_id" 350 + ], 351 + "columnsTo": [ 352 + "id" 353 + ], 354 + "onDelete": "no action", 355 + "onUpdate": "no action" 356 + } 357 + }, 358 + "compositePrimaryKeys": {}, 359 + "uniqueConstraints": {} 360 + }, 361 + "page": { 362 + "name": "page", 363 + "columns": { 364 + "id": { 365 + "name": "id", 366 + "type": "integer", 367 + "primaryKey": true, 368 + "notNull": true, 369 + "autoincrement": false 370 + }, 371 + "workspace_id": { 372 + "name": "workspace_id", 373 + "type": "integer", 374 + "primaryKey": false, 375 + "notNull": true, 376 + "autoincrement": false 377 + }, 378 + "title": { 379 + "name": "title", 380 + "type": "text", 381 + "primaryKey": false, 382 + "notNull": true, 383 + "autoincrement": false 384 + }, 385 + "description": { 386 + "name": "description", 387 + "type": "text", 388 + "primaryKey": false, 389 + "notNull": true, 390 + "autoincrement": false 391 + }, 392 + "icon": { 393 + "name": "icon", 394 + "type": "text(256)", 395 + "primaryKey": false, 396 + "notNull": false, 397 + "autoincrement": false, 398 + "default": "''" 399 + }, 400 + "slug": { 401 + "name": "slug", 402 + "type": "text(256)", 403 + "primaryKey": false, 404 + "notNull": true, 405 + "autoincrement": false 406 + }, 407 + "custom_domain": { 408 + "name": "custom_domain", 409 + "type": "text(256)", 410 + "primaryKey": false, 411 + "notNull": true, 412 + "autoincrement": false 413 + }, 414 + "published": { 415 + "name": "published", 416 + "type": "integer", 417 + "primaryKey": false, 418 + "notNull": false, 419 + "autoincrement": false, 420 + "default": false 421 + }, 422 + "created_at": { 423 + "name": "created_at", 424 + "type": "integer", 425 + "primaryKey": false, 426 + "notNull": false, 427 + "autoincrement": false, 428 + "default": "(strftime('%s', 'now'))" 429 + }, 430 + "updated_at": { 431 + "name": "updated_at", 432 + "type": "integer", 433 + "primaryKey": false, 434 + "notNull": false, 435 + "autoincrement": false, 436 + "default": "(strftime('%s', 'now'))" 437 + } 438 + }, 439 + "indexes": { 440 + "page_slug_unique": { 441 + "name": "page_slug_unique", 442 + "columns": [ 443 + "slug" 444 + ], 445 + "isUnique": true 446 + } 447 + }, 448 + "foreignKeys": { 449 + "page_workspace_id_workspace_id_fk": { 450 + "name": "page_workspace_id_workspace_id_fk", 451 + "tableFrom": "page", 452 + "tableTo": "workspace", 453 + "columnsFrom": [ 454 + "workspace_id" 455 + ], 456 + "columnsTo": [ 457 + "id" 458 + ], 459 + "onDelete": "cascade", 460 + "onUpdate": "no action" 461 + } 462 + }, 463 + "compositePrimaryKeys": {}, 464 + "uniqueConstraints": {} 465 + }, 466 + "monitor": { 467 + "name": "monitor", 468 + "columns": { 469 + "id": { 470 + "name": "id", 471 + "type": "integer", 472 + "primaryKey": true, 473 + "notNull": true, 474 + "autoincrement": false 475 + }, 476 + "job_type": { 477 + "name": "job_type", 478 + "type": "text", 479 + "primaryKey": false, 480 + "notNull": true, 481 + "autoincrement": false, 482 + "default": "'other'" 483 + }, 484 + "periodicity": { 485 + "name": "periodicity", 486 + "type": "text", 487 + "primaryKey": false, 488 + "notNull": true, 489 + "autoincrement": false, 490 + "default": "'other'" 491 + }, 492 + "status": { 493 + "name": "status", 494 + "type": "text", 495 + "primaryKey": false, 496 + "notNull": true, 497 + "autoincrement": false, 498 + "default": "'active'" 499 + }, 500 + "active": { 501 + "name": "active", 502 + "type": "integer", 503 + "primaryKey": false, 504 + "notNull": false, 505 + "autoincrement": false, 506 + "default": false 507 + }, 508 + "regions": { 509 + "name": "regions", 510 + "type": "text", 511 + "primaryKey": false, 512 + "notNull": true, 513 + "autoincrement": false, 514 + "default": "''" 515 + }, 516 + "url": { 517 + "name": "url", 518 + "type": "text(2048)", 519 + "primaryKey": false, 520 + "notNull": true, 521 + "autoincrement": false 522 + }, 523 + "name": { 524 + "name": "name", 525 + "type": "text(256)", 526 + "primaryKey": false, 527 + "notNull": true, 528 + "autoincrement": false, 529 + "default": "''" 530 + }, 531 + "description": { 532 + "name": "description", 533 + "type": "text", 534 + "primaryKey": false, 535 + "notNull": true, 536 + "autoincrement": false, 537 + "default": "''" 538 + }, 539 + "headers": { 540 + "name": "headers", 541 + "type": "text", 542 + "primaryKey": false, 543 + "notNull": false, 544 + "autoincrement": false, 545 + "default": "''" 546 + }, 547 + "body": { 548 + "name": "body", 549 + "type": "text", 550 + "primaryKey": false, 551 + "notNull": false, 552 + "autoincrement": false, 553 + "default": "''" 554 + }, 555 + "method": { 556 + "name": "method", 557 + "type": "text", 558 + "primaryKey": false, 559 + "notNull": false, 560 + "autoincrement": false, 561 + "default": "'GET'" 562 + }, 563 + "workspace_id": { 564 + "name": "workspace_id", 565 + "type": "integer", 566 + "primaryKey": false, 567 + "notNull": false, 568 + "autoincrement": false 569 + }, 570 + "assertions": { 571 + "name": "assertions", 572 + "type": "text", 573 + "primaryKey": false, 574 + "notNull": false, 575 + "autoincrement": false 576 + }, 577 + "public": { 578 + "name": "public", 579 + "type": "integer", 580 + "primaryKey": false, 581 + "notNull": false, 582 + "autoincrement": false, 583 + "default": false 584 + }, 585 + "created_at": { 586 + "name": "created_at", 587 + "type": "integer", 588 + "primaryKey": false, 589 + "notNull": false, 590 + "autoincrement": false, 591 + "default": "(strftime('%s', 'now'))" 592 + }, 593 + "updated_at": { 594 + "name": "updated_at", 595 + "type": "integer", 596 + "primaryKey": false, 597 + "notNull": false, 598 + "autoincrement": false, 599 + "default": "(strftime('%s', 'now'))" 600 + }, 601 + "deleted_at": { 602 + "name": "deleted_at", 603 + "type": "integer", 604 + "primaryKey": false, 605 + "notNull": false, 606 + "autoincrement": false 607 + } 608 + }, 609 + "indexes": {}, 610 + "foreignKeys": { 611 + "monitor_workspace_id_workspace_id_fk": { 612 + "name": "monitor_workspace_id_workspace_id_fk", 613 + "tableFrom": "monitor", 614 + "tableTo": "workspace", 615 + "columnsFrom": [ 616 + "workspace_id" 617 + ], 618 + "columnsTo": [ 619 + "id" 620 + ], 621 + "onDelete": "no action", 622 + "onUpdate": "no action" 623 + } 624 + }, 625 + "compositePrimaryKeys": {}, 626 + "uniqueConstraints": {} 627 + }, 628 + "monitors_to_pages": { 629 + "name": "monitors_to_pages", 630 + "columns": { 631 + "monitor_id": { 632 + "name": "monitor_id", 633 + "type": "integer", 634 + "primaryKey": false, 635 + "notNull": true, 636 + "autoincrement": false 637 + }, 638 + "page_id": { 639 + "name": "page_id", 640 + "type": "integer", 641 + "primaryKey": false, 642 + "notNull": true, 643 + "autoincrement": false 644 + }, 645 + "created_at": { 646 + "name": "created_at", 647 + "type": "integer", 648 + "primaryKey": false, 649 + "notNull": false, 650 + "autoincrement": false, 651 + "default": "(strftime('%s', 'now'))" 652 + } 653 + }, 654 + "indexes": {}, 655 + "foreignKeys": { 656 + "monitors_to_pages_monitor_id_monitor_id_fk": { 657 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 658 + "tableFrom": "monitors_to_pages", 659 + "tableTo": "monitor", 660 + "columnsFrom": [ 661 + "monitor_id" 662 + ], 663 + "columnsTo": [ 664 + "id" 665 + ], 666 + "onDelete": "cascade", 667 + "onUpdate": "no action" 668 + }, 669 + "monitors_to_pages_page_id_page_id_fk": { 670 + "name": "monitors_to_pages_page_id_page_id_fk", 671 + "tableFrom": "monitors_to_pages", 672 + "tableTo": "page", 673 + "columnsFrom": [ 674 + "page_id" 675 + ], 676 + "columnsTo": [ 677 + "id" 678 + ], 679 + "onDelete": "cascade", 680 + "onUpdate": "no action" 681 + } 682 + }, 683 + "compositePrimaryKeys": { 684 + "monitors_to_pages_monitor_id_page_id_pk": { 685 + "columns": [ 686 + "monitor_id", 687 + "page_id" 688 + ], 689 + "name": "monitors_to_pages_monitor_id_page_id_pk" 690 + } 691 + }, 692 + "uniqueConstraints": {} 693 + }, 694 + "user": { 695 + "name": "user", 696 + "columns": { 697 + "id": { 698 + "name": "id", 699 + "type": "integer", 700 + "primaryKey": true, 701 + "notNull": true, 702 + "autoincrement": false 703 + }, 704 + "tenant_id": { 705 + "name": "tenant_id", 706 + "type": "text(256)", 707 + "primaryKey": false, 708 + "notNull": false, 709 + "autoincrement": false 710 + }, 711 + "first_name": { 712 + "name": "first_name", 713 + "type": "text", 714 + "primaryKey": false, 715 + "notNull": false, 716 + "autoincrement": false, 717 + "default": "''" 718 + }, 719 + "last_name": { 720 + "name": "last_name", 721 + "type": "text", 722 + "primaryKey": false, 723 + "notNull": false, 724 + "autoincrement": false, 725 + "default": "''" 726 + }, 727 + "email": { 728 + "name": "email", 729 + "type": "text", 730 + "primaryKey": false, 731 + "notNull": false, 732 + "autoincrement": false, 733 + "default": "''" 734 + }, 735 + "photo_url": { 736 + "name": "photo_url", 737 + "type": "text", 738 + "primaryKey": false, 739 + "notNull": false, 740 + "autoincrement": false, 741 + "default": "''" 742 + }, 743 + "created_at": { 744 + "name": "created_at", 745 + "type": "integer", 746 + "primaryKey": false, 747 + "notNull": false, 748 + "autoincrement": false, 749 + "default": "(strftime('%s', 'now'))" 750 + }, 751 + "updated_at": { 752 + "name": "updated_at", 753 + "type": "integer", 754 + "primaryKey": false, 755 + "notNull": false, 756 + "autoincrement": false, 757 + "default": "(strftime('%s', 'now'))" 758 + } 759 + }, 760 + "indexes": { 761 + "user_tenant_id_unique": { 762 + "name": "user_tenant_id_unique", 763 + "columns": [ 764 + "tenant_id" 765 + ], 766 + "isUnique": true 767 + } 768 + }, 769 + "foreignKeys": {}, 770 + "compositePrimaryKeys": {}, 771 + "uniqueConstraints": {} 772 + }, 773 + "users_to_workspaces": { 774 + "name": "users_to_workspaces", 775 + "columns": { 776 + "user_id": { 777 + "name": "user_id", 778 + "type": "integer", 779 + "primaryKey": false, 780 + "notNull": true, 781 + "autoincrement": false 782 + }, 783 + "workspace_id": { 784 + "name": "workspace_id", 785 + "type": "integer", 786 + "primaryKey": false, 787 + "notNull": true, 788 + "autoincrement": false 789 + }, 790 + "role": { 791 + "name": "role", 792 + "type": "text", 793 + "primaryKey": false, 794 + "notNull": true, 795 + "autoincrement": false, 796 + "default": "'member'" 797 + }, 798 + "created_at": { 799 + "name": "created_at", 800 + "type": "integer", 801 + "primaryKey": false, 802 + "notNull": false, 803 + "autoincrement": false, 804 + "default": "(strftime('%s', 'now'))" 805 + } 806 + }, 807 + "indexes": {}, 808 + "foreignKeys": { 809 + "users_to_workspaces_user_id_user_id_fk": { 810 + "name": "users_to_workspaces_user_id_user_id_fk", 811 + "tableFrom": "users_to_workspaces", 812 + "tableTo": "user", 813 + "columnsFrom": [ 814 + "user_id" 815 + ], 816 + "columnsTo": [ 817 + "id" 818 + ], 819 + "onDelete": "no action", 820 + "onUpdate": "no action" 821 + }, 822 + "users_to_workspaces_workspace_id_workspace_id_fk": { 823 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 824 + "tableFrom": "users_to_workspaces", 825 + "tableTo": "workspace", 826 + "columnsFrom": [ 827 + "workspace_id" 828 + ], 829 + "columnsTo": [ 830 + "id" 831 + ], 832 + "onDelete": "no action", 833 + "onUpdate": "no action" 834 + } 835 + }, 836 + "compositePrimaryKeys": { 837 + "users_to_workspaces_user_id_workspace_id_pk": { 838 + "columns": [ 839 + "user_id", 840 + "workspace_id" 841 + ], 842 + "name": "users_to_workspaces_user_id_workspace_id_pk" 843 + } 844 + }, 845 + "uniqueConstraints": {} 846 + }, 847 + "page_subscriber": { 848 + "name": "page_subscriber", 849 + "columns": { 850 + "id": { 851 + "name": "id", 852 + "type": "integer", 853 + "primaryKey": true, 854 + "notNull": true, 855 + "autoincrement": false 856 + }, 857 + "email": { 858 + "name": "email", 859 + "type": "text", 860 + "primaryKey": false, 861 + "notNull": true, 862 + "autoincrement": false 863 + }, 864 + "page_id": { 865 + "name": "page_id", 866 + "type": "integer", 867 + "primaryKey": false, 868 + "notNull": true, 869 + "autoincrement": false 870 + }, 871 + "token": { 872 + "name": "token", 873 + "type": "text", 874 + "primaryKey": false, 875 + "notNull": false, 876 + "autoincrement": false 877 + }, 878 + "accepted_at": { 879 + "name": "accepted_at", 880 + "type": "integer", 881 + "primaryKey": false, 882 + "notNull": false, 883 + "autoincrement": false 884 + }, 885 + "expires_at": { 886 + "name": "expires_at", 887 + "type": "integer", 888 + "primaryKey": false, 889 + "notNull": false, 890 + "autoincrement": false 891 + }, 892 + "created_at": { 893 + "name": "created_at", 894 + "type": "integer", 895 + "primaryKey": false, 896 + "notNull": false, 897 + "autoincrement": false, 898 + "default": "(strftime('%s', 'now'))" 899 + }, 900 + "updated_at": { 901 + "name": "updated_at", 902 + "type": "integer", 903 + "primaryKey": false, 904 + "notNull": false, 905 + "autoincrement": false, 906 + "default": "(strftime('%s', 'now'))" 907 + } 908 + }, 909 + "indexes": {}, 910 + "foreignKeys": { 911 + "page_subscriber_page_id_page_id_fk": { 912 + "name": "page_subscriber_page_id_page_id_fk", 913 + "tableFrom": "page_subscriber", 914 + "tableTo": "page", 915 + "columnsFrom": [ 916 + "page_id" 917 + ], 918 + "columnsTo": [ 919 + "id" 920 + ], 921 + "onDelete": "no action", 922 + "onUpdate": "no action" 923 + } 924 + }, 925 + "compositePrimaryKeys": {}, 926 + "uniqueConstraints": {} 927 + }, 928 + "workspace": { 929 + "name": "workspace", 930 + "columns": { 931 + "id": { 932 + "name": "id", 933 + "type": "integer", 934 + "primaryKey": true, 935 + "notNull": true, 936 + "autoincrement": false 937 + }, 938 + "slug": { 939 + "name": "slug", 940 + "type": "text", 941 + "primaryKey": false, 942 + "notNull": true, 943 + "autoincrement": false 944 + }, 945 + "name": { 946 + "name": "name", 947 + "type": "text", 948 + "primaryKey": false, 949 + "notNull": false, 950 + "autoincrement": false 951 + }, 952 + "stripe_id": { 953 + "name": "stripe_id", 954 + "type": "text(256)", 955 + "primaryKey": false, 956 + "notNull": false, 957 + "autoincrement": false 958 + }, 959 + "subscription_id": { 960 + "name": "subscription_id", 961 + "type": "text", 962 + "primaryKey": false, 963 + "notNull": false, 964 + "autoincrement": false 965 + }, 966 + "plan": { 967 + "name": "plan", 968 + "type": "text", 969 + "primaryKey": false, 970 + "notNull": false, 971 + "autoincrement": false 972 + }, 973 + "ends_at": { 974 + "name": "ends_at", 975 + "type": "integer", 976 + "primaryKey": false, 977 + "notNull": false, 978 + "autoincrement": false 979 + }, 980 + "paid_until": { 981 + "name": "paid_until", 982 + "type": "integer", 983 + "primaryKey": false, 984 + "notNull": false, 985 + "autoincrement": false 986 + }, 987 + "created_at": { 988 + "name": "created_at", 989 + "type": "integer", 990 + "primaryKey": false, 991 + "notNull": false, 992 + "autoincrement": false, 993 + "default": "(strftime('%s', 'now'))" 994 + }, 995 + "updated_at": { 996 + "name": "updated_at", 997 + "type": "integer", 998 + "primaryKey": false, 999 + "notNull": false, 1000 + "autoincrement": false, 1001 + "default": "(strftime('%s', 'now'))" 1002 + } 1003 + }, 1004 + "indexes": { 1005 + "workspace_slug_unique": { 1006 + "name": "workspace_slug_unique", 1007 + "columns": [ 1008 + "slug" 1009 + ], 1010 + "isUnique": true 1011 + }, 1012 + "workspace_stripe_id_unique": { 1013 + "name": "workspace_stripe_id_unique", 1014 + "columns": [ 1015 + "stripe_id" 1016 + ], 1017 + "isUnique": true 1018 + } 1019 + }, 1020 + "foreignKeys": {}, 1021 + "compositePrimaryKeys": {}, 1022 + "uniqueConstraints": {} 1023 + }, 1024 + "notification": { 1025 + "name": "notification", 1026 + "columns": { 1027 + "id": { 1028 + "name": "id", 1029 + "type": "integer", 1030 + "primaryKey": true, 1031 + "notNull": true, 1032 + "autoincrement": false 1033 + }, 1034 + "name": { 1035 + "name": "name", 1036 + "type": "text", 1037 + "primaryKey": false, 1038 + "notNull": true, 1039 + "autoincrement": false 1040 + }, 1041 + "provider": { 1042 + "name": "provider", 1043 + "type": "text", 1044 + "primaryKey": false, 1045 + "notNull": true, 1046 + "autoincrement": false 1047 + }, 1048 + "data": { 1049 + "name": "data", 1050 + "type": "text", 1051 + "primaryKey": false, 1052 + "notNull": false, 1053 + "autoincrement": false, 1054 + "default": "'{}'" 1055 + }, 1056 + "workspace_id": { 1057 + "name": "workspace_id", 1058 + "type": "integer", 1059 + "primaryKey": false, 1060 + "notNull": false, 1061 + "autoincrement": false 1062 + }, 1063 + "created_at": { 1064 + "name": "created_at", 1065 + "type": "integer", 1066 + "primaryKey": false, 1067 + "notNull": false, 1068 + "autoincrement": false, 1069 + "default": "(strftime('%s', 'now'))" 1070 + }, 1071 + "updated_at": { 1072 + "name": "updated_at", 1073 + "type": "integer", 1074 + "primaryKey": false, 1075 + "notNull": false, 1076 + "autoincrement": false, 1077 + "default": "(strftime('%s', 'now'))" 1078 + } 1079 + }, 1080 + "indexes": {}, 1081 + "foreignKeys": { 1082 + "notification_workspace_id_workspace_id_fk": { 1083 + "name": "notification_workspace_id_workspace_id_fk", 1084 + "tableFrom": "notification", 1085 + "tableTo": "workspace", 1086 + "columnsFrom": [ 1087 + "workspace_id" 1088 + ], 1089 + "columnsTo": [ 1090 + "id" 1091 + ], 1092 + "onDelete": "no action", 1093 + "onUpdate": "no action" 1094 + } 1095 + }, 1096 + "compositePrimaryKeys": {}, 1097 + "uniqueConstraints": {} 1098 + }, 1099 + "notifications_to_monitors": { 1100 + "name": "notifications_to_monitors", 1101 + "columns": { 1102 + "monitor_id": { 1103 + "name": "monitor_id", 1104 + "type": "integer", 1105 + "primaryKey": false, 1106 + "notNull": true, 1107 + "autoincrement": false 1108 + }, 1109 + "notification_id": { 1110 + "name": "notification_id", 1111 + "type": "integer", 1112 + "primaryKey": false, 1113 + "notNull": true, 1114 + "autoincrement": false 1115 + }, 1116 + "created_at": { 1117 + "name": "created_at", 1118 + "type": "integer", 1119 + "primaryKey": false, 1120 + "notNull": false, 1121 + "autoincrement": false, 1122 + "default": "(strftime('%s', 'now'))" 1123 + } 1124 + }, 1125 + "indexes": {}, 1126 + "foreignKeys": { 1127 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 1128 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 1129 + "tableFrom": "notifications_to_monitors", 1130 + "tableTo": "monitor", 1131 + "columnsFrom": [ 1132 + "monitor_id" 1133 + ], 1134 + "columnsTo": [ 1135 + "id" 1136 + ], 1137 + "onDelete": "cascade", 1138 + "onUpdate": "no action" 1139 + }, 1140 + "notifications_to_monitors_notification_id_notification_id_fk": { 1141 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 1142 + "tableFrom": "notifications_to_monitors", 1143 + "tableTo": "notification", 1144 + "columnsFrom": [ 1145 + "notification_id" 1146 + ], 1147 + "columnsTo": [ 1148 + "id" 1149 + ], 1150 + "onDelete": "cascade", 1151 + "onUpdate": "no action" 1152 + } 1153 + }, 1154 + "compositePrimaryKeys": { 1155 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1156 + "columns": [ 1157 + "monitor_id", 1158 + "notification_id" 1159 + ], 1160 + "name": "notifications_to_monitors_monitor_id_notification_id_pk" 1161 + } 1162 + }, 1163 + "uniqueConstraints": {} 1164 + }, 1165 + "monitor_status": { 1166 + "name": "monitor_status", 1167 + "columns": { 1168 + "monitor_id": { 1169 + "name": "monitor_id", 1170 + "type": "integer", 1171 + "primaryKey": false, 1172 + "notNull": true, 1173 + "autoincrement": false 1174 + }, 1175 + "region": { 1176 + "name": "region", 1177 + "type": "text", 1178 + "primaryKey": false, 1179 + "notNull": true, 1180 + "autoincrement": false, 1181 + "default": "''" 1182 + }, 1183 + "status": { 1184 + "name": "status", 1185 + "type": "text", 1186 + "primaryKey": false, 1187 + "notNull": true, 1188 + "autoincrement": false, 1189 + "default": "'active'" 1190 + }, 1191 + "created_at": { 1192 + "name": "created_at", 1193 + "type": "integer", 1194 + "primaryKey": false, 1195 + "notNull": false, 1196 + "autoincrement": false, 1197 + "default": "(strftime('%s', 'now'))" 1198 + }, 1199 + "updated_at": { 1200 + "name": "updated_at", 1201 + "type": "integer", 1202 + "primaryKey": false, 1203 + "notNull": false, 1204 + "autoincrement": false, 1205 + "default": "(strftime('%s', 'now'))" 1206 + } 1207 + }, 1208 + "indexes": { 1209 + "monitor_status_idx": { 1210 + "name": "monitor_status_idx", 1211 + "columns": [ 1212 + "monitor_id", 1213 + "region" 1214 + ], 1215 + "isUnique": false 1216 + } 1217 + }, 1218 + "foreignKeys": { 1219 + "monitor_status_monitor_id_monitor_id_fk": { 1220 + "name": "monitor_status_monitor_id_monitor_id_fk", 1221 + "tableFrom": "monitor_status", 1222 + "tableTo": "monitor", 1223 + "columnsFrom": [ 1224 + "monitor_id" 1225 + ], 1226 + "columnsTo": [ 1227 + "id" 1228 + ], 1229 + "onDelete": "cascade", 1230 + "onUpdate": "no action" 1231 + } 1232 + }, 1233 + "compositePrimaryKeys": { 1234 + "monitor_status_monitor_id_region_pk": { 1235 + "columns": [ 1236 + "monitor_id", 1237 + "region" 1238 + ], 1239 + "name": "monitor_status_monitor_id_region_pk" 1240 + } 1241 + }, 1242 + "uniqueConstraints": {} 1243 + }, 1244 + "invitation": { 1245 + "name": "invitation", 1246 + "columns": { 1247 + "id": { 1248 + "name": "id", 1249 + "type": "integer", 1250 + "primaryKey": true, 1251 + "notNull": true, 1252 + "autoincrement": false 1253 + }, 1254 + "email": { 1255 + "name": "email", 1256 + "type": "text", 1257 + "primaryKey": false, 1258 + "notNull": true, 1259 + "autoincrement": false 1260 + }, 1261 + "role": { 1262 + "name": "role", 1263 + "type": "text", 1264 + "primaryKey": false, 1265 + "notNull": true, 1266 + "autoincrement": false, 1267 + "default": "'member'" 1268 + }, 1269 + "workspace_id": { 1270 + "name": "workspace_id", 1271 + "type": "integer", 1272 + "primaryKey": false, 1273 + "notNull": true, 1274 + "autoincrement": false 1275 + }, 1276 + "token": { 1277 + "name": "token", 1278 + "type": "text", 1279 + "primaryKey": false, 1280 + "notNull": true, 1281 + "autoincrement": false 1282 + }, 1283 + "expires_at": { 1284 + "name": "expires_at", 1285 + "type": "integer", 1286 + "primaryKey": false, 1287 + "notNull": true, 1288 + "autoincrement": false 1289 + }, 1290 + "created_at": { 1291 + "name": "created_at", 1292 + "type": "integer", 1293 + "primaryKey": false, 1294 + "notNull": false, 1295 + "autoincrement": false, 1296 + "default": "(strftime('%s', 'now'))" 1297 + }, 1298 + "accepted_at": { 1299 + "name": "accepted_at", 1300 + "type": "integer", 1301 + "primaryKey": false, 1302 + "notNull": false, 1303 + "autoincrement": false 1304 + } 1305 + }, 1306 + "indexes": {}, 1307 + "foreignKeys": {}, 1308 + "compositePrimaryKeys": {}, 1309 + "uniqueConstraints": {} 1310 + }, 1311 + "incident": { 1312 + "name": "incident", 1313 + "columns": { 1314 + "id": { 1315 + "name": "id", 1316 + "type": "integer", 1317 + "primaryKey": true, 1318 + "notNull": true, 1319 + "autoincrement": false 1320 + }, 1321 + "title": { 1322 + "name": "title", 1323 + "type": "text", 1324 + "primaryKey": false, 1325 + "notNull": true, 1326 + "autoincrement": false, 1327 + "default": "''" 1328 + }, 1329 + "summary": { 1330 + "name": "summary", 1331 + "type": "text", 1332 + "primaryKey": false, 1333 + "notNull": true, 1334 + "autoincrement": false, 1335 + "default": "''" 1336 + }, 1337 + "status": { 1338 + "name": "status", 1339 + "type": "text", 1340 + "primaryKey": false, 1341 + "notNull": true, 1342 + "autoincrement": false, 1343 + "default": "'triage'" 1344 + }, 1345 + "monitor_id": { 1346 + "name": "monitor_id", 1347 + "type": "integer", 1348 + "primaryKey": false, 1349 + "notNull": false, 1350 + "autoincrement": false 1351 + }, 1352 + "workspace_id": { 1353 + "name": "workspace_id", 1354 + "type": "integer", 1355 + "primaryKey": false, 1356 + "notNull": false, 1357 + "autoincrement": false 1358 + }, 1359 + "started_at": { 1360 + "name": "started_at", 1361 + "type": "integer", 1362 + "primaryKey": false, 1363 + "notNull": true, 1364 + "autoincrement": false, 1365 + "default": "(strftime('%s', 'now'))" 1366 + }, 1367 + "acknowledged_at": { 1368 + "name": "acknowledged_at", 1369 + "type": "integer", 1370 + "primaryKey": false, 1371 + "notNull": false, 1372 + "autoincrement": false 1373 + }, 1374 + "acknowledged_by": { 1375 + "name": "acknowledged_by", 1376 + "type": "integer", 1377 + "primaryKey": false, 1378 + "notNull": false, 1379 + "autoincrement": false 1380 + }, 1381 + "resolved_at": { 1382 + "name": "resolved_at", 1383 + "type": "integer", 1384 + "primaryKey": false, 1385 + "notNull": false, 1386 + "autoincrement": false 1387 + }, 1388 + "resolved_by": { 1389 + "name": "resolved_by", 1390 + "type": "integer", 1391 + "primaryKey": false, 1392 + "notNull": false, 1393 + "autoincrement": false 1394 + }, 1395 + "incident_screenshot_url": { 1396 + "name": "incident_screenshot_url", 1397 + "type": "text", 1398 + "primaryKey": false, 1399 + "notNull": false, 1400 + "autoincrement": false 1401 + }, 1402 + "recovery_screenshot_url": { 1403 + "name": "recovery_screenshot_url", 1404 + "type": "text", 1405 + "primaryKey": false, 1406 + "notNull": false, 1407 + "autoincrement": false 1408 + }, 1409 + "auto_resolved": { 1410 + "name": "auto_resolved", 1411 + "type": "integer", 1412 + "primaryKey": false, 1413 + "notNull": false, 1414 + "autoincrement": false, 1415 + "default": false 1416 + }, 1417 + "created_at": { 1418 + "name": "created_at", 1419 + "type": "integer", 1420 + "primaryKey": false, 1421 + "notNull": false, 1422 + "autoincrement": false, 1423 + "default": "(strftime('%s', 'now'))" 1424 + }, 1425 + "updated_at": { 1426 + "name": "updated_at", 1427 + "type": "integer", 1428 + "primaryKey": false, 1429 + "notNull": false, 1430 + "autoincrement": false, 1431 + "default": "(strftime('%s', 'now'))" 1432 + } 1433 + }, 1434 + "indexes": { 1435 + "incident_monitor_id_started_at_unique": { 1436 + "name": "incident_monitor_id_started_at_unique", 1437 + "columns": [ 1438 + "monitor_id", 1439 + "started_at" 1440 + ], 1441 + "isUnique": true 1442 + } 1443 + }, 1444 + "foreignKeys": { 1445 + "incident_monitor_id_monitor_id_fk": { 1446 + "name": "incident_monitor_id_monitor_id_fk", 1447 + "tableFrom": "incident", 1448 + "tableTo": "monitor", 1449 + "columnsFrom": [ 1450 + "monitor_id" 1451 + ], 1452 + "columnsTo": [ 1453 + "id" 1454 + ], 1455 + "onDelete": "set default", 1456 + "onUpdate": "no action" 1457 + }, 1458 + "incident_workspace_id_workspace_id_fk": { 1459 + "name": "incident_workspace_id_workspace_id_fk", 1460 + "tableFrom": "incident", 1461 + "tableTo": "workspace", 1462 + "columnsFrom": [ 1463 + "workspace_id" 1464 + ], 1465 + "columnsTo": [ 1466 + "id" 1467 + ], 1468 + "onDelete": "no action", 1469 + "onUpdate": "no action" 1470 + }, 1471 + "incident_acknowledged_by_user_id_fk": { 1472 + "name": "incident_acknowledged_by_user_id_fk", 1473 + "tableFrom": "incident", 1474 + "tableTo": "user", 1475 + "columnsFrom": [ 1476 + "acknowledged_by" 1477 + ], 1478 + "columnsTo": [ 1479 + "id" 1480 + ], 1481 + "onDelete": "no action", 1482 + "onUpdate": "no action" 1483 + }, 1484 + "incident_resolved_by_user_id_fk": { 1485 + "name": "incident_resolved_by_user_id_fk", 1486 + "tableFrom": "incident", 1487 + "tableTo": "user", 1488 + "columnsFrom": [ 1489 + "resolved_by" 1490 + ], 1491 + "columnsTo": [ 1492 + "id" 1493 + ], 1494 + "onDelete": "no action", 1495 + "onUpdate": "no action" 1496 + } 1497 + }, 1498 + "compositePrimaryKeys": {}, 1499 + "uniqueConstraints": {} 1500 + }, 1501 + "monitor_tag": { 1502 + "name": "monitor_tag", 1503 + "columns": { 1504 + "id": { 1505 + "name": "id", 1506 + "type": "integer", 1507 + "primaryKey": true, 1508 + "notNull": true, 1509 + "autoincrement": false 1510 + }, 1511 + "workspace_id": { 1512 + "name": "workspace_id", 1513 + "type": "integer", 1514 + "primaryKey": false, 1515 + "notNull": true, 1516 + "autoincrement": false 1517 + }, 1518 + "name": { 1519 + "name": "name", 1520 + "type": "text", 1521 + "primaryKey": false, 1522 + "notNull": true, 1523 + "autoincrement": false 1524 + }, 1525 + "color": { 1526 + "name": "color", 1527 + "type": "text", 1528 + "primaryKey": false, 1529 + "notNull": true, 1530 + "autoincrement": false 1531 + }, 1532 + "created_at": { 1533 + "name": "created_at", 1534 + "type": "integer", 1535 + "primaryKey": false, 1536 + "notNull": false, 1537 + "autoincrement": false, 1538 + "default": "(strftime('%s', 'now'))" 1539 + }, 1540 + "updated_at": { 1541 + "name": "updated_at", 1542 + "type": "integer", 1543 + "primaryKey": false, 1544 + "notNull": false, 1545 + "autoincrement": false, 1546 + "default": "(strftime('%s', 'now'))" 1547 + } 1548 + }, 1549 + "indexes": {}, 1550 + "foreignKeys": { 1551 + "monitor_tag_workspace_id_workspace_id_fk": { 1552 + "name": "monitor_tag_workspace_id_workspace_id_fk", 1553 + "tableFrom": "monitor_tag", 1554 + "tableTo": "workspace", 1555 + "columnsFrom": [ 1556 + "workspace_id" 1557 + ], 1558 + "columnsTo": [ 1559 + "id" 1560 + ], 1561 + "onDelete": "cascade", 1562 + "onUpdate": "no action" 1563 + } 1564 + }, 1565 + "compositePrimaryKeys": {}, 1566 + "uniqueConstraints": {} 1567 + }, 1568 + "monitor_tag_to_monitor": { 1569 + "name": "monitor_tag_to_monitor", 1570 + "columns": { 1571 + "monitor_id": { 1572 + "name": "monitor_id", 1573 + "type": "integer", 1574 + "primaryKey": false, 1575 + "notNull": true, 1576 + "autoincrement": false 1577 + }, 1578 + "monitor_tag_id": { 1579 + "name": "monitor_tag_id", 1580 + "type": "integer", 1581 + "primaryKey": false, 1582 + "notNull": true, 1583 + "autoincrement": false 1584 + }, 1585 + "created_at": { 1586 + "name": "created_at", 1587 + "type": "integer", 1588 + "primaryKey": false, 1589 + "notNull": false, 1590 + "autoincrement": false, 1591 + "default": "(strftime('%s', 'now'))" 1592 + } 1593 + }, 1594 + "indexes": {}, 1595 + "foreignKeys": { 1596 + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { 1597 + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", 1598 + "tableFrom": "monitor_tag_to_monitor", 1599 + "tableTo": "monitor", 1600 + "columnsFrom": [ 1601 + "monitor_id" 1602 + ], 1603 + "columnsTo": [ 1604 + "id" 1605 + ], 1606 + "onDelete": "cascade", 1607 + "onUpdate": "no action" 1608 + }, 1609 + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { 1610 + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", 1611 + "tableFrom": "monitor_tag_to_monitor", 1612 + "tableTo": "monitor_tag", 1613 + "columnsFrom": [ 1614 + "monitor_tag_id" 1615 + ], 1616 + "columnsTo": [ 1617 + "id" 1618 + ], 1619 + "onDelete": "cascade", 1620 + "onUpdate": "no action" 1621 + } 1622 + }, 1623 + "compositePrimaryKeys": { 1624 + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { 1625 + "columns": [ 1626 + "monitor_id", 1627 + "monitor_tag_id" 1628 + ], 1629 + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" 1630 + } 1631 + }, 1632 + "uniqueConstraints": {} 1633 + } 1634 + }, 1635 + "enums": {}, 1636 + "_meta": { 1637 + "schemas": {}, 1638 + "tables": {}, 1639 + "columns": {} 1640 + } 1641 + }
+7
packages/db/drizzle/meta/_journal.json
··· 176 176 "when": 1712354121499, 177 177 "tag": "0024_young_proudstar", 178 178 "breakpoints": true 179 + }, 180 + { 181 + "idx": 25, 182 + "version": "5", 183 + "when": 1713095971713, 184 + "tag": "0025_strong_thunderball", 185 + "breakpoints": true 179 186 } 180 187 ] 181 188 }
+2
packages/db/src/schema/monitors/monitor.ts
··· 43 43 44 44 assertions: text("assertions"), 45 45 46 + public: integer("public", { mode: "boolean" }).default(false), 47 + 46 48 createdAt: integer("created_at", { mode: "timestamp" }).default( 47 49 sql`(strftime('%s', 'now'))`, 48 50 ),
+8 -2
packages/db/src/schema/shared.ts
··· 14 14 export const selectPublicMonitorSchema = selectMonitorSchema.omit({ 15 15 body: true, 16 16 headers: true, 17 - regions: true, 18 17 method: true, 19 18 }); 20 19 ··· 54 53 export const selectPublicStatusReportSchemaWithRelation = 55 54 selectStatusReportSchema.extend({ 56 55 monitorsToStatusReports: z 57 - .array(z.object({ monitor: selectPublicMonitorSchema })) 56 + .array( 57 + z.object({ 58 + monitorId: z.number(), 59 + statusReportId: z.number(), 60 + monitor: selectPublicMonitorSchema, 61 + }), 62 + ) 58 63 .default([]), 59 64 statusReportUpdates: z.array(selectStatusReportUpdateSchema), 60 65 }); ··· 63 68 typeof selectStatusReportPageSchema 64 69 >; 65 70 export type PublicMonitor = z.infer<typeof selectPublicMonitorSchema>; 71 + export type PublicPage = z.infer<typeof selectPublicPageSchemaWithRelation>;
+1
packages/db/src/seed.mts
··· 73 73 url: "https://www.google.com", 74 74 method: "GET", 75 75 regions: "gru", 76 + public: true, 76 77 }, 77 78 { 78 79 id: 3,
+22
packages/tinybird/src/os-client.ts
··· 71 71 }; 72 72 } 73 73 74 + endpointChartAllRegions(period: "7d" | "14d") { 75 + const parameters = z.object({ 76 + monitorId: z.string(), 77 + }); 78 + 79 + return async (props: z.infer<typeof parameters>) => { 80 + try { 81 + const res = await this.tb.buildPipe({ 82 + pipe: `__ttl_${period}_chart_all_regions_get__${VERSION}`, // TODO: add pipe to @openstatus/tinybird 83 + parameters, 84 + data: z.object({ timestamp: z.number().int() }).merge(latencySchema), 85 + opts: { 86 + revalidate: DEFAULT_CACHE, 87 + }, 88 + })(props); 89 + return res.data; 90 + } catch (e) { 91 + console.error(e); 92 + } 93 + }; 94 + } 95 + 74 96 endpointMetrics(period: "1h" | "1d" | "3d" | "7d" | "14d") { 75 97 const parameters = z.object({ monitorId: z.string() }); 76 98
+15
pnpm-lock.yaml
··· 262 262 '@t3-oss/env-nextjs': 263 263 specifier: 0.7.0 264 264 version: 0.7.0(typescript@5.4.2)(zod@3.22.2) 265 + '@tailwindcss/container-queries': 266 + specifier: 0.1.1 267 + version: 0.1.1(tailwindcss@3.3.2) 265 268 '@tailwindcss/typography': 266 269 specifier: 0.5.10 267 270 version: 0.5.10(tailwindcss@3.3.2) ··· 2206 2209 peerDependencies: 2207 2210 '@effect-ts/otel-node': '*' 2208 2211 peerDependenciesMeta: 2212 + '@effect-ts/core': 2213 + optional: true 2214 + '@effect-ts/otel': 2215 + optional: true 2209 2216 '@effect-ts/otel-node': 2210 2217 optional: true 2211 2218 dependencies: ··· 6397 6404 '@t3-oss/env-core': 0.7.0(typescript@5.4.2)(zod@3.22.2) 6398 6405 typescript: 5.4.2 6399 6406 zod: 3.22.2 6407 + dev: false 6408 + 6409 + /@tailwindcss/container-queries@0.1.1(tailwindcss@3.3.2): 6410 + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} 6411 + peerDependencies: 6412 + tailwindcss: '>=3.2.0' 6413 + dependencies: 6414 + tailwindcss: 3.3.2 6400 6415 dev: false 6401 6416 6402 6417 /@tailwindcss/typography@0.5.10(tailwindcss@3.3.2):