Openstatus www.openstatus.dev

chore: link email templates (#1208)

* fix: on delete cascade

* feat: status page form danger section

* chore: add badge to quick actions

* ci: apply automated fixes

* fix: export

* chore: link email templates

* fix: lint

* chore: remove workflows api route

* fix: int64

* fix: go

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
1af6c3eb 8412ca57

+453 -363
+6
apps/checker/handlers/checker.go
··· 202 202 Region: h.Region, 203 203 Message: res.Error, 204 204 CronTimestamp: req.CronTimestamp, 205 + Latency: res.Latency, 205 206 }) 206 207 } 207 208 // Check if the status is degraded ··· 212 213 Region: h.Region, 213 214 StatusCode: res.Status, 214 215 CronTimestamp: req.CronTimestamp, 216 + Latency: res.Latency, 215 217 }) 216 218 } 217 219 } ··· 225 227 Region: h.Region, 226 228 StatusCode: res.Status, 227 229 CronTimestamp: req.CronTimestamp, 230 + Latency: res.Latency, 228 231 }) 229 232 } 230 233 ··· 246 249 Region: h.Region, 247 250 StatusCode: res.Status, 248 251 CronTimestamp: req.CronTimestamp, 252 + Latency: res.Latency, 249 253 }) 250 254 } 251 255 } ··· 259 263 Region: h.Region, 260 264 StatusCode: res.Status, 261 265 CronTimestamp: req.CronTimestamp, 266 + Latency: res.Latency, 262 267 }) 263 268 } 264 269 ··· 269 274 Region: h.Region, 270 275 StatusCode: res.Status, 271 276 CronTimestamp: req.CronTimestamp, 277 + Latency: res.Latency, 272 278 }) 273 279 } 274 280 }
+4
apps/checker/handlers/tcp.go
··· 133 133 Status: "degraded", 134 134 Region: h.Region, 135 135 CronTimestamp: req.CronTimestamp, 136 + Latency: latency, 136 137 }) 137 138 } 138 139 ··· 142 143 Status: "active", 143 144 Region: h.Region, 144 145 CronTimestamp: req.CronTimestamp, 146 + Latency: latency, 145 147 }) 146 148 } 147 149 ··· 152 154 Status: "active", 153 155 Region: h.Region, 154 156 CronTimestamp: req.CronTimestamp, 157 + Latency: latency, 155 158 }) 156 159 } 157 160 ··· 161 164 Status: "degraded", 162 165 Region: h.Region, 163 166 CronTimestamp: req.CronTimestamp, 167 + Latency: latency, 164 168 }) 165 169 } 166 170
+1
apps/checker/update.go
··· 18 18 Region string `json:"region"` 19 19 CronTimestamp int64 `json:"cronTimestamp"` 20 20 StatusCode int `json:"statusCode,omitempty"` 21 + Latency int64 `json:"latency,omitempty"` 21 22 } 22 23 23 24 func UpdateStatus(ctx context.Context, updateData UpdateData) {
+1
apps/server/src/env.ts
··· 15 15 QSTASH_TOKEN: z.string(), 16 16 NODE_ENV: z.string().default("development"), 17 17 SUPER_ADMIN_TOKEN: z.string(), 18 + RESEND_API_KEY: z.string(), 18 19 }, 19 20 20 21 /**
+1 -1
apps/server/src/libs/cache/memory.ts
··· 48 48 } 49 49 } 50 50 51 - const cache = new MemoryCache(); 51 + const _cache = new MemoryCache();
+14 -1
apps/server/src/routes/checker/alerting.ts
··· 6 6 } from "@openstatus/db/src/schema"; 7 7 8 8 import { checkerAudit } from "@/utils/audit-log"; 9 - import type { MonitorFlyRegion } from "@openstatus/db/src/schema/constants"; 9 + import type { 10 + MonitorFlyRegion, 11 + Region, 12 + } from "@openstatus/db/src/schema/constants"; 10 13 import { Redis } from "@openstatus/upstash"; 11 14 import { providerToFunction } from "./utils"; 12 15 ··· 19 22 notifType, 20 23 cronTimestamp, 21 24 incidentId, 25 + region, 26 + latency, 22 27 }: { 23 28 monitorId: string; 24 29 statusCode?: number; ··· 26 31 notifType: "alert" | "recovery" | "degraded"; 27 32 cronTimestamp: number; 28 33 incidentId: string; 34 + region?: Region; 35 + latency?: number; 29 36 }) => { 30 37 console.log(`💌 triggerAlerting for ${monitorId}`); 31 38 const notifications = await db ··· 64 71 message, 65 72 incidentId, 66 73 cronTimestamp, 74 + region, 75 + latency, 67 76 }); 68 77 break; 69 78 case "recovery": ··· 74 83 message, 75 84 incidentId, 76 85 cronTimestamp, 86 + region, 87 + latency, 77 88 }); 78 89 break; 79 90 case "degraded": ··· 83 94 statusCode, 84 95 message, 85 96 cronTimestamp, 97 + region, 98 + latency, 86 99 }); 87 100 break; 88 101 }
+16 -2
apps/server/src/routes/checker/index.ts
··· 33 33 region: z.enum(flyRegions), 34 34 cronTimestamp: z.number(), 35 35 status: monitorStatusSchema, 36 + latency: z.number().optional(), 36 37 }); 37 38 38 39 const result = payloadSchema.safeParse(json); ··· 40 41 if (!result.success) { 41 42 return c.text("Unprocessable Entity", 422); 42 43 } 43 - const { monitorId, message, region, statusCode, cronTimestamp, status } = 44 - result.data; 44 + const { 45 + monitorId, 46 + message, 47 + region, 48 + statusCode, 49 + cronTimestamp, 50 + status, 51 + latency, 52 + } = result.data; 45 53 46 54 console.log(`📝 update monitor status ${JSON.stringify(result.data)}`); 47 55 ··· 103 111 notifType: "degraded", 104 112 cronTimestamp, 105 113 incidentId: `${cronTimestamp}`, 114 + region, 115 + latency, 106 116 }); 107 117 } 108 118 } ··· 182 192 incidentId: newIncident.length 183 193 ? String(newIncident[0]?.id) 184 194 : `${cronTimestamp}`, 195 + region, 196 + latency, 185 197 }); 186 198 187 199 if (newIncident.length > 0) { ··· 287 299 notifType: "recovery", 288 300 cronTimestamp, 289 301 incidentId: String(incident.id), 302 + region, 303 + latency, 290 304 }); 291 305 292 306 const monitor = await db
+5
apps/server/src/routes/checker/utils.ts
··· 30 30 sendAlert as sendPagerdutyAlert, 31 31 } from "@openstatus/notification-pagerduty"; 32 32 33 + import type { Region } from "@openstatus/db/src/schema/constants"; 33 34 import { 34 35 sendAlert as sendOpsGenieAlert, 35 36 sendDegraded as sendOpsGenieDegraded, ··· 43 44 message, 44 45 incidentId, 45 46 cronTimestamp, 47 + latency, 48 + region, 46 49 }: { 47 50 monitor: Monitor; 48 51 notification: Notification; ··· 50 53 message?: string; 51 54 incidentId?: string; 52 55 cronTimestamp: number; 56 + latency?: number; 57 + region?: Region; 53 58 }) => Promise<void>; 54 59 55 60 type Notif = {
+24 -15
apps/server/src/routes/v1/statusReportUpdates/post.ts
··· 7 7 statusReport, 8 8 statusReportUpdate, 9 9 } from "@openstatus/db/src/schema"; 10 - import { sendEmailHtml } from "@openstatus/emails"; 11 10 11 + import { env } from "@/env"; 12 12 import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 13 + import { EmailClient } from "@openstatus/emails"; 13 14 import type { statusReportUpdatesApi } from "./index"; 14 15 import { StatusReportUpdateSchema } from "./schema"; 16 + 17 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 15 18 16 19 const createStatusUpdate = createRoute({ 17 20 method: "post", ··· 89 92 ) 90 93 .all(); 91 94 92 - const pageInfo = await db 93 - .select() 94 - .from(page) 95 - .where(eq(page.id, _statusReport.pageId)) 96 - .get(); 97 - if (pageInfo) { 98 - const subscribersEmails = subscribers.map((subscriber) => ({ 99 - to: subscriber.email, 100 - subject: `New status update for ${pageInfo.title}`, 101 - html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 102 - `, 103 - from: "Notification OpenStatus <notification@notifications.openstatus.dev>", 104 - })); 95 + const _page = await db.query.page.findFirst({ 96 + where: eq(page.id, _statusReport.pageId), 97 + with: { 98 + monitorsToPages: { 99 + with: { 100 + monitor: true, 101 + }, 102 + }, 103 + }, 104 + }); 105 105 106 - await sendEmailHtml(subscribersEmails); 106 + if (_page && subscribers.length > 0) { 107 + await emailClient.sendStatusReportUpdate({ 108 + to: subscribers.map((subscriber) => subscriber.email), 109 + pageTitle: _page.title, 110 + reportTitle: _statusReport.title, 111 + status: _statusReport.status, 112 + message: _statusReportUpdate.message, 113 + date: _statusReportUpdate.date.toISOString(), 114 + monitors: _page.monitorsToPages.map((i) => i.monitor.name), 115 + }); 107 116 } 108 117 } 109 118
+23 -17
apps/server/src/routes/v1/statusReports/post.ts
··· 10 10 statusReportUpdate, 11 11 } from "@openstatus/db/src/schema"; 12 12 13 + import { env } from "@/env"; 13 14 import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 14 - import { sendBatchEmailHtml } from "@openstatus/emails/src/send"; 15 + import { EmailClient } from "@openstatus/emails"; 15 16 import type { statusReportsApi } from "./index"; 16 17 import { StatusReportSchema } from "./schema"; 18 + 19 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 17 20 18 21 const postRoute = createRoute({ 19 22 method: "post", ··· 142 145 ) 143 146 .all(); 144 147 145 - const pageInfo = await db 146 - .select() 147 - .from(page) 148 - .where(eq(page.id, _newStatusReport.pageId)) 149 - .get(); 148 + const pageInfo = await db.query.page.findFirst({ 149 + where: eq(page.id, _newStatusReport.pageId), 150 + with: { 151 + monitorsToPages: { 152 + with: { 153 + monitor: true, 154 + }, 155 + }, 156 + }, 157 + }); 150 158 151 - if (pageInfo) { 152 - const emails = subscribers.map((subscriber) => { 153 - return { 154 - to: subscriber.email, 155 - subject: `New status update for ${pageInfo.title}`, 156 - html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 157 - `, 158 - from: "Notification OpenStatus <notification@notifications.openstatus.dev>", 159 - }; 159 + if (pageInfo && subscribers.length > 0) { 160 + await emailClient.sendStatusReportUpdate({ 161 + to: subscribers.map((subscriber) => subscriber.email), 162 + pageTitle: pageInfo.title, 163 + reportTitle: _newStatusReport.title, 164 + status: _newStatusReport.status, 165 + message: _newStatusReportUpdate.message, 166 + date: _newStatusReportUpdate.date.toISOString(), 167 + monitors: pageInfo.monitorsToPages.map((i) => i.monitor.name), 160 168 }); 161 - 162 - await sendBatchEmailHtml(emails); 163 169 } 164 170 } 165 171
+29 -15
apps/server/src/routes/v1/statusReports/update/post.ts
··· 1 + import { env } from "@/env"; 1 2 import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 2 3 import { createRoute } from "@hono/zod-openapi"; 3 4 import { and, db, eq, isNotNull } from "@openstatus/db"; 4 5 import { 6 + monitor, 5 7 page, 6 8 pageSubscriber, 7 9 statusReport, 8 10 statusReportUpdate, 9 11 } from "@openstatus/db/src/schema"; 12 + import { EmailClient } from "@openstatus/emails/src/client"; 10 13 import { sendBatchEmailHtml } from "@openstatus/emails/src/send"; 11 14 import { StatusReportUpdateSchema } from "../../statusReportUpdates/schema"; 12 15 import type { statusReportsApi } from "../index"; 13 16 import { ParamsSchema, StatusReportSchema } from "../schema"; 17 + 18 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 14 19 15 20 const postRouteUpdate = createRoute({ 16 21 method: "post", ··· 82 87 .get(); 83 88 84 89 if (limits.notifications && _statusReport.pageId) { 90 + const allInfo = await db.query.statusReport.findFirst({ 91 + where: eq(statusReport.id, Number(id)), 92 + with: { 93 + monitorsToStatusReports: { 94 + with: { 95 + monitor: true, 96 + }, 97 + }, 98 + page: true, 99 + }, 100 + }); 101 + 85 102 const subscribers = await db 86 103 .select() 87 104 .from(pageSubscriber) ··· 92 109 ), 93 110 ) 94 111 .all(); 95 - const pageInfo = await db 96 - .select() 97 - .from(page) 98 - .where(eq(page.id, _statusReport.pageId)) 99 - .get(); 100 - if (pageInfo) { 101 - const subscribersEmails = subscribers.map((subscriber) => { 102 - return { 103 - to: subscriber.email, 104 - subject: `New status update for ${pageInfo.title}`, 105 - html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 106 - `, 107 - from: "Notification OpenStatus <notification@notifications.openstatus.dev>", 108 - }; 112 + 113 + if (allInfo?.page) { 114 + await emailClient.sendStatusReportUpdate({ 115 + to: subscribers.map((subscriber) => subscriber.email), 116 + pageTitle: allInfo.page.title, 117 + reportTitle: allInfo.title, 118 + status: allInfo.status, 119 + message: _statusReportUpdate.message, 120 + date: _statusReportUpdate.date.toISOString(), 121 + monitors: allInfo.monitorsToStatusReports.map( 122 + (monitor) => monitor.monitor.name, 123 + ), 109 124 }); 110 - await sendBatchEmailHtml(subscribersEmails); 111 125 } 112 126 } 113 127
+1 -1
apps/server/src/routes/v1/utils.ts
··· 7 7 return new Date(String(val)).toISOString(); 8 8 } 9 9 return new Date().toISOString(); 10 - } catch (e) { 10 + } catch (_e) { 11 11 throw new ZodError([ 12 12 { code: "invalid_date", message: "Invalid date", path: [] }, 13 13 ]);
+1 -1
apps/web/src/app/api/checker/cron/10m/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "10m", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+1 -1
apps/web/src/app/api/checker/cron/1h/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "1h", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+1 -1
apps/web/src/app/api/checker/cron/1m/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "1m", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+1 -1
apps/web/src/app/api/checker/cron/30m/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "30m", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+1 -1
apps/web/src/app/api/checker/cron/30s/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "30s", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+1 -1
apps/web/src/app/api/checker/cron/5m/route.ts
··· 16 16 try { 17 17 await cron({ periodicity: "5m", req }); 18 18 await cronCompleted(); 19 - } catch (error) { 19 + } catch (_error) { 20 20 await cronFailed(); 21 21 } 22 22 }
+2 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/team/_components/invite-button.tsx
··· 50 50 async function onSubmit(data: Schema) { 51 51 startTransition(async () => { 52 52 try { 53 - api.invitation.create.mutate(data); 53 + const invitation = await api.invitation.create.mutate(data); 54 + await api.emailRouter.sendTeamInvitation.mutate({ id: invitation.id }); 54 55 toastAction("saved"); 55 56 router.refresh(); 56 57 } catch {
+2 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/(overview)/_components/status-report-button.tsx
··· 33 33 "mt-2", 34 34 )} 35 35 > 36 - <Megaphone className="h-4 w-4 mr-1 pb-0.5" /> 36 + <Megaphone className="mr-1 h-4 w-4 pb-0.5" /> 37 37 New Status Report 38 - <span className="h-8 w-px bg-background mx-2" /> 38 + <span className="mx-2 h-8 w-px bg-background" /> 39 39 <ChevronDown className="h-4 w-4" /> 40 40 </DropdownMenuTrigger> 41 41 <DropdownMenuContent align="end">
+9 -10
apps/web/src/app/status-page/[domain]/_components/actions.ts
··· 2 2 3 3 import { z } from "zod"; 4 4 5 + import { env } from "@/env"; 5 6 import { Events, setupAnalytics } from "@openstatus/analytics"; 6 7 import { and, eq, sql } from "@openstatus/db"; 7 8 import { db } from "@openstatus/db/src/db"; 8 9 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 9 - import { SubscribeEmail, sendEmail } from "@openstatus/emails"; 10 + import { EmailClient } from "@openstatus/emails"; 11 + 12 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 10 13 11 14 const subscribeSchema = z.object({ 12 15 email: z ··· 76 79 }) 77 80 .execute(); 78 81 79 - await sendEmail({ 80 - react: SubscribeEmail({ 81 - domain: pageData.slug, 82 - token: token, 83 - page: pageData.title, 84 - }), 85 - from: "OpenStatus <notification@notifications.openstatus.dev>", 86 - to: [validatedFields.data.email], 87 - subject: `Verify your subscription to ${pageData.title}`, 82 + await emailClient.sendPageSubscription({ 83 + domain: pageData.slug, 84 + token, 85 + page: pageData.title, 86 + to: validatedFields.data.email, 88 87 }); 89 88 90 89 const analytics = await setupAnalytics({});
+1 -1
apps/web/src/app/status-page/[domain]/_components/subscribe-button.tsx
··· 76 76 ) : null} 77 77 {isSubscribers ? ( 78 78 <DropdownMenuItem onClick={() => setShowModal(true)}> 79 - <Mail className="h-4 w-4 mr-2" /> 79 + <Mail className="mr-2 h-4 w-4" /> 80 80 Email 81 81 </DropdownMenuItem> 82 82 ) : null}
+1 -1
apps/web/src/components/content/article.tsx
··· 40 40 > 41 41 {post.author.name} 42 42 </Link> 43 - <div className="flex items-center gap-1.5 flex-wrap"> 43 + <div className="flex flex-wrap items-center gap-1.5"> 44 44 <time className="font-mono">{formatDate(post.publishedAt)}</time> 45 45 <span className="text-muted-foreground/70">&bull;</span> 46 46 <span className="font-mono">{post.readingTime}</span>
+1 -1
apps/web/src/components/content/changelog.tsx
··· 6 6 7 7 export function ChangelogCard({ post }: { post: Changelog }) { 8 8 return ( 9 - <article className="relative mx-auto flex max-w-prose w-full flex-col gap-8"> 9 + <article className="relative mx-auto flex w-full max-w-prose flex-col gap-8"> 10 10 <div className="grid w-full gap-3"> 11 11 <p className="font-mono text-muted-foreground text-sm"> 12 12 {formatDate(new Date(post.publishedAt))}
+1 -1
apps/web/src/components/content/mdx.tsx
··· 13 13 // FIXME: weird behaviour when `prose-headings:font-cal` and on mouse movement font gets bigger 14 14 <div 15 15 className={cn( 16 - "prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:bg-background prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-headings:font-cal prose-headings:font-normal prose-blockquote:font-light prose-blockquote:border-l-2", 16 + "prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-blockquote:border-l-2 prose-pre:bg-background prose-blockquote:font-light prose-headings:font-cal prose-headings:font-normal", 17 17 className, 18 18 )} 19 19 >
+1 -1
apps/web/src/components/data-table/status-page/columns.tsx
··· 112 112 <span className="group-hover:text-muted-foreground/70"> 113 113 {formatDate(date)} 114 114 </span> 115 - <div className="absolute -inset-x-2 -inset-y-1 invisible group-hover:visible backdrop-blur-sm flex items-center px-2 py-1"> 115 + <div className="-inset-x-2 -inset-y-1 invisible absolute flex items-center px-2 py-1 backdrop-blur-sm group-hover:visible"> 116 116 <Link 117 117 href={`./status-pages/${row.original.id}/reports/${lastReport.id}`} 118 118 className="hover:underline"
+2 -2
apps/web/src/components/data-table/status-page/data-table-row-actions.tsx
··· 150 150 Create an uptime badge for your status page. 151 151 </DialogDescription> 152 152 <div className="flex items-center justify-center"> 153 - <div className="flex items-center justify-center p-4 border rounded-md w-full"> 153 + <div className="flex w-full items-center justify-center rounded-md border p-4"> 154 154 <img 155 155 src={`/status-page/${page.slug}/badge?size=${size}&theme=${theme}`} 156 156 alt="Badge" ··· 175 175 ))} 176 176 </RadioGroup> 177 177 <div> 178 - <span className="text-sm text-muted-foreground font-mono"> 178 + <span className="font-mono text-muted-foreground text-sm"> 179 179 {SIZE[size].width}x{SIZE[size].height} 180 180 </span> 181 181 </div>
+11 -2
apps/web/src/components/forms/status-report-update/form.tsx
··· 50 50 startTransition(async () => { 51 51 try { 52 52 if (defaultValues) { 53 - await api.statusReport.updateStatusReportUpdate.mutate({ ...props }); 53 + await api.statusReport.updateStatusReportUpdate.mutate({ 54 + ...props, 55 + }); 54 56 } else { 55 - await api.statusReport.createStatusReportUpdate.mutate({ ...props }); 57 + const statusReportUpdate = 58 + await api.statusReport.createStatusReportUpdate.mutate({ 59 + ...props, 60 + }); 61 + if (!statusReportUpdate) return; 62 + await api.emailRouter.sendStatusReport.mutate({ 63 + id: statusReportUpdate.id, 64 + }); 56 65 } 57 66 toastAction("saved"); 58 67 onSubmit?.();
+13 -6
apps/web/src/components/forms/status-report/form.tsx
··· 82 82 ); 83 83 // include update on creation 84 84 if (statusReport?.id) { 85 - await api.statusReport.createStatusReportUpdate.mutate({ 86 - message, 87 - date, 88 - status, 89 - statusReportId: statusReport.id, 90 - }); 85 + const statusReportUpdate = 86 + await api.statusReport.createStatusReportUpdate.mutate({ 87 + message, 88 + date, 89 + status, 90 + statusReportId: statusReport.id, 91 + }); 92 + 93 + if (statusReportUpdate) { 94 + await api.emailRouter.sendStatusReport.mutate({ 95 + id: statusReportUpdate.id, 96 + }); 97 + } 91 98 } 92 99 } 93 100 if (nextUrl) {
+1 -1
apps/web/src/trpc/shared.ts
··· 11 11 return "http://localhost:3000"; 12 12 }; 13 13 14 - const lambdas = ["stripeRouter", "rumRouter"]; 14 + const lambdas = ["stripeRouter", "emailRouter"]; 15 15 16 16 export const endingLink = (opts?: { 17 17 headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);
+2 -1
packages/api/src/lambda.ts
··· 1 + import { emailRouter } from "./router/email"; 1 2 import { stripeRouter } from "./router/stripe"; 2 3 import { createTRPCRouter } from "./trpc"; 3 - 4 4 // Deployed to /trpc/lambda/** 5 5 export const lambdaRouter = createTRPCRouter({ 6 6 stripeRouter: stripeRouter, 7 + emailRouter: emailRouter, 7 8 }); 8 9 9 10 export { stripe } from "./router/stripe/shared";
+88
packages/api/src/router/email/index.ts
··· 1 + import { z } from "zod"; 2 + 3 + import { and, eq, isNotNull } from "@openstatus/db"; 4 + import { 5 + invitation, 6 + pageSubscriber, 7 + statusReportUpdate, 8 + } from "@openstatus/db/src/schema"; 9 + import { EmailClient } from "@openstatus/emails"; 10 + import { env } from "../../env"; 11 + import { createTRPCRouter, protectedProcedure } from "../../trpc"; 12 + 13 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 14 + 15 + export const emailRouter = createTRPCRouter({ 16 + sendStatusReport: protectedProcedure 17 + .input(z.object({ id: z.number() })) 18 + .mutation(async (opts) => { 19 + const limits = opts.ctx.workspace.limits; 20 + 21 + if (limits["status-subscribers"]) { 22 + const _statusReportUpdate = 23 + await opts.ctx.db.query.statusReportUpdate.findFirst({ 24 + where: eq(statusReportUpdate.id, opts.input.id), 25 + with: { 26 + statusReport: { 27 + with: { 28 + page: { 29 + with: { 30 + pageSubscribers: { 31 + where: isNotNull(pageSubscriber.acceptedAt), 32 + }, 33 + monitorsToPages: { 34 + with: { 35 + monitor: true, 36 + }, 37 + }, 38 + }, 39 + }, 40 + }, 41 + }, 42 + }, 43 + }); 44 + 45 + if (!_statusReportUpdate) return; 46 + if (!_statusReportUpdate.statusReport.page) return; 47 + if (!_statusReportUpdate.statusReport.page.pageSubscribers.length) 48 + return; 49 + 50 + await emailClient.sendStatusReportUpdate({ 51 + to: _statusReportUpdate.statusReport.page.pageSubscribers.map( 52 + (subscriber) => subscriber.email, 53 + ), 54 + pageTitle: _statusReportUpdate.statusReport.page.title, 55 + reportTitle: _statusReportUpdate.statusReport.title, 56 + status: _statusReportUpdate.status, 57 + message: _statusReportUpdate.message, 58 + date: new Date(_statusReportUpdate.date).toISOString(), 59 + monitors: _statusReportUpdate.statusReport.page.monitorsToPages.map( 60 + (i) => i.monitor.name, 61 + ), 62 + }); 63 + } 64 + }), 65 + sendTeamInvitation: protectedProcedure 66 + .input(z.object({ id: z.number() })) 67 + .mutation(async (opts) => { 68 + const limits = opts.ctx.workspace.limits; 69 + 70 + if (limits.members === "Unlimited" || limits.members > 1) { 71 + const _invitation = await opts.ctx.db.query.invitation.findFirst({ 72 + where: and( 73 + eq(invitation.id, opts.input.id), 74 + eq(invitation.workspaceId, opts.ctx.workspace.id), 75 + ), 76 + }); 77 + 78 + if (!_invitation) return; 79 + 80 + await emailClient.sendTeamInvitation({ 81 + to: _invitation.email, 82 + token: _invitation.token, 83 + invitedBy: `${opts.ctx.user.email}`, 84 + workspaceName: opts.ctx.workspace.name || "OpenStatus", 85 + }); 86 + } 87 + }), 88 + });
-24
packages/api/src/router/invitation.ts
··· 62 62 console.log( 63 63 `>>>> Invitation token: http://localhost:3000/app/invite?token=${token} <<<< `, 64 64 ); 65 - } else { 66 - await fetch("https://api.resend.com/emails", { 67 - method: "POST", 68 - headers: { 69 - "Content-Type": "application/json", 70 - Authorization: `Bearer ${process.env.RESEND_API_KEY}`, 71 - }, 72 - body: JSON.stringify({ 73 - to: email, 74 - from: "OpenStatus <ping@openstatus.dev>", 75 - subject: "You have been invited to join OpenStatus.dev", 76 - html: `<p>You have been invited by ${opts.ctx.user.email} ${ 77 - opts.ctx.workspace.name 78 - ? `to join the workspace '${opts.ctx.workspace.name}'.` 79 - : "to join a workspace." 80 - }</p> 81 - <br> 82 - <p>Click here to access the workspace: <a href='https://openstatus.dev/app/invite?token=${ 83 - _invitation.token 84 - }'>accept invitation</a>.</p> 85 - <p>If you don't have an account yet, it will require you to create one.</p> 86 - `, 87 - }), 88 - }); 89 65 } 90 66 91 67 return _invitation;
+5 -45
packages/api/src/router/statusReport.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { and, eq, inArray, isNotNull, sql } from "@openstatus/db"; 3 + import { and, eq, inArray, sql } from "@openstatus/db"; 4 4 import { 5 5 insertStatusReportSchema, 6 6 insertStatusReportUpdateSchema, 7 7 monitorsToStatusReport, 8 8 page, 9 - pageSubscriber, 10 9 selectMonitorSchema, 11 10 selectPublicStatusReportSchemaWithRelation, 12 11 selectStatusReportSchema, ··· 14 13 statusReport, 15 14 statusReportStatusSchema, 16 15 statusReportUpdate, 17 - workspace, 18 16 } from "@openstatus/db/src/schema"; 19 - import { sendBatchEmailHtml } from "@openstatus/emails/src/send"; 20 17 21 18 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 22 19 ··· 67 64 .returning() 68 65 .get(); 69 66 70 - const { id, ...statusReportUpdateInput } = opts.input; 67 + if (!_statusReport) return; 71 68 72 - // Send email 69 + const { id, ...statusReportUpdateInput } = opts.input; 73 70 74 71 const updatedValue = await opts.ctx.db 75 72 .insert(statusReportUpdate) ··· 77 74 .returning() 78 75 .get(); 79 76 80 - const currentWorkspace = await opts.ctx.db 81 - .select() 82 - .from(workspace) 83 - .where(eq(workspace.id, opts.ctx.workspace.id)) 84 - .get(); 85 - if (currentWorkspace?.plan !== "pro" && _statusReport.pageId) { 86 - const subscribers = await opts.ctx.db 87 - .select() 88 - .from(pageSubscriber) 89 - .where( 90 - and( 91 - eq(pageSubscriber.pageId, _statusReport.pageId), 92 - isNotNull(pageSubscriber.acceptedAt), 93 - ), 94 - ) 95 - .all(); 96 - const pageInfo = await opts.ctx.db 97 - .select() 98 - .from(page) 99 - .where(eq(page.id, _statusReport.pageId)) 100 - .get(); 101 - if (pageInfo) { 102 - const emails = subscribers.map((subscriber) => { 103 - return { 104 - to: subscriber.email, 105 - 106 - subject: `New status update for ${pageInfo.title}`, 107 - html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${updatedValue.status}</p><p>${updatedValue.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 108 - `, 109 - from: "Notification OpenStatus <notification@notifications.openstatus.dev>", 110 - }; 111 - }); 112 - if (emails.length > 0) { 113 - await sendBatchEmailHtml(emails); 114 - } 115 - } 116 - } 117 - return updatedValue; 77 + return selectStatusReportUpdateSchema.parse(updatedValue); 118 78 }), 119 79 120 80 updateStatusReport: protectedProcedure ··· 195 155 .returning() 196 156 .get(); 197 157 198 - return currentStatusReportUpdate; 158 + return selectStatusReportUpdateSchema.parse(currentStatusReportUpdate); 199 159 }), 200 160 201 161 deleteStatusReport: protectedProcedure
+2
packages/db/src/schema/pages/page.ts
··· 3 3 4 4 import { maintenance } from "../maintenances"; 5 5 import { monitorsToPages } from "../monitors"; 6 + import { pageSubscriber } from "../page_subscribers"; 6 7 import { statusReport } from "../status_reports"; 7 8 import { workspace } from "../workspaces"; 8 9 ··· 49 50 fields: [page.workspaceId], 50 51 references: [workspace.id], 51 52 }), 53 + pageSubscribers: many(pageSubscriber), 52 54 }));
+1 -1
packages/db/src/schema/status_reports/status_reports.ts
··· 37 37 export const statusReportUpdate = sqliteTable("status_report_update", { 38 38 id: integer("id").primaryKey(), 39 39 40 - status: text("status", statusReportStatus).notNull(), 40 + status: text("status", { enum: statusReportStatus }).notNull(), 41 41 date: integer("date", { mode: "timestamp" }).notNull(), 42 42 message: text("message").notNull(), 43 43
+33 -27
packages/emails/emails/monitor-alert.tsx
··· 20 20 import { colors, styles } from "./_components/styles"; 21 21 22 22 const MonitorAlertSchema = z.object({ 23 - type: z.enum(["degraded", "up", "down"]), 23 + type: z.enum(["degraded", "alert", "recovery"]), 24 24 name: z.string().optional(), 25 25 url: z.string().optional(), 26 26 method: z.string().optional(), 27 27 status: z.string().optional(), 28 28 latency: z.string().optional(), 29 - location: z.string().optional(), 29 + region: z.string().optional(), 30 30 timestamp: z.string().optional(), 31 + message: z.string().optional(), 31 32 }); 32 33 33 34 export type MonitorAlertProps = z.infer<typeof MonitorAlertSchema>; ··· 37 38 color: string; 38 39 } { 39 40 switch (type) { 40 - case "up": 41 + case "recovery": 41 42 return { 42 43 src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNoZWNrIj48cGF0aCBkPSJNMjAgNiA5IDE3bC01LTUiLz48L3N2Zz4=", 43 44 color: colors.success, 44 45 }; 45 - case "down": 46 + case "alert": 46 47 return { 47 48 src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXgiPjxwYXRoIGQ9Ik0xOCA2IDYgMTgiLz48cGF0aCBkPSJtNiA2IDEyIDEyIi8+PC9zdmc+", 48 49 color: colors.danger, ··· 58 59 const MonitorAlertEmail = (props: MonitorAlertProps) => ( 59 60 <Html> 60 61 <Head /> 61 - <Preview> 62 - A fine-grained personal access token has been added to your account 63 - </Preview> 62 + <Preview>Your monitor's status is: {props.type}</Preview> 64 63 <Body style={styles.main}> 65 64 <Layout> 66 65 <Container ··· 108 107 </Text> 109 108 </Column> 110 109 </Row> 111 - <Row style={styles.row}> 112 - <Column> 113 - <Text style={styles.bold}>Status</Text> 114 - </Column> 115 - <Column style={{ textAlign: "right" }}> 116 - <Text>{props.status}</Text> 117 - </Column> 118 - </Row> 119 - <Row style={styles.row}> 120 - <Column> 121 - <Text style={styles.bold}>Latency</Text> 122 - </Column> 123 - <Column style={{ textAlign: "right" }}> 124 - <Text>{props.latency}</Text> 125 - </Column> 126 - </Row> 110 + {/* REMINDER: no status code for TCP monitors */} 111 + {props.status ? ( 112 + <Row style={styles.row}> 113 + <Column> 114 + <Text style={styles.bold}>Status</Text> 115 + </Column> 116 + <Column style={{ textAlign: "right" }}> 117 + <Text>{props.status}</Text> 118 + </Column> 119 + </Row> 120 + ) : null} 127 121 <Row style={styles.row}> 128 122 <Column> 129 - <Text style={styles.bold}>Location</Text> 123 + <Text style={styles.bold}>Region</Text> 130 124 </Column> 131 125 <Column style={{ textAlign: "right" }}> 132 - <Text>{props.location}</Text> 126 + <Text>{props.region}</Text> 133 127 </Column> 134 128 </Row> 135 129 <Row style={styles.row}> ··· 140 134 <Text>{props.timestamp}</Text> 141 135 </Column> 142 136 </Row> 137 + {props.message ? ( 138 + <Row style={styles.row}> 139 + <Column> 140 + <Text> 141 + {props.message?.slice(0, 200)} 142 + {props.message?.length > 200 ? "..." : ""} 143 + </Text> 144 + </Column> 145 + </Row> 146 + ) : null} 143 147 <Row style={styles.row}> 144 148 <Column> 145 149 <Text style={{ textAlign: "center" }}> ··· 155 159 ); 156 160 157 161 MonitorAlertEmail.PreviewProps = { 158 - type: "up", 162 + type: "alert", 159 163 name: "Ping Pong", 160 164 url: "https://openstatus.dev/ping", 161 165 method: "GET", 162 166 status: "200", 163 167 latency: "300ms", 164 - location: "San Francisco", 168 + region: "Amsterdam, Netherlands", 165 169 timestamp: "2021-10-13T17:29:00Z", 170 + message: 171 + "This is a very long test message that will be truncated. Just testing it for the preview. Veryy veryy long message.", 166 172 } satisfies MonitorAlertProps; 167 173 168 174 export default MonitorAlertEmail;
+3 -2
packages/emails/emails/status-report.tsx
··· 16 16 17 17 export const StatusReportSchema = z.object({ 18 18 pageTitle: z.string(), 19 + // statusReportStatus from db 19 20 status: z.enum(["investigating", "identified", "monitoring", "resolved"]), 20 21 date: z.string(), 21 22 message: z.string(), ··· 91 92 </Column> 92 93 <Column style={{ textAlign: "right" }}> 93 94 <Text style={{ flexWrap: "wrap", wordWrap: "break-word" }}> 94 - {monitors.join(", ")} 95 + {monitors.length > 0 ? monitors.join(", ") : "N/A"} 95 96 </Text> 96 97 </Column> 97 98 </Row> ··· 112 113 pageTitle: "OpenStatus Status", 113 114 reportTitle: "API Unavaible", 114 115 status: "investigating", 115 - date: "2021-07-19", 116 + date: new Date().toISOString(), 116 117 message: 117 118 "The API is down, including the webhook. We are actively investigating the issue and will provide updates as soon as possible.", 118 119 monitors: ["OpenStatus API", "OpenStatus Webhook"],
+16 -11
packages/emails/src/client.tsx
··· 42 42 } 43 43 } 44 44 45 - public async sendStatusReportUpdate(req: StatusReportProps & { to: string }) { 45 + public async sendStatusReportUpdate( 46 + req: StatusReportProps & { to: string[] }, 47 + ) { 46 48 if (process.env.NODE_ENV === "development") return; 47 49 48 50 try { 49 51 const html = await render(<StatusReportEmail {...req} />); 50 - const result = await this.client.emails.send({ 51 - from: `${req.pageTitle} <notifications@openstatus.dev>`, 52 - subject: req.reportTitle, 53 - to: req.to, 54 - html, 55 - }); 52 + const result = await this.client.batch.send( 53 + req.to.map((subscriber) => ({ 54 + from: `${req.pageTitle} <notifications@openstatus.dev>`, 55 + subject: req.reportTitle, 56 + to: subscriber, 57 + html, 58 + })), 59 + ); 56 60 57 61 if (!result.error) { 58 62 console.log(`Sent status report update email to ${req.to}`); ··· 62 66 throw result.error; 63 67 } catch (err) { 64 68 console.error( 65 - `Error sending status report update email to ${req.to}: ${err}`, 69 + `Error sending status report update email to ${req.to}`, 70 + err, 66 71 ); 67 72 } 68 73 } ··· 86 91 87 92 throw result.error; 88 93 } catch (err) { 89 - console.error(`Error sending team invitation email to ${req.to}: ${err}`); 94 + console.error(`Error sending team invitation email to ${req.to}`, err); 90 95 } 91 96 } 92 97 ··· 109 114 110 115 throw result.error; 111 116 } catch (err) { 112 - console.error(`Error sending monitor alert to ${req.to}: ${err}`); 117 + console.error(`Error sending monitor alert to ${req.to}`, err); 113 118 } 114 119 } 115 120 ··· 134 139 135 140 throw result.error; 136 141 } catch (err) { 137 - console.error(`Error sending page subscription to ${req.to}: ${err}`); 142 + console.error(`Error sending page subscription to ${req.to}`, err); 138 143 } 139 144 } 140 145 }
+7 -1
packages/notifications/discord/src/index.ts
··· 1 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import type { Region } from "@openstatus/db/src/schema/constants"; 2 3 import { DataSchema } from "./schema"; 3 - 4 4 const postToWebhook = async (content: string, webhookUrl: string) => { 5 5 await fetch(webhookUrl, { 6 6 method: "POST", ··· 29 29 message?: string; 30 30 incidentId?: string; 31 31 cronTimestamp: number; 32 + latency?: number; 33 + region?: Region; 32 34 }) => { 33 35 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 34 36 const { discord: webhookUrl } = notificationData; // webhook url ··· 61 63 message?: string; 62 64 incidentId?: string; 63 65 cronTimestamp: number; 66 + latency?: number; 67 + region?: Region; 64 68 }) => { 65 69 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 66 70 const { discord: webhookUrl } = notificationData; // webhook url ··· 93 97 message?: string; 94 98 incidentId?: string; 95 99 cronTimestamp: number; 100 + latency?: number; 101 + region?: Region; 96 102 }) => { 97 103 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 98 104 const { discord: webhookUrl } = notificationData; // webhook url
+1
packages/notifications/email/package.json
··· 7 7 "@openstatus/db": "workspace:*", 8 8 "@openstatus/emails": "workspace:*", 9 9 "@openstatus/tinybird": "workspace:*", 10 + "@openstatus/utils": "workspace:*", 10 11 "@react-email/components": "0.0.31", 11 12 "@react-email/render": "1.0.3", 12 13 "@t3-oss/env-core": "0.7.0",
+46 -72
packages/notifications/email/src/index.ts
··· 4 4 emailDataSchema, 5 5 } from "@openstatus/db/src/schema"; 6 6 7 + import type { Region } from "@openstatus/db/src/schema/constants"; 8 + import { EmailClient } from "@openstatus/emails/src/client"; 9 + import { flyRegionsDict } from "@openstatus/utils"; 7 10 import { env } from "../env"; 8 11 9 - async function send({ 10 - subject, 11 - html, 12 - email, 13 - id, 14 - type, 15 - }: { 16 - subject: string; 17 - html: string; 18 - email: string; 19 - id: number; 20 - type: "recovered" | "alert" | "degraded"; 21 - }) { 22 - const res = await fetch("https://api.resend.com/emails", { 23 - method: "POST", 24 - headers: { 25 - "Content-Type": "application/json", 26 - Authorization: `Bearer ${env.RESEND_API_KEY}`, 27 - }, 28 - body: JSON.stringify({ 29 - to: email, 30 - from: "Notifications <ping@openstatus.dev>", 31 - subject, 32 - html, 33 - }), 34 - }); 35 - 36 - if (res.ok) { 37 - const data = await res.json(); 38 - console.log(data); 39 - // return NextResponse.json(data); 40 - } 41 - if (!res.ok) { 42 - console.log(`Error sending ${type} email ${id}`); 43 - } 44 - } 12 + const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 45 13 46 14 export const sendAlert = async ({ 47 15 monitor, ··· 49 17 statusCode, 50 18 message, 51 19 cronTimestamp, 20 + latency, 21 + region, 52 22 }: { 53 23 monitor: Monitor; 54 24 notification: Notification; ··· 56 26 message?: string; 57 27 incidentId?: string; 58 28 cronTimestamp: number; 29 + region?: Region; 30 + latency?: number; 59 31 }) => { 60 32 const config = emailDataSchema.safeParse(JSON.parse(notification.data)); 61 33 62 34 if (!config.success) return; 63 35 64 - await send({ 65 - id: monitor.id, 36 + await emailClient.sendMonitorAlert({ 37 + name: monitor.name, 66 38 type: "alert", 67 - email: config.data.email, 68 - subject: `🚨 Alert ${monitor.name}`, 69 - html: ` 70 - <p>Hi,</p> 71 - <p>Your monitor <strong>${monitor.name}</strong> is down.</p> 72 - <p>URL: ${monitor.url}</p> 73 - ${ 74 - statusCode 75 - ? `<p>Status Code: ${statusCode}</p>` 76 - : `<p>Error message: ${message}</p>` 77 - } 78 - <p>Cron Timestamp: ${cronTimestamp} (${new Date(cronTimestamp).toISOString()})</p> 79 - <p>OpenStatus 🏓</p>`, 39 + to: config.data.email, 40 + url: monitor.url, 41 + status: statusCode?.toString(), 42 + latency: latency ? `${latency}ms` : "N/A", 43 + region: region ? flyRegionsDict[region].location : "N/A", 44 + timestamp: new Date(cronTimestamp).toISOString(), 45 + message, 80 46 }); 81 47 }; 82 48 83 49 export const sendRecovery = async ({ 84 50 monitor, 85 51 notification, 52 + statusCode, 53 + cronTimestamp, 54 + region, 55 + latency, 86 56 }: { 87 57 monitor: Monitor; 88 58 notification: Notification; ··· 90 60 message?: string; 91 61 incidentId?: string; 92 62 cronTimestamp: number; 63 + region?: Region; 64 + latency?: number; 93 65 }) => { 94 66 const config = emailDataSchema.safeParse(JSON.parse(notification.data)); 95 67 96 68 if (!config.success) return; 97 69 98 - send({ 99 - id: monitor.id, 100 - type: "recovered", 101 - email: config.data.email, 102 - subject: `✅ Recovered ${monitor.name}`, 103 - html: ` 104 - <p>Hi,</p> 105 - <p>Your monitor <strong>${monitor.name}</strong> is up again.</p> 106 - <p>URL: ${monitor.url}</p> 107 - <p>OpenStatus 🏓</p> 108 - `, 70 + await emailClient.sendMonitorAlert({ 71 + name: monitor.name, 72 + type: "recovery", 73 + to: config.data.email, 74 + url: monitor.url, 75 + status: statusCode?.toString(), 76 + latency: latency ? `${latency}ms` : "N/A", 77 + region: region ?? "N/A", 78 + timestamp: new Date(cronTimestamp).toISOString(), 109 79 }); 110 80 }; 111 81 112 82 export const sendDegraded = async ({ 113 83 monitor, 114 84 notification, 85 + statusCode, 86 + cronTimestamp, 87 + region, 88 + latency, 115 89 }: { 116 90 monitor: Monitor; 117 91 notification: Notification; 118 92 statusCode?: number; 119 93 message?: string; 120 94 cronTimestamp: number; 95 + region?: Region; 96 + latency?: number; 121 97 }) => { 122 98 const config = emailDataSchema.safeParse(JSON.parse(notification.data)); 123 99 124 100 if (!config.success) return; 125 101 126 - send({ 127 - id: monitor.id, 102 + await emailClient.sendMonitorAlert({ 103 + name: monitor.name, 128 104 type: "degraded", 129 - email: config.data.email, 130 - subject: `⚠️ Degraded ${monitor.name}`, 131 - html: ` 132 - <p>Hi,</p> 133 - <p>Your monitor <strong>${monitor.name}</strong> is taking longer than expected to respond</p> 134 - <p>URL: ${monitor.url}</p> 135 - <p>OpenStatus 🏓</p> 136 - `, 105 + to: config.data.email, 106 + url: monitor.url, 107 + status: statusCode?.toString(), 108 + latency: latency ? `${latency}ms` : "N/A", 109 + region: region ?? "N/A", 110 + timestamp: new Date(cronTimestamp).toISOString(), 137 111 }); 138 112 };
+7
packages/notifications/opsgenie/src/index.ts
··· 1 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import type { Region } from "@openstatus/db/src/schema/constants"; 2 3 import { OpsGeniePayloadAlert, OpsGenieSchema } from "./schema"; 3 4 4 5 export const sendAlert = async ({ ··· 15 16 message?: string; 16 17 incidentId?: string; 17 18 cronTimestamp: number; 19 + latency?: number; 20 + region?: Region; 18 21 }) => { 19 22 const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 20 23 const { name } = monitor; ··· 62 65 message?: string; 63 66 incidentId?: string; 64 67 cronTimestamp: number; 68 + latency?: number; 69 + region?: Region; 65 70 }) => { 66 71 const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 67 72 const { name } = monitor; ··· 113 118 message?: string; 114 119 incidentId?: string; 115 120 cronTimestamp: number; 121 + latency?: number; 122 + region?: Region; 116 123 }) => { 117 124 const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 118 125
+8 -1
packages/notifications/pagerduty/src/index.ts
··· 1 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 2 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 3 4 import { 4 5 PagerDutySchema, 5 6 resolveEventPayloadSchema, ··· 20 21 message?: string; 21 22 incidentId?: string; 22 23 cronTimestamp: number; 24 + latency?: number; 25 + region?: Region; 23 26 }) => { 24 27 const notificationData = PagerDutySchema.parse(JSON.parse(notification.data)); 25 28 const { name } = monitor; ··· 66 69 message?: string; 67 70 incidentId?: string; 68 71 cronTimestamp: number; 72 + latency?: number; 73 + region?: Region; 69 74 }) => { 70 75 const notificationData = PagerDutySchema.parse(JSON.parse(notification.data)); 71 76 const { name } = monitor; ··· 117 122 message?: string; 118 123 incidentId?: string; 119 124 cronTimestamp: number; 125 + latency?: number; 126 + region?: Region; 120 127 }) => { 121 128 const notificationData = PagerDutySchema.parse(JSON.parse(notification.data)); 122 129 ··· 162 169 }, 163 170 }); 164 171 165 - const res = await fetch("https://events.pagerduty.com/v2/enqueue", { 172 + const _res = await fetch("https://events.pagerduty.com/v2/enqueue", { 166 173 method: "POST", 167 174 body: JSON.stringify(event), 168 175 });
+7
packages/notifications/slack/src/index.ts
··· 1 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import type { Region } from "@openstatus/db/src/schema/constants"; 2 3 import { DataSchema } from "./schema"; 3 4 4 5 // biome-ignore lint/suspicious/noExplicitAny: <explanation> ··· 29 30 message?: string; 30 31 incidentId?: string; 31 32 cronTimestamp: number; 33 + latency?: number; 34 + region?: Region; 32 35 }) => { 33 36 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 34 37 const { slack: webhookUrl } = notificationData; // webhook url ··· 88 91 message?: string; 89 92 incidentId?: string; 90 93 cronTimestamp: number; 94 + region?: Region; 95 + latency?: number; 91 96 }) => { 92 97 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 93 98 const { slack: webhookUrl } = notificationData; // webhook url ··· 139 144 statusCode?: number; 140 145 message?: string; 141 146 cronTimestamp: number; 147 + region?: Region; 148 + latency?: number; 142 149 }) => { 143 150 const notificationData = DataSchema.parse(JSON.parse(notification.data)); 144 151 const { slack: webhookUrl } = notificationData; // webhook url
+7
packages/notifications/twillio-sms/src/index.ts
··· 1 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 2 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 3 4 import { env } from "./env"; 4 5 import { SmsConfigurationSchema } from "./schema/config"; 5 6 ··· 17 18 message?: string; 18 19 incidentId?: string; 19 20 cronTimestamp: number; 21 + latency?: number; 22 + region?: Region; 20 23 }) => { 21 24 const notificationData = SmsConfigurationSchema.parse( 22 25 JSON.parse(notification.data), ··· 68 71 message?: string; 69 72 incidentId?: string; 70 73 cronTimestamp: number; 74 + latency?: number; 75 + region?: Region; 71 76 }) => { 72 77 const notificationData = SmsConfigurationSchema.parse( 73 78 JSON.parse(notification.data), ··· 112 117 message?: string; 113 118 incidentId?: string; 114 119 cronTimestamp: number; 120 + latency?: number; 121 + region?: Region; 115 122 }) => { 116 123 const notificationData = SmsConfigurationSchema.parse( 117 124 JSON.parse(notification.data),
+44 -91
pnpm-lock.yaml
··· 171 171 version: link:../../packages/utils 172 172 '@scalar/hono-api-reference': 173 173 specifier: 0.5.131 174 - version: 0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 174 + version: 0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 175 175 '@t3-oss/env-core': 176 176 specifier: 0.7.1 177 177 version: 0.7.1(typescript@5.7.2)(zod@3.23.8) ··· 286 286 version: 1.1.3(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 287 287 '@sentry/nextjs': 288 288 specifier: 8.46.0 289 - version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.21.5)) 289 + version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.21.5)) 290 290 '@stripe/stripe-js': 291 291 specifier: 2.1.6 292 292 version: 2.1.6 ··· 313 313 version: 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 314 314 '@trpc/next': 315 315 specifier: 11.0.0-rc.666 316 - version: 11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/react-query@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2) 316 + version: 11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/react-query@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2) 317 317 '@trpc/react-query': 318 318 specifier: 11.0.0-rc.666 319 319 version: 11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2) ··· 361 361 version: 5.0.7 362 362 next: 363 363 specifier: 15.1.7 364 - version: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 364 + version: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 365 365 next-auth: 366 366 specifier: 5.0.0-beta.25 367 - version: 5.0.0-beta.25(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 367 + version: 5.0.0-beta.25(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 368 368 next-plausible: 369 369 specifier: 3.12.4 370 - version: 3.12.4(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 370 + version: 3.12.4(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 371 371 next-themes: 372 372 specifier: 0.2.1 373 - version: 0.2.1(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 373 + version: 0.2.1(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 374 374 nuqs: 375 375 specifier: 2.2.3 376 - version: 2.2.3(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 376 + version: 2.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 377 377 random-word-slugs: 378 378 specifier: 0.1.7 379 379 version: 0.1.7 ··· 443 443 version: 0.2.0(@content-collections/core@0.7.3(typescript@5.6.2))(acorn@8.14.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 444 444 '@content-collections/next': 445 445 specifier: 0.2.4 446 - version: 0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) 446 + version: 0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) 447 447 '@openstatus/tsconfig': 448 448 specifier: workspace:* 449 449 version: link:../../packages/tsconfig ··· 761 761 '@openstatus/tinybird': 762 762 specifier: workspace:* 763 763 version: link:../../tinybird 764 + '@openstatus/utils': 765 + specifier: workspace:* 766 + version: link:../../utils 764 767 '@react-email/components': 765 768 specifier: 0.0.31 766 769 version: 0.0.31(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 11470 11473 - acorn 11471 11474 - supports-color 11472 11475 11473 - '@content-collections/next@0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': 11476 + '@content-collections/next@0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': 11474 11477 dependencies: 11475 11478 '@content-collections/core': 0.7.3(typescript@5.6.2) 11476 11479 '@content-collections/integrations': 0.2.1(@content-collections/core@0.7.3(typescript@5.6.2)) 11477 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 11480 + next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 11478 11481 11479 11482 '@cspotcode/source-map-support@0.8.1': 11480 11483 dependencies: ··· 12019 12022 protobufjs: 7.2.5 12020 12023 yargs: 17.7.2 12021 12024 12022 - '@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))': 12025 + '@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))': 12023 12026 dependencies: 12024 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)) 12027 + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)) 12025 12028 12026 12029 '@headlessui/vue@1.7.22(vue@3.4.31(typescript@5.7.2))': 12027 12030 dependencies: ··· 13652 13655 '@rollup/rollup-win32-x64-msvc@4.34.8': 13653 13656 optional: true 13654 13657 13655 - '@scalar/api-client@2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13658 + '@scalar/api-client@2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13656 13659 dependencies: 13657 - '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2))) 13660 + '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2))) 13658 13661 '@headlessui/vue': 1.7.22(vue@3.4.31(typescript@5.7.2)) 13659 13662 '@scalar/components': 0.12.28(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(typescript@5.7.2) 13660 13663 '@scalar/draggable': 0.1.4(typescript@5.7.2) ··· 13690 13693 - typescript 13691 13694 - vitest 13692 13695 13693 - '@scalar/api-reference@1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13696 + '@scalar/api-reference@1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13694 13697 dependencies: 13695 13698 '@floating-ui/vue': 1.1.1(vue@3.4.31(typescript@5.7.2)) 13696 13699 '@headlessui/vue': 1.7.22(vue@3.4.31(typescript@5.7.2)) 13697 - '@scalar/api-client': 2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 13700 + '@scalar/api-client': 2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 13698 13701 '@scalar/components': 0.12.28(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(typescript@5.7.2) 13699 13702 '@scalar/oas-utils': 0.2.26(typescript@5.7.2) 13700 13703 '@scalar/openapi-parser': 0.7.2 ··· 13779 13782 transitivePeerDependencies: 13780 13783 - typescript 13781 13784 13782 - '@scalar/hono-api-reference@0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13785 + '@scalar/hono-api-reference@0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13783 13786 dependencies: 13784 - '@scalar/api-reference': 1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 13787 + '@scalar/api-reference': 1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 13785 13788 hono: 4.5.3 13786 13789 transitivePeerDependencies: 13787 13790 - '@jest/globals' ··· 13999 14002 '@sentry/types': 8.9.2 14000 14003 '@sentry/utils': 8.9.2 14001 14004 14002 - '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.21.5))': 14005 + '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.21.5))': 14003 14006 dependencies: 14004 14007 '@opentelemetry/api': 1.9.0 14005 14008 '@opentelemetry/semantic-conventions': 1.28.0 ··· 14012 14015 '@sentry/vercel-edge': 8.46.0 14013 14016 '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 14014 14017 chalk: 3.0.0 14015 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14018 + next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14016 14019 resolve: 1.22.8 14017 14020 rollup: 3.29.5 14018 14021 stacktrace-parser: 0.1.10 ··· 14640 14643 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14641 14644 typescript: 5.6.2 14642 14645 14643 - '@trpc/next@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/react-query@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2)': 14646 + '@trpc/next@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/react-query@11.0.0-rc.666(@tanstack/react-query@5.62.8(react@19.0.0))(@trpc/client@11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2))(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.2)': 14644 14647 dependencies: 14645 14648 '@trpc/client': 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 14646 14649 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14647 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14650 + next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14648 14651 react: 19.0.0 14649 14652 react-dom: 19.0.0(react@19.0.0) 14650 14653 typescript: 5.6.2 ··· 18648 18651 18649 18652 netmask@2.0.2: {} 18650 18653 18651 - next-auth@5.0.0-beta.25(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18652 - dependencies: 18653 - '@auth/core': 0.37.2 18654 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18655 - react: 19.0.0 18656 - 18657 18654 next-auth@5.0.0-beta.25(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18658 18655 dependencies: 18659 18656 '@auth/core': 0.37.2 18660 18657 next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18661 18658 react: 19.0.0 18662 18659 18663 - next-plausible@3.12.4(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18664 - dependencies: 18665 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18666 - react: 19.0.0 18667 - react-dom: 19.0.0(react@19.0.0) 18668 - 18669 - next-themes@0.2.1(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18660 + next-plausible@3.12.4(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18670 18661 dependencies: 18671 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18662 + next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18672 18663 react: 19.0.0 18673 18664 react-dom: 19.0.0(react@19.0.0) 18674 18665 ··· 18704 18695 - '@babel/core' 18705 18696 - babel-plugin-macros 18706 18697 18707 - next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18708 - dependencies: 18709 - '@next/env': 15.1.7 18710 - '@swc/counter': 0.1.3 18711 - '@swc/helpers': 0.5.15 18712 - busboy: 1.6.0 18713 - caniuse-lite: 1.0.30001689 18714 - postcss: 8.4.31 18715 - react: 19.0.0 18716 - react-dom: 19.0.0(react@19.0.0) 18717 - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) 18718 - optionalDependencies: 18719 - '@next/swc-darwin-arm64': 15.1.7 18720 - '@next/swc-darwin-x64': 15.1.7 18721 - '@next/swc-linux-arm64-gnu': 15.1.7 18722 - '@next/swc-linux-arm64-musl': 15.1.7 18723 - '@next/swc-linux-x64-gnu': 15.1.7 18724 - '@next/swc-linux-x64-musl': 15.1.7 18725 - '@next/swc-win32-arm64-msvc': 15.1.7 18726 - '@next/swc-win32-x64-msvc': 15.1.7 18727 - '@opentelemetry/api': 1.9.0 18728 - sharp: 0.33.5 18729 - transitivePeerDependencies: 18730 - - '@babel/core' 18731 - - babel-plugin-macros 18732 - 18733 18698 next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18734 18699 dependencies: 18735 18700 '@next/env': 15.1.7 ··· 18740 18705 postcss: 8.4.31 18741 18706 react: 19.0.0 18742 18707 react-dom: 19.0.0(react@19.0.0) 18743 - styled-jsx: 5.1.6(react@19.0.0) 18708 + styled-jsx: 5.1.6(@babel/core@7.24.5)(react@19.0.0) 18744 18709 optionalDependencies: 18745 18710 '@next/swc-darwin-arm64': 15.1.7 18746 18711 '@next/swc-darwin-x64': 15.1.7 ··· 18833 18798 dependencies: 18834 18799 boolbase: 1.0.0 18835 18800 18836 - nuqs@2.2.3(next@15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18801 + nuqs@2.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18837 18802 dependencies: 18838 18803 mitt: 3.0.1 18839 18804 react: 19.0.0 18840 18805 optionalDependencies: 18841 - next: 15.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18806 + next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18842 18807 18843 18808 oauth4webapi@2.10.4: {} 18844 18809 ··· 19187 19152 postcss: 8.4.38 19188 19153 ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19189 19154 19190 - postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 19155 + postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)): 19191 19156 dependencies: 19192 19157 lilconfig: 3.1.3 19193 19158 yaml: 2.6.1 19194 19159 optionalDependencies: 19195 19160 postcss: 8.5.2 19196 - ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19161 + ts-node: 10.9.2(@types/node@20.8.0)(typescript@5.7.2) 19197 19162 19198 - postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)): 19163 + postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 19199 19164 dependencies: 19200 19165 lilconfig: 3.1.3 19201 19166 yaml: 2.6.1 19202 19167 optionalDependencies: 19203 19168 postcss: 8.5.2 19204 - ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.7.2) 19169 + ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19205 19170 19206 19171 postcss-nested@6.0.1(postcss@8.5.2): 19207 19172 dependencies: ··· 20351 20316 optionalDependencies: 20352 20317 '@babel/core': 7.24.5 20353 20318 20354 - styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): 20355 - dependencies: 20356 - client-only: 0.0.1 20357 - react: 19.0.0 20358 - optionalDependencies: 20359 - '@babel/core': 7.26.0 20360 - 20361 - styled-jsx@5.1.6(react@19.0.0): 20362 - dependencies: 20363 - client-only: 0.0.1 20364 - react: 19.0.0 20365 - 20366 20319 sucrase@3.34.0: 20367 20320 dependencies: 20368 20321 '@jridgewell/gen-mapping': 0.3.5 ··· 20426 20379 dependencies: 20427 20380 tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20428 20381 20429 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 20382 + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)): 20430 20383 dependencies: 20431 20384 '@alloc/quick-lru': 5.2.0 20432 20385 arg: 5.0.2 ··· 20445 20398 postcss: 8.5.2 20446 20399 postcss-import: 15.1.0(postcss@8.5.2) 20447 20400 postcss-js: 4.0.1(postcss@8.5.2) 20448 - postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20401 + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)) 20449 20402 postcss-nested: 6.2.0(postcss@8.5.2) 20450 20403 postcss-selector-parser: 6.1.2 20451 20404 resolve: 1.22.9 ··· 20453 20406 transitivePeerDependencies: 20454 20407 - ts-node 20455 20408 20456 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)): 20409 + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 20457 20410 dependencies: 20458 20411 '@alloc/quick-lru': 5.2.0 20459 20412 arg: 5.0.2 ··· 20472 20425 postcss: 8.5.2 20473 20426 postcss-import: 15.1.0(postcss@8.5.2) 20474 20427 postcss-js: 4.0.1(postcss@8.5.2) 20475 - postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)) 20428 + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20476 20429 postcss-nested: 6.2.0(postcss@8.5.2) 20477 20430 postcss-selector-parser: 6.1.2 20478 20431 resolve: 1.22.9 ··· 20726 20679 v8-compile-cache-lib: 3.0.1 20727 20680 yn: 3.1.1 20728 20681 20729 - ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2): 20682 + ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2): 20730 20683 dependencies: 20731 20684 '@cspotcode/source-map-support': 0.8.1 20732 20685 '@tsconfig/node10': 1.0.11 20733 20686 '@tsconfig/node12': 1.0.11 20734 20687 '@tsconfig/node14': 1.0.3 20735 20688 '@tsconfig/node16': 1.0.4 20736 - '@types/node': 22.10.2 20689 + '@types/node': 20.8.0 20737 20690 acorn: 8.11.3 20738 20691 acorn-walk: 8.3.2 20739 20692 arg: 4.1.3 20740 20693 create-require: 1.1.1 20741 20694 diff: 4.0.2 20742 20695 make-error: 1.3.6 20743 - typescript: 5.6.2 20696 + typescript: 5.7.2 20744 20697 v8-compile-cache-lib: 3.0.1 20745 20698 yn: 3.1.1 20746 20699 optional: true 20747 20700 20748 - ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2): 20701 + ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2): 20749 20702 dependencies: 20750 20703 '@cspotcode/source-map-support': 0.8.1 20751 20704 '@tsconfig/node10': 1.0.11 ··· 20759 20712 create-require: 1.1.1 20760 20713 diff: 4.0.2 20761 20714 make-error: 1.3.6 20762 - typescript: 5.7.2 20715 + typescript: 5.6.2 20763 20716 v8-compile-cache-lib: 3.0.1 20764 20717 yn: 3.1.1 20765 20718 optional: true