Openstatus www.openstatus.dev

Merge branch 'main' into feat/add-maintenance-resource-to-API

authored by

Maximilian Kaske and committed by
GitHub
6046176c 90d2f039

+2389 -501
+52
.github/workflows/dx.yml
···
··· 1 + # https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml 2 + name: DX Check 3 + on: 4 + push: 5 + branches: 6 + - "main" 7 + pull_request: 8 + branches: [main] 9 + 10 + jobs: 11 + dx: 12 + name: 🧑‍💻 DX checker 13 + runs-on: ubuntu-latest 14 + timeout-minutes: 15 15 + services: 16 + sqld: 17 + image: ghcr.io/tursodatabase/libsql-server:latest 18 + ports: 19 + - 8080:8080 20 + # env: 21 + # SQLD_HTTP_AUTH: "basic:token" 22 + 23 + env: 24 + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 25 + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 26 + DATABASE_URL: http://127.0.0.1:8080 27 + DATABASE_AUTH_TOKEN: "basic:token" 28 + steps: 29 + - name: ⬇️ Checkout repo 30 + uses: actions/checkout@v4 31 + 32 + - name: Set up pnpm 33 + uses: pnpm/action-setup@v4 34 + with: 35 + version: 9.1.4 36 + 37 + - name: ⎔ Setup node 38 + uses: actions/setup-node@v4 39 + with: 40 + node-version: 20 41 + cache: "pnpm" 42 + 43 + - name: 📥 Download deps 44 + run: pnpm install 45 + 46 + - name: 🔥 Install bun 47 + uses: oven-sh/setup-bun@v2 48 + with: 49 + bun-version: latest 50 + 51 + - name: 🔥 DX task 52 + run: pnpm dx
+1
.gitignore
··· 60 .wrangler 61 apps/web/.env.dev 62 packages/db/.env.dev 63 openstatus-dev.db-wal 64 openstatus-dev.db-shm 65 apps/ingest-worker/.production.vars
··· 60 .wrangler 61 apps/web/.env.dev 62 packages/db/.env.dev 63 + openstatus-dev.db 64 openstatus-dev.db-wal 65 openstatus-dev.db-shm 66 apps/ingest-worker/.production.vars
+37 -23
README.md
··· 84 85 ## Getting Started 🚀 86 87 ### Requirements 88 89 - [Node.js](https://nodejs.org/en/) >= 20.0.0 90 - [pnpm](https://pnpm.io/) >= 8.6.2 91 92 ### Setup 93 94 1. Clone the repository 95 96 - ```sh 97 - git clone https://github.com/openstatushq/openstatus.git 98 - ``` 99 100 2. Install dependencies 101 102 - ```sh 103 - pnpm install 104 - ``` 105 106 - 3. Set up your .env file 107 108 - From `apps/web` and `packages/db`, you will find .env.example. Create your 109 - own copy. 110 111 - 4. Follow the steps to run your sqlite database locally inside of 112 - [README.md](https://github.com/openstatusHQ/openstatus/blob/main/packages/db/README.md) 113 114 - 5. Start the development with the below command 115 116 - ```sh 117 - pnpm dev 118 - ``` 119 120 - It will: 121 122 - - run the web app on port `3000` 123 - - run the api server on port `3001` 124 - - run the docs on port `3002` 125 126 - 6. See the results: 127 128 - open [http://localhost:3000](http://localhost:3000) for the web app 129 - - open [http://localhost:3001/ping](http://localhost:3001/ping) for the api 130 - server health check 131 - - open [http://localhost:3002](http://localhost:3002) for the docs 132 133 ### Videos 134
··· 84 85 ## Getting Started 🚀 86 87 + ### With Devbox 88 + 89 + You can use [Devbox](https://www.jetify.com/devbox/) and get started with the following commands: 90 + 91 + 1. Install Devbox 92 + ```sh 93 + curl -fsSL https://get.jetify.com/devbox | bash 94 + ``` 95 + 2. Install project dependencies, build and start services 96 + ```sh 97 + devbox services up 98 + ``` 99 + 100 + Alternatively, follow the instructions below. 101 + 102 ### Requirements 103 104 - [Node.js](https://nodejs.org/en/) >= 20.0.0 105 - [pnpm](https://pnpm.io/) >= 8.6.2 106 + - [Bun](https://bun.sh/) 107 + - [Turso CLI](https://docs.turso.tech/quickstart) 108 109 ### Setup 110 111 1. Clone the repository 112 113 + ```sh 114 + git clone https://github.com/openstatushq/openstatus.git 115 + ``` 116 117 2. Install dependencies 118 119 + ```sh 120 + pnpm install 121 + ``` 122 123 + 3. Initialize the development environment 124 125 + Launch the database in one terminal: 126 127 + ```sh 128 + turso dev --db-file openstatus-dev.db 129 + ``` 130 131 + In another terminal, run the following command: 132 133 + ```sh 134 + pnpm dx 135 + ``` 136 137 + 4. Launch the web app 138 139 + ```sh 140 + pnpm dev:web 141 + ``` 142 143 + 5. See the results: 144 145 - open [http://localhost:3000](http://localhost:3000) for the web app 146 147 ### Videos 148
apps/docs/src/assets/notification/opsgenie/opsgenie-1.png

This is a binary file and will not be displayed.

apps/docs/src/assets/notification/opsgenie/opsgenie-2.png

This is a binary file and will not be displayed.

apps/docs/src/assets/notification/opsgenie/opsgenie-3.png

This is a binary file and will not be displayed.

apps/docs/src/assets/notification/opsgenie/opsgenie-4.png

This is a binary file and will not be displayed.

apps/docs/src/assets/notification/opsgenie/opsgenie-5.png

This is a binary file and will not be displayed.

+69
apps/docs/src/content/docs/alerting/providers/opsgenie.mdx
···
··· 1 + --- 2 + title: OpsGenie 3 + description: "How to set up OpsGenie notifications in OpenStatus to get alerts when your synthetic check fail" 4 + 5 + --- 6 + import { Image} from 'astro:assets'; 7 + import OpsGenie1 from '../../../../assets/notification/opsgenie/opsgenie-1.png'; 8 + import OpsGenie2 from '../../../../assets/notification/opsgenie/opsgenie-2.png'; 9 + import OpsGenie3 from '../../../../assets/notification/opsgenie/opsgenie-3.png'; 10 + import OpsGenie4 from '../../../../assets/notification/opsgenie/opsgenie-4.png'; 11 + import OpsGenie5 from '../../../../assets/notification/opsgenie/opsgenie-5.png'; 12 + 13 + Get Notified on OpsGenie when we create an incident. 14 + 15 + ## How to connect OpsGenie 16 + 17 + ### Create an OpsGenie Integration 18 + 19 + First we need to create an integration in OpsGenie. 20 + 21 + Go to your team in OpsGenie and select `Integrations` from the menu. 22 + 23 + 24 + <Image 25 + src={OpsGenie1} 26 + alt="OpsGenie integration page" 27 + /> 28 + In the integrations page, search for `API` and select it. 29 + 30 + 31 + <Image 32 + src={OpsGenie2} 33 + alt="Connect to OpsGenie" 34 + /> 35 + 36 + Give your integration a name and save it. 37 + Copy your API key and save it. You will need it to connect OpenStatus to OpsGenie. 38 + 39 + <Image 40 + src={OpsGenie3} 41 + alt="Connect to OpsGenie" 42 + /> 43 + 44 + 45 + ### Connect your OpenStatus account to OpsGenie 46 + 47 + 48 + Go to the Alerts Page . Select `OpsGenie` from the list of available integrations. 49 + 50 + 51 + <Image 52 + src={OpsGenie4} 53 + alt="OpenStatus Notifications Page" 54 + /> 55 + 56 + 57 + Give you integration a name and paste the API key you copied from OpsGenie and select the region of your OpsGenie account. 58 + 59 + <Image 60 + src={OpsGenie5} 61 + alt="Connect to OpsGenie in OpenStatus" 62 + /> 63 + 64 + Select the service you want to use to send notifications. You can create a new service if you don't have one. 65 + 66 + 67 + You are now connected to OpsGenie. 68 + 69 + You will receive some notifications if we detect an incident
+25 -27
apps/docs/src/content/docs/contributing/getting-started.mdx
··· 2 title: Getting Started 3 --- 4 5 - import { Aside } from "@astrojs/starlight/components"; 6 - 7 - <Aside>WIP</Aside> 8 9 ## Setup 10 11 - 1. Clone the repository and open the created directory 12 13 - ```sh 14 - git clone https://github.com/openstatushq/openstatus.git 15 - cd openstatus 16 - ``` 17 18 2. Install dependencies 19 20 - ```sh 21 - pnpm install 22 - ``` 23 24 - 3. Database setup 25 - 26 - We are using Turso for the database to make it work locally you need to install 27 - it and run it. 28 - 29 - If you are on a mac install it with [Homebrew](https://brew.sh/) 30 - 31 32 - Here is the official Turso 33 - [installation guide](https://docs.turso.tech/reference/turso-cli) 34 35 - 4. Install Bun 36 - All the up to date information are on the official [Bun website](https://bun.sh/) 37 38 - 4. Set up the dev environment 39 40 ```sh 41 - pnpm dx 42 ``` 43 - 5. Start the development server 44 45 ```sh 46 - pnpm dev:web 47 ``` 48 49 - 6. Open your browser and navigate to [http://localhost:3000](http://localhost:3000)
··· 2 title: Getting Started 3 --- 4 5 6 ## Setup 7 8 + 1. Clone the repository 9 10 + ```sh 11 + git clone https://github.com/openstatushq/openstatus.git 12 + ``` 13 14 2. Install dependencies 15 16 + ```sh 17 + pnpm install 18 + ``` 19 20 + 3. Initialize the development environment 21 22 + Launch the database in one terminal: 23 24 + ```sh 25 + turso dev --db-file openstatus-dev.db 26 + ``` 27 28 + In another terminal, run the following command: 29 30 ```sh 31 + pnpm dx 32 ``` 33 + 34 + 4. Launch the web app 35 36 ```sh 37 + pnpm dev:web 38 ``` 39 40 + It will: 41 + 42 + - run the web app on port `3000` 43 + 44 + 5. See the results: 45 + 46 + - open [http://localhost:3000](http://localhost:3000) for the web app 47 +
+5
apps/docs/src/content/docs/contributing/requirements.mdx
··· 73 74 If you want to run Turso locally you can follow the instructions in the 75 [Turso CLI](https://docs.turso.tech/reference/turso-cli)
··· 73 74 If you want to run Turso locally you can follow the instructions in the 75 [Turso CLI](https://docs.turso.tech/reference/turso-cli) 76 + 77 + 78 + ### Installing Bun 79 + 80 + To install Bun, visit the official website [https://bun.sh/](https://bun.sh/)
+3 -2
apps/server/Dockerfile
··· 3 # See https://github.com/lenra-io/dofigen 4 5 # install 6 - FROM oven/bun@sha256:9a45ebd9a1e5403177064592e1564791443b1a459356c905d1112e32758dd454 AS install 7 WORKDIR /app/ 8 RUN \ 9 --mount=type=bind,target=package.json,source=package.json \ ··· 14 --mount=type=bind,target=packages/error/package.json,source=packages/error/package.json \ 15 --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \ 16 --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \ 17 --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ 18 --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \ 19 --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \ ··· 27 bun install --production --ignore-scripts --frozen-lockfile --verbose 28 29 # build 30 - FROM oven/bun@sha256:9a45ebd9a1e5403177064592e1564791443b1a459356c905d1112e32758dd454 AS build 31 ENV NODE_ENV="production" 32 WORKDIR /app/apps/server 33 COPY \
··· 3 # See https://github.com/lenra-io/dofigen 4 5 # install 6 + FROM oven/bun@sha256:10cda3ac52b7ddfb3dda2fd1f0ed2147dcb8d5b7ed7baeffbfcaf6e15c1c00df AS install 7 WORKDIR /app/ 8 RUN \ 9 --mount=type=bind,target=package.json,source=package.json \ ··· 14 --mount=type=bind,target=packages/error/package.json,source=packages/error/package.json \ 15 --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \ 16 --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \ 17 + --mount=type=bind,target=packages/notifications/opsgenie/package.json,source=packages/notifications/opsgenie/package.json \ 18 --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ 19 --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \ 20 --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \ ··· 28 bun install --production --ignore-scripts --frozen-lockfile --verbose 29 30 # build 31 + FROM oven/bun@sha256:10cda3ac52b7ddfb3dda2fd1f0ed2147dcb8d5b7ed7baeffbfcaf6e15c1c00df AS build 32 ENV NODE_ENV="production" 33 WORKDIR /app/apps/server 34 COPY \
+26 -23
apps/server/dofigen.lock
··· 7 - /packages/api 8 - /packages/integrations/vercel 9 builders: 10 install: 11 fromImage: 12 path: oven/bun 13 - digest: sha256:9a45ebd9a1e5403177064592e1564791443b1a459356c905d1112e32758dd454 14 workdir: /app/ 15 run: 16 - bun install --production --ignore-scripts --frozen-lockfile --verbose ··· 33 source: packages/notifications/discord/package.json 34 - target: packages/notifications/email/package.json 35 source: packages/notifications/email/package.json 36 - target: packages/notifications/pagerduty/package.json 37 source: packages/notifications/pagerduty/package.json 38 - target: packages/notifications/slack/package.json ··· 51 source: packages/tsconfig/package.json 52 - target: packages/assertions/package.json 53 source: packages/assertions/package.json 54 - build: 55 - fromImage: 56 - path: oven/bun 57 - digest: sha256:9a45ebd9a1e5403177064592e1564791443b1a459356c905d1112e32758dd454 58 - workdir: /app/apps/server 59 - env: 60 - NODE_ENV: production 61 - copy: 62 - - paths: 63 - - . 64 - target: /app/ 65 - - fromBuilder: install 66 - paths: 67 - - /app/node_modules 68 - target: /app/node_modules 69 - run: 70 - - bun build --compile --sourcemap src/index.ts --outfile=app 71 fromImage: 72 path: debian 73 digest: sha256:b0c91cc181796d34c53f7ea106fbcddaf87f3e601cc371af6a24a019a489c980 ··· 83 - port: 3000 84 images: 85 registry.hub.docker.com:443: 86 library: 87 debian: 88 bullseye-slim: 89 digest: sha256:b0c91cc181796d34c53f7ea106fbcddaf87f3e601cc371af6a24a019a489c980 90 - oven: 91 - bun: 92 - latest: 93 - digest: sha256:9a45ebd9a1e5403177064592e1564791443b1a459356c905d1112e32758dd454 94 resources: 95 dofigen.yml: 96 - hash: 06641706a075437d0d038b10609949fde32ab3340f6eb081add0b1e0a1889d8b 97 content: | 98 ignore: 99 - node_modules ··· 116 - packages/error/package.json 117 - packages/notifications/discord/package.json 118 - packages/notifications/email/package.json 119 - packages/notifications/pagerduty/package.json 120 - packages/notifications/slack/package.json 121 - packages/notifications/twillio-sms/package.json
··· 7 - /packages/api 8 - /packages/integrations/vercel 9 builders: 10 + build: 11 + fromImage: 12 + path: oven/bun 13 + digest: sha256:10cda3ac52b7ddfb3dda2fd1f0ed2147dcb8d5b7ed7baeffbfcaf6e15c1c00df 14 + workdir: /app/apps/server 15 + env: 16 + NODE_ENV: production 17 + copy: 18 + - paths: 19 + - . 20 + target: /app/ 21 + - fromBuilder: install 22 + paths: 23 + - /app/node_modules 24 + target: /app/node_modules 25 + run: 26 + - bun build --compile --sourcemap src/index.ts --outfile=app 27 install: 28 fromImage: 29 path: oven/bun 30 + digest: sha256:10cda3ac52b7ddfb3dda2fd1f0ed2147dcb8d5b7ed7baeffbfcaf6e15c1c00df 31 workdir: /app/ 32 run: 33 - bun install --production --ignore-scripts --frozen-lockfile --verbose ··· 50 source: packages/notifications/discord/package.json 51 - target: packages/notifications/email/package.json 52 source: packages/notifications/email/package.json 53 + - target: packages/notifications/opsgenie/package.json 54 + source: packages/notifications/opsgenie/package.json 55 - target: packages/notifications/pagerduty/package.json 56 source: packages/notifications/pagerduty/package.json 57 - target: packages/notifications/slack/package.json ··· 70 source: packages/tsconfig/package.json 71 - target: packages/assertions/package.json 72 source: packages/assertions/package.json 73 fromImage: 74 path: debian 75 digest: sha256:b0c91cc181796d34c53f7ea106fbcddaf87f3e601cc371af6a24a019a489c980 ··· 85 - port: 3000 86 images: 87 registry.hub.docker.com:443: 88 + oven: 89 + bun: 90 + latest: 91 + digest: sha256:10cda3ac52b7ddfb3dda2fd1f0ed2147dcb8d5b7ed7baeffbfcaf6e15c1c00df 92 library: 93 debian: 94 bullseye-slim: 95 digest: sha256:b0c91cc181796d34c53f7ea106fbcddaf87f3e601cc371af6a24a019a489c980 96 resources: 97 dofigen.yml: 98 + hash: 4983836e4e9309e7c36e9d8b42696abad4223effb74f65efdc47e8f651e323a6 99 content: | 100 ignore: 101 - node_modules ··· 118 - packages/error/package.json 119 - packages/notifications/discord/package.json 120 - packages/notifications/email/package.json 121 + - packages/notifications/opsgenie/package.json 122 - packages/notifications/pagerduty/package.json 123 - packages/notifications/slack/package.json 124 - packages/notifications/twillio-sms/package.json
+1
apps/server/dofigen.yml
··· 19 - packages/error/package.json 20 - packages/notifications/discord/package.json 21 - packages/notifications/email/package.json 22 - packages/notifications/pagerduty/package.json 23 - packages/notifications/slack/package.json 24 - packages/notifications/twillio-sms/package.json
··· 19 - packages/error/package.json 20 - packages/notifications/discord/package.json 21 - packages/notifications/email/package.json 22 + - packages/notifications/opsgenie/package.json 23 - packages/notifications/pagerduty/package.json 24 - packages/notifications/slack/package.json 25 - packages/notifications/twillio-sms/package.json
+2
apps/server/env.ts
···
··· 1 + const file = Bun.file("./.env.example"); 2 + await Bun.write("./.env", file);
+2
apps/server/package.json
··· 5 "type": "module", 6 "main": "src/index.ts", 7 "scripts": { 8 "dev": "bun run --hot src/index.ts", 9 "start": "NODE_ENV=production bun run src/index.ts", 10 "test": "bun test", ··· 21 "@openstatus/error": "workspace:*", 22 "@openstatus/notification-discord": "workspace:*", 23 "@openstatus/notification-emails": "workspace:*", 24 "@openstatus/notification-pagerduty": "workspace:*", 25 "@openstatus/notification-slack": "workspace:*", 26 "@openstatus/notification-twillio-sms": "workspace:*",
··· 5 "type": "module", 6 "main": "src/index.ts", 7 "scripts": { 8 + "env": "bun env.ts", 9 "dev": "bun run --hot src/index.ts", 10 "start": "NODE_ENV=production bun run src/index.ts", 11 "test": "bun test", ··· 22 "@openstatus/error": "workspace:*", 23 "@openstatus/notification-discord": "workspace:*", 24 "@openstatus/notification-emails": "workspace:*", 25 + "@openstatus/notification-opsgenie": "workspace:*", 26 "@openstatus/notification-pagerduty": "workspace:*", 27 "@openstatus/notification-slack": "workspace:*", 28 "@openstatus/notification-twillio-sms": "workspace:*",
+11 -1
apps/server/src/routes/checker/utils.ts
··· 30 sendAlert as sendPagerdutyAlert, 31 } from "@openstatus/notification-pagerduty"; 32 33 type SendNotification = ({ 34 monitor, 35 notification, ··· 73 sendRecovery: sendSmsRecovery, 74 sendDegraded: sendSmsDegraded, 75 }, 76 - 77 pagerduty: { 78 sendAlert: sendPagerdutyAlert, 79 sendRecovery: sendPagerDutyRecovery,
··· 30 sendAlert as sendPagerdutyAlert, 31 } from "@openstatus/notification-pagerduty"; 32 33 + import { 34 + sendAlert as sendOpsGenieAlert, 35 + sendDegraded as sendOpsGenieDegraded, 36 + sendRecovery as sendOpsGenieRecovery, 37 + } from "@openstatus/notification-opsgenie"; 38 + 39 type SendNotification = ({ 40 monitor, 41 notification, ··· 79 sendRecovery: sendSmsRecovery, 80 sendDegraded: sendSmsDegraded, 81 }, 82 + opsgenie: { 83 + sendAlert: sendOpsGenieAlert, 84 + sendRecovery: sendOpsGenieRecovery, 85 + sendDegraded: sendOpsGenieDegraded, 86 + }, 87 pagerduty: { 88 sendAlert: sendPagerdutyAlert, 89 sendRecovery: sendPagerDutyRecovery,
+1 -2
apps/server/src/routes/v1/pageSubscribers/post.ts
··· 6 import { and, eq } from "@openstatus/db"; 7 import { db } from "@openstatus/db/src/db"; 8 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 9 - import { SubscribeEmail } from "@openstatus/emails"; 10 - import { sendEmail } from "@openstatus/emails/src/send"; 11 import type { pageSubscribersApi } from "./index"; 12 import { PageSubscriberSchema, ParamsSchema } from "./schema"; 13
··· 6 import { and, eq } from "@openstatus/db"; 7 import { db } from "@openstatus/db/src/db"; 8 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 9 + import { SubscribeEmail, sendEmail } from "@openstatus/emails"; 10 import type { pageSubscribersApi } from "./index"; 11 import { PageSubscriberSchema, ParamsSchema } from "./schema"; 12
+26 -3
apps/server/src/routes/v1/statusReports/delete.test.ts
··· 1 import { expect, test } from "bun:test"; 2 3 import { app } from "@/index"; 4 5 test("delete the status report", async () => { 6 - const res = await app.request("/v1/status_report/3", { 7 method: "DELETE", 8 headers: { 9 "x-openstatus-key": "1", 10 }, 11 }); 12 13 - expect(res.status).toBe(200); 14 - expect(await res.json()).toMatchObject({}); 15 }); 16 17 test("no auth key should return 401", async () => {
··· 1 import { expect, test } from "bun:test"; 2 3 import { app } from "@/index"; 4 + import { StatusReportSchema } from "./schema"; 5 6 test("delete the status report", async () => { 7 + // Create a status report we will delete 8 + const date = new Date(); 9 + date.setMilliseconds(0); 10 + 11 + const res = await app.request("/v1/status_report", { 12 + method: "POST", 13 + headers: { 14 + "x-openstatus-key": "1", 15 + "content-type": "application/json", 16 + }, 17 + body: JSON.stringify({ 18 + status: "investigating", 19 + title: "New Status Report", 20 + message: "Message", 21 + monitorIds: [1], 22 + date: date.toISOString(), 23 + pageId: 1, 24 + }), 25 + }); 26 + 27 + const result = StatusReportSchema.safeParse(await res.json()); 28 + 29 + const del = await app.request(`/v1/status_report/${result.data?.id}`, { 30 method: "DELETE", 31 headers: { 32 "x-openstatus-key": "1", 33 }, 34 }); 35 36 + expect(del.status).toBe(200); 37 + expect(await del.json()).toMatchObject({}); 38 }); 39 40 test("no auth key should return 401", async () => {
+1 -1
apps/web/.env.example
··· 15 TINY_BIRD_API_KEY=tiny-bird-api-key 16 17 # TURSO SQLITE 18 - DATABASE_URL=file:./../../openstatus-dev.db 19 DATABASE_AUTH_TOKEN=any-token 20 21 # Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580
··· 15 TINY_BIRD_API_KEY=tiny-bird-api-key 16 17 # TURSO SQLITE 18 + DATABASE_URL=http://127.0.0.1:8080 19 DATABASE_AUTH_TOKEN=any-token 20 21 # Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580
+3 -1
apps/web/package.json
··· 1 { 2 - "name": "web", 3 "version": "1.0.0", 4 "private": true, 5 "scripts": { 6 "dev": "next dev", 7 "build": "next build", 8 "start": "next start", ··· 26 "@openstatus/notification-emails": "workspace:*", 27 "@openstatus/notification-pagerduty": "workspace:*", 28 "@openstatus/notification-slack": "workspace:*", 29 "@openstatus/react": "workspace:*", 30 "@openstatus/tinybird": "workspace:*", 31 "@openstatus/tracker": "workspace:*",
··· 1 { 2 + "name": "@openstatus/web", 3 "version": "1.0.0", 4 "private": true, 5 "scripts": { 6 + "env": "bun env.ts", 7 "dev": "next dev", 8 "build": "next build", 9 "start": "next start", ··· 27 "@openstatus/notification-emails": "workspace:*", 28 "@openstatus/notification-pagerduty": "workspace:*", 29 "@openstatus/notification-slack": "workspace:*", 30 + "@openstatus/notification-opsgenie": "workspace:*", 31 "@openstatus/react": "workspace:*", 32 "@openstatus/tinybird": "workspace:*", 33 "@openstatus/tracker": "workspace:*",
apps/web/public/assets/changelog/raycast-integration.png

This is a binary file and will not be displayed.

+12
apps/web/src/app/api/callback/pagerduty/route.ts
···
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export async function GET(request: Request) { 4 + const { searchParams } = new URL(request.url); 5 + const workspace = searchParams.get("workspace"); 6 + const url = `${ 7 + process.env.NODE_ENV === "development" // FIXME: This sucks 8 + ? "http://localhost:3000" 9 + : "https://www.openstatus.dev" 10 + }/app/${workspace}/notifications/new/pagerduty?${searchParams}`; 11 + redirect(url); 12 + }
+15 -21
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx
··· 8 PaginationState, 9 Row, 10 } from "@tanstack/react-table"; 11 - import { Suspense, use } from "react"; 12 13 import * as assertions from "@openstatus/assertions"; 14 ··· 18 import { LoadingAnimation } from "@/components/loading-animation"; 19 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 20 import type { Trigger } from "@/lib/monitor/utils"; 21 - import { api } from "@/trpc/client"; 22 import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 23 import type { z } from "zod"; 24 ··· 62 } 63 64 function renderSubComponent({ row }: { row: Row<Monitor> }) { 65 - return ( 66 - <Suspense 67 - fallback={ 68 - <div className="py-4"> 69 - <LoadingAnimation variant="inverse" /> 70 - </div> 71 - } 72 - > 73 - <Details row={row} /> 74 - </Suspense> 75 - ); 76 } 77 78 // REMINDER: only HTTP monitors have more details 79 function Details({ row }: { row: Row<Monitor> }) { 80 - const data = use( 81 - api.tinybird.httpGetMonthly.query({ 82 - monitorId: row.original.monitorId, 83 - region: row.original.region, 84 - cronTimestamp: row.original.cronTimestamp || undefined, 85 - }), 86 - ); 87 88 - if (!data.data || data.data.length === 0) return <p>Something went wrong</p>; 89 90 const first = data.data?.[0]; 91
··· 8 PaginationState, 9 Row, 10 } from "@tanstack/react-table"; 11 12 import * as assertions from "@openstatus/assertions"; 13 ··· 17 import { LoadingAnimation } from "@/components/loading-animation"; 18 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 19 import type { Trigger } from "@/lib/monitor/utils"; 20 + import { api } from "@/trpc/rq-client"; 21 import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 22 import type { z } from "zod"; 23 ··· 61 } 62 63 function renderSubComponent({ row }: { row: Row<Monitor> }) { 64 + return <Details row={row} />; 65 } 66 67 // REMINDER: only HTTP monitors have more details 68 function Details({ row }: { row: Row<Monitor> }) { 69 + const { data, isLoading } = api.tinybird.httpGetMonthly.useQuery({ 70 + monitorId: row.original.monitorId, 71 + region: row.original.region, 72 + cronTimestamp: row.original.cronTimestamp || undefined, 73 + }); 74 75 + if (isLoading) 76 + return ( 77 + <div className="py-4"> 78 + <LoadingAnimation variant="inverse" /> 79 + </div> 80 + ); 81 + 82 + if (!data) return <p>Something went wrong</p>; 83 84 const first = data.data?.[0]; 85
+12 -6
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx
··· 1 import type { Workspace } from "@openstatus/db/src/schema"; 2 import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 3 ··· 12 } 13 14 export default function ChannelTable({ workspace, disabled }: ChannelTable) { 15 - const isPagerDutyAllowed = getLimit(workspace.limits, "pagerduty"); 16 - const isSMSAllowed = getLimit(workspace.limits, "sms"); 17 return ( 18 <div className="col-span-full w-full rounded-lg border border-border border-dashed bg-background p-8"> 19 <h2 className="font-cal text-2xl">Channels</h2> ··· 36 <Channel 37 title="PagerDuty" 38 description="Send notifications to PagerDuty." 39 - href={`https://app.pagerduty.com/install/integration?app_id=PN76M56&redirect_url=${ 40 process.env.NODE_ENV === "development" // FIXME: This sucks 41 ? "http://localhost:3000" 42 : "https://www.openstatus.dev" 43 - }/app/${workspace.slug}/notifications/new/pagerduty&version=2`} 44 - disabled={disabled || !isPagerDutyAllowed} 45 /> 46 <Separator /> 47 <Channel ··· 55 title="SMS" 56 description="Send notifications to your phones." 57 href="./notifications/new/sms" 58 - disabled={disabled || !isSMSAllowed} 59 /> 60 </div> 61 </div>
··· 1 + import { env } from "@/env"; 2 import type { Workspace } from "@openstatus/db/src/schema"; 3 import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 4 ··· 13 } 14 15 export default function ChannelTable({ workspace, disabled }: ChannelTable) { 16 return ( 17 <div className="col-span-full w-full rounded-lg border border-border border-dashed bg-background p-8"> 18 <h2 className="font-cal text-2xl">Channels</h2> ··· 35 <Channel 36 title="PagerDuty" 37 description="Send notifications to PagerDuty." 38 + href={`https://app.pagerduty.com/install/integration?app_id=${env.PAGERDUTY_APP_ID}&redirect_url=${ 39 process.env.NODE_ENV === "development" // FIXME: This sucks 40 ? "http://localhost:3000" 41 : "https://www.openstatus.dev" 42 + }/api/callback/pagerduty?workspace=${workspace.slug}&version=2`} 43 + disabled={disabled || !workspace.limits.pagerduty} 44 /> 45 <Separator /> 46 <Channel ··· 54 title="SMS" 55 description="Send notifications to your phones." 56 href="./notifications/new/sms" 57 + disabled={disabled || !workspace.limits.sms} 58 + /> 59 + <Separator /> 60 + <Channel 61 + title="OpsGenie" 62 + description="Send notifications to OpsGenie." 63 + href="./notifications/new/opsgenie" 64 + disabled={disabled || !workspace.limits.opsgenie} 65 /> 66 </div> 67 </div>
+4 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/layout.tsx
··· 24 25 return ( 26 <AppPageWithSidebarLayout id="notifications"> 27 - <Header title={notification.name} description={notification.provider} /> 28 {children} 29 </AppPageWithSidebarLayout> 30 );
··· 24 25 return ( 26 <AppPageWithSidebarLayout id="notifications"> 27 + <Header 28 + title={notification.name} 29 + description={<span className="font-mono">{notification.provider}</span>} 30 + /> 31 {children} 32 </AppPageWithSidebarLayout> 33 );
+2 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx
··· 29 const isLimitReached = 30 await api.notification.isNotificationLimitReached.query(); 31 32 - if (isLimitReached) 33 return <ProFeatureAlert feature="More notification channel" />; 34 35 return ( 36 <NotificationForm
··· 29 const isLimitReached = 30 await api.notification.isNotificationLimitReached.query(); 31 32 + if (isLimitReached) { 33 return <ProFeatureAlert feature="More notification channel" />; 34 + } 35 36 return ( 37 <NotificationForm
+2
apps/web/src/app/status-page/[domain]/subscribe/route.ts
··· 5 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 6 import { SubscribeEmail, sendEmail } from "@openstatus/emails"; 7 8 export async function POST( 9 req: Request, 10 props: { params: Promise<{ domain: string }> },
··· 5 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 6 import { SubscribeEmail, sendEmail } from "@openstatus/emails"; 7 8 + // TODO: use trpc route 9 + 10 export async function POST( 11 req: Request, 12 props: { params: Promise<{ domain: string }> },
+2
apps/web/src/components/data-table/monitor/data-table.tsx
··· 73 pagination, 74 sorting, 75 }, 76 onPaginationChange: setPagination, 77 getPaginationRowModel: getPaginationRowModel(), 78 onColumnFiltersChange: setColumnFilters,
··· 73 pagination, 74 sorting, 75 }, 76 + // @ts-expect-error - REMINDER: unfortunately we cannot pass a function from a RSC to a client component 77 + getRowId: (row, index) => row.monitor?.id?.toString() ?? index, 78 onPaginationChange: setPagination, 79 getPaginationRowModel: getPaginationRowModel(), 80 onColumnFiltersChange: setColumnFilters,
+39 -5
apps/web/src/components/forms/monitor/section-scheduling.tsx
··· 1 "use client"; 2 3 import type { UseFormReturn } from "react-hook-form"; 4 5 import type { InsertMonitor, WorkspacePlan } from "@openstatus/db/src/schema"; 6 import { monitorPeriodicitySchema } from "@openstatus/db/src/schema/constants"; 7 import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 8 9 import { 10 FormControl, 11 FormDescription, ··· 21 } from "@openstatus/ui"; 22 import { groupByContinent } from "@openstatus/utils"; 23 24 - import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 25 import { CheckboxLabel } from "../shared/checkbox-label"; 26 import { SectionHeader } from "../shared/section-header"; 27 import { SelectRegion } from "./select-region"; ··· 108 <FormDescription> 109 Select the regions you want to monitor your endpoint from.{" "} 110 <br /> 111 - {plan === "free" 112 - ? "Only a few regions are available in the free plan. Upgrade to access all regions." 113 - : ""} 114 </FormDescription> 115 <div> 116 {Object.entries(groupByContinent) 117 .sort((a, b) => a[0].localeCompare(b[0])) ··· 183 ); 184 })} 185 </div> 186 - 187 <FormMessage /> 188 </FormItem> 189 ); ··· 192 </div> 193 ); 194 }
··· 1 "use client"; 2 3 + import { Info } from "lucide-react"; 4 import type { UseFormReturn } from "react-hook-form"; 5 6 import type { InsertMonitor, WorkspacePlan } from "@openstatus/db/src/schema"; 7 import { monitorPeriodicitySchema } from "@openstatus/db/src/schema/constants"; 8 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 9 import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 10 11 + import { cn } from "@/lib/utils"; 12 import { 13 FormControl, 14 FormDescription, ··· 24 } from "@openstatus/ui"; 25 import { groupByContinent } from "@openstatus/utils"; 26 27 import { CheckboxLabel } from "../shared/checkbox-label"; 28 import { SectionHeader } from "../shared/section-header"; 29 import { SelectRegion } from "./select-region"; ··· 110 <FormDescription> 111 Select the regions you want to monitor your endpoint from.{" "} 112 <br /> 113 + <span className="text-xs"> 114 + {plan === "free" 115 + ? "Only a few regions are available in the free plan. Upgrade to access all regions." 116 + : ""} 117 + </span> 118 </FormDescription> 119 + <FewRegionsBanner form={form} className="max-w-md" /> 120 <div> 121 {Object.entries(groupByContinent) 122 .sort((a, b) => a[0].localeCompare(b[0])) ··· 188 ); 189 })} 190 </div> 191 <FormMessage /> 192 </FormItem> 193 ); ··· 196 </div> 197 ); 198 } 199 + 200 + // REMINDER: only watch the regions in a new component to avoid re-renders 201 + function FewRegionsBanner({ 202 + form, 203 + className, 204 + }: Pick<Props, "form"> & { className?: string }) { 205 + const watchRegions = form.watch("regions"); 206 + 207 + if (watchRegions?.length && watchRegions.length > 2) return null; 208 + 209 + return ( 210 + <div 211 + className={cn( 212 + "flex items-center gap-3 rounded-lg border border-border px-3 py-2", 213 + className, 214 + )} 215 + > 216 + <Info 217 + className="-mt-0.5 shrink-0 text-status-monitoring" 218 + size={16} 219 + strokeWidth={2} 220 + aria-hidden="true" 221 + /> 222 + <p className="text-sm"> 223 + To minimize false positives, we recommend monitoring your endpoint in at 224 + least 3 regions. 225 + </p> 226 + </div> 227 + ); 228 + }
-81
apps/web/src/components/forms/notification/config.ts
··· 1 - import type { 2 - InsertNotification, 3 - NotificationProvider, 4 - } from "@openstatus/db/src/schema"; 5 - import { allPlans } from "@openstatus/db/src/schema/plan/config"; 6 - import { workspacePlans } from "@openstatus/db/src/schema/workspaces/constants"; 7 - import { sendTestDiscordMessage } from "@openstatus/notification-discord"; 8 - import { sendTestSlackMessage } from "@openstatus/notification-slack"; 9 - export function getDefaultProviderData(defaultValues?: InsertNotification) { 10 - if (!defaultValues?.provider) return ""; // FIXME: input can empty - needs to be undefined 11 - return JSON.parse(defaultValues?.data || "{}")[defaultValues?.provider]; 12 - } 13 - 14 - export function setProviderData(provider: NotificationProvider, data: string) { 15 - return { [provider]: data }; 16 - } 17 - 18 - export function getProviderMetaData(provider: NotificationProvider) { 19 - switch (provider) { 20 - case "email": 21 - return { 22 - label: "Email", 23 - dataType: "email", 24 - placeholder: "dev@documenso.com", 25 - setupDocLink: null, 26 - sendTest: null, 27 - plans: workspacePlans, 28 - }; 29 - 30 - case "slack": 31 - return { 32 - label: "Slack", 33 - dataType: "url", 34 - placeholder: "https://hooks.slack.com/services/xxx...", 35 - setupDocLink: 36 - "https://api.slack.com/messaging/webhooks#getting_started", 37 - sendTest: sendTestSlackMessage, 38 - plans: workspacePlans, 39 - }; 40 - 41 - case "discord": 42 - return { 43 - label: "Discord", 44 - dataType: "url", 45 - placeholder: "https://discord.com/api/webhooks/{channelId}/xxx...", 46 - setupDocLink: "https://support.discord.com/hc/en-us/articles/228383668", 47 - sendTest: sendTestDiscordMessage, 48 - plans: workspacePlans, 49 - }; 50 - case "sms": 51 - return { 52 - label: "SMS", 53 - dataType: "tel", 54 - placeholder: "+123456789", 55 - setupDocLink: null, 56 - sendTest: null, 57 - plans: workspacePlans.filter((plan) => allPlans[plan].limits.sms), 58 - }; 59 - 60 - case "pagerduty": 61 - return { 62 - label: "PagerDuty", 63 - dataType: null, 64 - placeholder: "", 65 - setupDocLink: 66 - "https://docs.openstatus.dev/synthetic/features/notification/pagerduty", 67 - sendTest: null, 68 - plans: workspacePlans.filter((plan) => allPlans[plan].limits.pagerduty), 69 - }; 70 - 71 - default: 72 - return { 73 - label: "Webhook", 74 - dataType: "url", 75 - placeholder: "xxxx", 76 - setupDocLink: `https://docs.openstatus.dev/integrations/${provider}`, 77 - send: null, 78 - plans: workspacePlans, 79 - }; 80 - } 81 - }
···
+14 -13
apps/web/src/components/forms/notification/form.tsx
··· 7 8 import type { 9 InsertNotification, 10 Monitor, 11 NotificationProvider, 12 WorkspacePlan, 13 } from "@openstatus/db/src/schema"; 14 - import { insertNotificationSchema } from "@openstatus/db/src/schema"; 15 import { Badge, Form } from "@openstatus/ui"; 16 17 import { ··· 24 import { api } from "@/trpc/client"; 25 import { TRPCClientError } from "@trpc/client"; 26 import { SaveButton } from "../shared/save-button"; 27 - import { getDefaultProviderData, setProviderData } from "./config"; 28 import { General } from "./general"; 29 import { SectionConnect } from "./section-connect"; 30 ··· 51 }: Props) { 52 const [isPending, startTransition] = useTransition(); 53 const router = useRouter(); 54 - const form = useForm<InsertNotification>({ 55 - resolver: zodResolver(insertNotificationSchema), 56 defaultValues: { 57 ...defaultValues, 58 provider, 59 name: defaultValues?.name || "", 60 - data: getDefaultProviderData(defaultValues), 61 }, 62 }); 63 64 - async function onSubmit({ provider, data, ...rest }: InsertNotification) { 65 startTransition(async () => { 66 try { 67 if (provider === "pagerduty") { 68 if (callbackData) { 69 - data = callbackData; 70 } 71 } 72 - if (data === "") { 73 - form.setError("data", { message: "This field is required" }); 74 - return; 75 - } 76 if (defaultValues) { 77 await api.notification.update.mutate({ 78 provider, 79 - data: JSON.stringify(setProviderData(provider, data)), 80 ...rest, 81 }); 82 } else { 83 await api.notification.create.mutate({ 84 provider, 85 - data: JSON.stringify(setProviderData(provider, data)), 86 ...rest, 87 }); 88 }
··· 7 8 import type { 9 InsertNotification, 10 + InsertNotificationWithData, 11 Monitor, 12 NotificationProvider, 13 WorkspacePlan, 14 } from "@openstatus/db/src/schema"; 15 + import { InsertNotificationWithDataSchema } from "@openstatus/db/src/schema"; 16 import { Badge, Form } from "@openstatus/ui"; 17 18 import { ··· 25 import { api } from "@/trpc/client"; 26 import { TRPCClientError } from "@trpc/client"; 27 import { SaveButton } from "../shared/save-button"; 28 import { General } from "./general"; 29 import { SectionConnect } from "./section-connect"; 30 ··· 51 }: Props) { 52 const [isPending, startTransition] = useTransition(); 53 const router = useRouter(); 54 + const form = useForm<InsertNotificationWithData>({ 55 + resolver: zodResolver(InsertNotificationWithDataSchema), 56 defaultValues: { 57 ...defaultValues, 58 provider, 59 name: defaultValues?.name || "", 60 + data: JSON.parse(defaultValues?.data || "{}"), 61 }, 62 }); 63 64 + async function onSubmit({ 65 + provider, 66 + data, 67 + ...rest 68 + }: InsertNotificationWithData) { 69 + console.log({ provider, data, ...rest }); 70 startTransition(async () => { 71 try { 72 if (provider === "pagerduty") { 73 if (callbackData) { 74 + data.pagerduty = callbackData; 75 } 76 } 77 if (defaultValues) { 78 await api.notification.update.mutate({ 79 provider, 80 + data: JSON.stringify(data), 81 ...rest, 82 }); 83 } else { 84 await api.notification.create.mutate({ 85 provider, 86 + data: JSON.stringify(data), 87 ...rest, 88 }); 89 }
+37 -75
apps/web/src/components/forms/notification/general.tsx
··· 1 "use client"; 2 3 - import { useMemo, useTransition } from "react"; 4 import type { UseFormReturn } from "react-hook-form"; 5 6 import type { 7 - InsertNotification, 8 WorkspacePlan, 9 } from "@openstatus/db/src/schema"; 10 import { 11 - Button, 12 FormControl, 13 FormDescription, 14 FormField, ··· 18 Input, 19 } from "@openstatus/ui"; 20 21 - import { LoadingAnimation } from "@/components/loading-animation"; 22 - import { toastAction } from "@/lib/toast"; 23 import { SectionHeader } from "../shared/section-header"; 24 - import { getProviderMetaData } from "./config"; 25 26 interface Props { 27 - form: UseFormReturn<InsertNotification>; 28 plan: WorkspacePlan; 29 } 30 31 export function General({ form, plan }: Props) { 32 - const [isTestPending, startTestTransition] = useTransition(); 33 const watchProvider = form.watch("provider"); 34 - const watchWebhookUrl = form.watch("data"); 35 - const providerMetaData = useMemo( 36 - () => getProviderMetaData(watchProvider), 37 - [watchProvider], 38 - ); 39 40 - async function sendTestWebhookPing() { 41 - const webhookUrl = form.getValues("data"); 42 - if (!webhookUrl) return; 43 - startTestTransition(async () => { 44 - const isSuccessfull = await providerMetaData.sendTest?.(webhookUrl); 45 - if (isSuccessfull) { 46 - toastAction("test-success"); 47 - } else { 48 - toastAction("test-error"); 49 - } 50 - }); 51 } 52 53 return ( 54 <div className="grid gap-4 sm:grid-cols-3 sm:gap-6"> 55 <SectionHeader 56 title="Alert" 57 - description={`Update your ${providerMetaData.label} settings`} 58 /> 59 <div className="grid gap-4 sm:col-span-2 sm:grid-cols-2"> 60 <FormField ··· 71 </FormItem> 72 )} 73 /> 74 - {providerMetaData.dataType && ( 75 - <FormField 76 - control={form.control} 77 - name="data" 78 - render={({ field }) => ( 79 - <FormItem className="sm:col-span-full"> 80 - <FormLabel>{providerMetaData.label}</FormLabel> 81 - <FormControl> 82 - <Input 83 - type={providerMetaData.dataType} 84 - placeholder={providerMetaData.placeholder} 85 - {...field} 86 - disabled={!providerMetaData.plans?.includes(plan)} 87 - /> 88 - </FormControl> 89 - <FormDescription className="flex items-center justify-between"> 90 - The data is required. 91 - {providerMetaData.setupDocLink && ( 92 - <a 93 - href={providerMetaData.setupDocLink} 94 - target="_blank" 95 - className="underline hover:no-underline" 96 - rel="noreferrer" 97 - > 98 - How to setup your {providerMetaData.label} webhook 99 - </a> 100 - )} 101 - </FormDescription> 102 - <FormMessage /> 103 - </FormItem> 104 - )} 105 - /> 106 - )} 107 - <div className="col-span-full text-right"> 108 - {providerMetaData.sendTest && ( 109 - <Button 110 - type="button" 111 - variant="secondary" 112 - className="w-full sm:w-auto" 113 - disabled={!watchWebhookUrl || isTestPending} 114 - onClick={sendTestWebhookPing} 115 - > 116 - {!isTestPending ? ( 117 - "Test Webhook" 118 - ) : ( 119 - <LoadingAnimation variant="inverse" /> 120 - )} 121 - </Button> 122 - )} 123 - </div> 124 </div> 125 </div> 126 );
··· 1 "use client"; 2 3 + import { useMemo } from "react"; 4 import type { UseFormReturn } from "react-hook-form"; 5 6 import type { 7 + InsertNotificationWithData, 8 WorkspacePlan, 9 } from "@openstatus/db/src/schema"; 10 import { 11 FormControl, 12 FormDescription, 13 FormField, ··· 17 Input, 18 } from "@openstatus/ui"; 19 20 import { SectionHeader } from "../shared/section-header"; 21 + import { SectionDiscord } from "./provider/section-discord"; 22 + import { SectionEmail } from "./provider/section-email"; 23 + import { SectionOpsGenie } from "./provider/section-opsgenie"; 24 + import { SectionPagerDuty } from "./provider/section-pagerduty"; 25 + import { SectionSlack } from "./provider/section-slack"; 26 + import { SectionSms } from "./provider/section-sms"; 27 + 28 + const LABELS = { 29 + slack: "Slack", 30 + discord: "Discord", 31 + sms: "SMS", 32 + pagerduty: "PagerDuty", 33 + opsgenie: "OpsGenie", 34 + email: "Email", 35 + }; 36 37 interface Props { 38 + form: UseFormReturn<InsertNotificationWithData>; 39 plan: WorkspacePlan; 40 } 41 42 export function General({ form, plan }: Props) { 43 const watchProvider = form.watch("provider"); 44 45 + function renderProviderSection() { 46 + switch (watchProvider) { 47 + case "slack": 48 + return <SectionSlack form={form} />; 49 + case "discord": 50 + return <SectionDiscord form={form} />; 51 + case "sms": 52 + return <SectionSms form={form} />; 53 + case "pagerduty": 54 + return <SectionPagerDuty form={form} plan={plan} />; 55 + case "opsgenie": 56 + return <SectionOpsGenie form={form} plan={plan} />; 57 + case "email": 58 + return <SectionEmail form={form} />; 59 + default: 60 + return <div>No provider selected</div>; 61 + } 62 } 63 64 return ( 65 <div className="grid gap-4 sm:grid-cols-3 sm:gap-6"> 66 <SectionHeader 67 title="Alert" 68 + description={`Update your ${LABELS[watchProvider]} settings`} 69 /> 70 <div className="grid gap-4 sm:col-span-2 sm:grid-cols-2"> 71 <FormField ··· 82 </FormItem> 83 )} 84 /> 85 + {renderProviderSection()} 86 </div> 87 </div> 88 );
+19
apps/web/src/components/forms/notification/provider/actions.ts
···
··· 1 + "use server"; 2 + 3 + import { sendTest as sendOpsGenieAlert } from "@openstatus/notification-opsgenie"; 4 + 5 + import { sendTest as sendPagerDutyAlert } from "@openstatus/notification-pagerduty"; 6 + export async function sendOpsGenieTestAlert( 7 + apiKey: string, 8 + region: "us" | "eu", 9 + ) { 10 + const isSuccessfull = await sendOpsGenieAlert({ apiKey, region }); 11 + return isSuccessfull; 12 + } 13 + 14 + export async function sendPagerDutyTestAlert(integrationKey: string) { 15 + const isSuccessfull = await sendPagerDutyAlert({ 16 + integrationKey: integrationKey, 17 + }); 18 + return isSuccessfull; 19 + }
+90
apps/web/src/components/forms/notification/provider/section-discord.tsx
···
··· 1 + "use client"; 2 + 3 + import { useTransition } from "react"; 4 + import type { UseFormReturn } from "react-hook-form"; 5 + 6 + import type { InsertNotificationWithData } from "@openstatus/db/src/schema"; 7 + import { 8 + Button, 9 + FormControl, 10 + FormDescription, 11 + FormField, 12 + FormItem, 13 + FormLabel, 14 + FormMessage, 15 + Input, 16 + } from "@openstatus/ui"; 17 + 18 + import { LoadingAnimation } from "@/components/loading-animation"; 19 + import { toastAction } from "@/lib/toast"; 20 + import { sendTestDiscordMessage } from "@openstatus/notification-discord"; 21 + 22 + interface Props { 23 + form: UseFormReturn<InsertNotificationWithData>; 24 + } 25 + 26 + export function SectionDiscord({ form }: Props) { 27 + const [isTestPending, startTestTransition] = useTransition(); 28 + const watchUrl = form.watch("data.discord"); 29 + 30 + async function sendTestWebhookPing() { 31 + if (!watchUrl) return; 32 + startTestTransition(async () => { 33 + const isSuccessfull = await sendTestDiscordMessage(watchUrl); 34 + if (isSuccessfull) { 35 + toastAction("test-success"); 36 + } else { 37 + toastAction("test-error"); 38 + } 39 + }); 40 + } 41 + 42 + return ( 43 + <> 44 + <FormField 45 + control={form.control} 46 + name="data.discord" 47 + render={({ field }) => ( 48 + <FormItem className="sm:col-span-full"> 49 + <FormLabel>Webhook URL</FormLabel> 50 + <FormControl> 51 + <Input 52 + type="url" 53 + placeholder="https://discord.com/api/webhooks/{channelId}/xxx..." 54 + required 55 + {...field} 56 + /> 57 + </FormControl> 58 + <FormDescription className="flex items-center justify-between"> 59 + The data is required. 60 + <a 61 + href={"https://support.discord.com/hc/en-us/articles/228383668"} 62 + target="_blank" 63 + className="underline hover:no-underline" 64 + rel="noreferrer" 65 + > 66 + How to setup your Discord webhook 67 + </a> 68 + </FormDescription> 69 + <FormMessage /> 70 + </FormItem> 71 + )} 72 + /> 73 + <div className="col-span-full text-right"> 74 + <Button 75 + type="button" 76 + variant="secondary" 77 + className="w-full sm:w-auto" 78 + disabled={isTestPending || !watchUrl} 79 + onClick={sendTestWebhookPing} 80 + > 81 + {!isTestPending ? ( 82 + "Test Webhook" 83 + ) : ( 84 + <LoadingAnimation variant="inverse" /> 85 + )} 86 + </Button> 87 + </div> 88 + </> 89 + ); 90 + }
+44
apps/web/src/components/forms/notification/provider/section-email.tsx
···
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import type { InsertNotificationWithData } from "@openstatus/db/src/schema"; 6 + import { 7 + FormControl, 8 + FormDescription, 9 + FormField, 10 + FormItem, 11 + FormLabel, 12 + FormMessage, 13 + Input, 14 + } from "@openstatus/ui"; 15 + 16 + interface Props { 17 + form: UseFormReturn<InsertNotificationWithData>; 18 + } 19 + 20 + export function SectionEmail({ form }: Props) { 21 + return ( 22 + <FormField 23 + control={form.control} 24 + name="data.email" 25 + render={({ field }) => ( 26 + <FormItem className="sm:col-span-full"> 27 + <FormLabel>Email</FormLabel> 28 + <FormControl> 29 + <Input 30 + type="email" 31 + placeholder="dev@documenso.com" 32 + required 33 + {...field} 34 + /> 35 + </FormControl> 36 + <FormDescription className="flex items-center justify-between"> 37 + The email is required. 38 + </FormDescription> 39 + <FormMessage /> 40 + </FormItem> 41 + )} 42 + /> 43 + ); 44 + }
+124
apps/web/src/components/forms/notification/provider/section-opsgenie.tsx
···
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import { LoadingAnimation } from "@/components/loading-animation"; 6 + import { toastAction } from "@/lib/toast"; 7 + import type { 8 + InsertNotificationWithData, 9 + WorkspacePlan, 10 + } from "@openstatus/db/src/schema"; 11 + import { 12 + Button, 13 + FormControl, 14 + FormDescription, 15 + FormField, 16 + FormItem, 17 + FormLabel, 18 + FormMessage, 19 + Input, 20 + Select, 21 + SelectContent, 22 + SelectItem, 23 + SelectTrigger, 24 + SelectValue, 25 + } from "@openstatus/ui"; 26 + import { useTransition } from "react"; 27 + 28 + import { sendOpsGenieTestAlert } from "./actions"; 29 + 30 + interface Props { 31 + form: UseFormReturn<InsertNotificationWithData>; 32 + plan: WorkspacePlan; 33 + } 34 + 35 + export function SectionOpsGenie({ form, plan }: Props) { 36 + const [isTestPending, startTestTransition] = useTransition(); 37 + const watchApiKey = form.watch("data.opsgenie.apiKey"); 38 + const watchRegion = form.watch("data.opsgenie.region"); 39 + 40 + async function sendTestAlert() { 41 + if (!watchApiKey || !watchRegion) return; 42 + startTestTransition(async () => { 43 + const isSuccessfull = await sendOpsGenieTestAlert( 44 + watchApiKey, 45 + watchRegion, 46 + ); 47 + if (isSuccessfull) { 48 + toastAction("test-success"); 49 + } else { 50 + toastAction("test-error"); 51 + } 52 + }); 53 + } 54 + 55 + return ( 56 + <> 57 + <FormField 58 + control={form.control} 59 + name="data.opsgenie.apiKey" 60 + render={({ field }) => ( 61 + <FormItem className="sm:col-span-full"> 62 + <FormLabel>API Key</FormLabel> 63 + <FormControl> 64 + <Input placeholder={"xxx-yyy-zzz"} {...field} /> 65 + </FormControl> 66 + <FormDescription className="flex items-center justify-between"> 67 + The API key is required. 68 + </FormDescription> 69 + <FormMessage /> 70 + </FormItem> 71 + )} 72 + /> 73 + <FormField 74 + control={form.control} 75 + name="data.opsgenie.region" 76 + render={({ field }) => ( 77 + <FormItem className="sm:col-span-full"> 78 + <FormLabel>Region</FormLabel> 79 + <FormControl> 80 + <Select onValueChange={field.onChange} defaultValue={field.value}> 81 + <FormControl> 82 + <SelectTrigger> 83 + <SelectValue placeholder="Select a region" /> 84 + </SelectTrigger> 85 + </FormControl> 86 + <SelectContent> 87 + <SelectItem value="us">US</SelectItem> 88 + <SelectItem value="eu">EU</SelectItem> 89 + </SelectContent> 90 + </Select> 91 + </FormControl> 92 + <FormDescription className="flex items-center justify-between"> 93 + The region is required. 94 + <a 95 + href="https://docs.openstatus.dev/synthetic/features/notification/opsgenie" 96 + target="_blank" 97 + className="underline hover:no-underline" 98 + rel="noreferrer" 99 + > 100 + How to setup your OpsGenie. 101 + </a> 102 + </FormDescription> 103 + <FormMessage /> 104 + </FormItem> 105 + )} 106 + /> 107 + <div className="col-span-full text-right"> 108 + <Button 109 + type="button" 110 + variant="secondary" 111 + className="w-full sm:w-auto" 112 + disabled={isTestPending || !watchApiKey || !watchRegion} 113 + onClick={sendTestAlert} 114 + > 115 + {!isTestPending ? ( 116 + "Test Alert" 117 + ) : ( 118 + <LoadingAnimation variant="inverse" /> 119 + )} 120 + </Button> 121 + </div> 122 + </> 123 + ); 124 + }
+68
apps/web/src/components/forms/notification/provider/section-pagerduty.tsx
···
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import { LoadingAnimation } from "@/components/loading-animation"; 6 + import { toastAction } from "@/lib/toast"; 7 + import { 8 + type InsertNotificationWithData, 9 + InsertNotificationWithDataSchema, 10 + NotificationDataSchema, 11 + type WorkspacePlan, 12 + selectNotificationSchema, 13 + } from "@openstatus/db/src/schema"; 14 + import { PagerDutySchema } from "@openstatus/notification-pagerduty"; 15 + import { Button } from "@openstatus/ui"; 16 + import { useSearchParams } from "next/navigation"; 17 + import { useTransition } from "react"; 18 + import { sendPagerDutyTestAlert } from "./actions"; 19 + 20 + interface Props { 21 + form: UseFormReturn<InsertNotificationWithData>; 22 + plan: WorkspacePlan; 23 + } 24 + 25 + export function SectionPagerDuty({ form }: Props) { 26 + const [isTestPending, startTestTransition] = useTransition(); 27 + const searchParams = useSearchParams(); 28 + 29 + const config = searchParams.get("config"); 30 + 31 + const result = PagerDutySchema.safeParse(JSON.parse(config || "")); 32 + // We should fix that but that's not working for editing pagerduty notifications 33 + if (result.success === false) { 34 + return null; 35 + } 36 + 37 + const data = result.data; 38 + 39 + if (config) { 40 + form.setValue("data.pagerduty", config); 41 + } 42 + 43 + async function sendTestAlert() { 44 + startTestTransition(async () => { 45 + const isSuccessfull = await sendPagerDutyTestAlert( 46 + data.integration_keys[0].integration_key, 47 + ); 48 + if (isSuccessfull) { 49 + toastAction("test-success"); 50 + } else { 51 + toastAction("test-error"); 52 + } 53 + }); 54 + } 55 + 56 + return ( 57 + <div className="col-span-full text-right"> 58 + <Button 59 + type="button" 60 + variant="secondary" 61 + className="w-full sm:w-auto" 62 + onClick={sendTestAlert} 63 + > 64 + {!isTestPending ? "Test Alert" : <LoadingAnimation variant="inverse" />} 65 + </Button> 66 + </div> 67 + ); 68 + }
+92
apps/web/src/components/forms/notification/provider/section-slack.tsx
···
··· 1 + "use client"; 2 + 3 + import { useTransition } from "react"; 4 + import type { UseFormReturn } from "react-hook-form"; 5 + 6 + import type { InsertNotificationWithData } from "@openstatus/db/src/schema"; 7 + import { 8 + Button, 9 + FormControl, 10 + FormDescription, 11 + FormField, 12 + FormItem, 13 + FormLabel, 14 + FormMessage, 15 + Input, 16 + } from "@openstatus/ui"; 17 + 18 + import { LoadingAnimation } from "@/components/loading-animation"; 19 + import { toastAction } from "@/lib/toast"; 20 + import { sendTestSlackMessage } from "@openstatus/notification-slack"; 21 + 22 + interface Props { 23 + form: UseFormReturn<InsertNotificationWithData>; 24 + } 25 + 26 + export function SectionSlack({ form }: Props) { 27 + const [isTestPending, startTestTransition] = useTransition(); 28 + const watchUrl = form.watch("data.slack"); 29 + 30 + async function sendTestWebhookPing() { 31 + if (!watchUrl) return; 32 + startTestTransition(async () => { 33 + const isSuccessfull = await sendTestSlackMessage(watchUrl); 34 + if (isSuccessfull) { 35 + toastAction("test-success"); 36 + } else { 37 + toastAction("test-error"); 38 + } 39 + }); 40 + } 41 + 42 + return ( 43 + <> 44 + <FormField 45 + control={form.control} 46 + name="data.slack" 47 + render={({ field }) => ( 48 + <FormItem className="sm:col-span-full"> 49 + <FormLabel>Webhook URL</FormLabel> 50 + <FormControl> 51 + <Input 52 + type="url" 53 + placeholder="https://hooks.slack.com/services/xxx..." 54 + required 55 + {...field} 56 + /> 57 + </FormControl> 58 + <FormDescription className="flex items-center justify-between"> 59 + The data is required. 60 + <a 61 + href={ 62 + "https://api.slack.com/messaging/webhooks#getting_started" 63 + } 64 + target="_blank" 65 + className="underline hover:no-underline" 66 + rel="noreferrer" 67 + > 68 + How to setup your Slack webhook 69 + </a> 70 + </FormDescription> 71 + <FormMessage /> 72 + </FormItem> 73 + )} 74 + /> 75 + <div className="col-span-full text-right"> 76 + <Button 77 + type="button" 78 + variant="secondary" 79 + className="w-full sm:w-auto" 80 + disabled={isTestPending || !watchUrl} 81 + onClick={sendTestWebhookPing} 82 + > 83 + {!isTestPending ? ( 84 + "Test Webhook" 85 + ) : ( 86 + <LoadingAnimation variant="inverse" /> 87 + )} 88 + </Button> 89 + </div> 90 + </> 91 + ); 92 + }
+39
apps/web/src/components/forms/notification/provider/section-sms.tsx
···
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import type { InsertNotificationWithData } from "@openstatus/db/src/schema"; 6 + import { 7 + FormControl, 8 + FormDescription, 9 + FormField, 10 + FormItem, 11 + FormLabel, 12 + FormMessage, 13 + Input, 14 + } from "@openstatus/ui"; 15 + 16 + interface Props { 17 + form: UseFormReturn<InsertNotificationWithData>; 18 + } 19 + 20 + export function SectionSms({ form }: Props) { 21 + return ( 22 + <FormField 23 + control={form.control} 24 + name="data.sms" 25 + render={({ field }) => ( 26 + <FormItem className="sm:col-span-full"> 27 + <FormLabel>Phone Number</FormLabel> 28 + <FormControl> 29 + <Input type="tel" placeholder="+1234567890" required {...field} /> 30 + </FormControl> 31 + <FormDescription className="flex items-center justify-between"> 32 + The phone number is required. 33 + </FormDescription> 34 + <FormMessage /> 35 + </FormItem> 36 + )} 37 + /> 38 + ); 39 + }
+5 -2
apps/web/src/components/forms/notification/section-connect.tsx
··· 3 import * as React from "react"; 4 import type { UseFormReturn } from "react-hook-form"; 5 6 - import type { InsertNotification, Monitor } from "@openstatus/db/src/schema"; 7 import { 8 FormControl, 9 FormDescription, ··· 16 import { CheckboxLabel } from "../shared/checkbox-label"; 17 18 interface Props { 19 - form: UseFormReturn<InsertNotification>; 20 monitors?: Monitor[]; 21 } 22
··· 3 import * as React from "react"; 4 import type { UseFormReturn } from "react-hook-form"; 5 6 + import type { 7 + InsertNotificationWithData, 8 + Monitor, 9 + } from "@openstatus/db/src/schema"; 10 import { 11 FormControl, 12 FormDescription, ··· 19 import { CheckboxLabel } from "../shared/checkbox-label"; 20 21 interface Props { 22 + form: UseFormReturn<InsertNotificationWithData>; 23 monitors?: Monitor[]; 24 } 25
+22
apps/web/src/content/changelog/raycast-integration.mdx
···
··· 1 + --- 2 + title: Raycast Extension 3 + description: Use Raycast to manage your monitors, status page and status updates. 4 + image: /assets/changelog/raycast-integration.png 5 + publishedAt: 2025-01-15 6 + --- 7 + 8 + We have published our Raycast integration. You can now manage your monitors, status page and status updates directly from Raycast. 9 + 10 + Here are the commands you can use: 11 + 12 + - Show Monitors 13 + - Create Status Report 14 + - Create Status Report Update 15 + - Show Status Page 16 + 17 + Go to our [Raycast extension](https://www.raycast.com/thibaultleouay/openstatus) page to install it. 18 + 19 + ``` 20 + 21 + ``` 22 +
+1 -2
apps/web/src/lib/auth/index.ts
··· 4 import { Events, setupAnalytics } from "@openstatus/analytics"; 5 import { db, eq } from "@openstatus/db"; 6 import { user } from "@openstatus/db/src/schema"; 7 - import { sendEmail } from "@openstatus/emails/src/send"; 8 9 - import { WelcomeEmail } from "@openstatus/emails/emails/welcome"; 10 import { adapter } from "./adapter"; 11 import { GitHubProvider, GoogleProvider, ResendProvider } from "./providers"; 12
··· 4 import { Events, setupAnalytics } from "@openstatus/analytics"; 5 import { db, eq } from "@openstatus/db"; 6 import { user } from "@openstatus/db/src/schema"; 7 8 + import { WelcomeEmail, sendEmail } from "@openstatus/emails"; 9 import { adapter } from "./adapter"; 10 import { GitHubProvider, GoogleProvider, ResendProvider } from "./providers"; 11
-1
apps/web/src/middleware.ts
··· 155 // "/node_modules/function-bind/**", 156 // "**/node_modules/.pnpm/**/function-bind/**", 157 // "../../packages/analytics/src/index.ts", 158 - "**/node_modules/.pnpm/@jitsu*/**", 159 ], 160 };
··· 155 // "/node_modules/function-bind/**", 156 // "**/node_modules/.pnpm/**/function-bind/**", 157 // "../../packages/analytics/src/index.ts", 158 ], 159 };
+2 -2
apps/workflows/src/scripts/tinybird.ts
··· 28 const date = new Date(); 29 date.setDate(date.getDate() - days); 30 const timestamp = date.getTime(); 31 - console.log(`${days}: ${timestamp}`); 32 return timestamp; 33 } 34 ··· 95 const lastTwoWeeks = calculatePastTimestamp(14); 96 const lastThreeMonths = calculatePastTimestamp(90); 97 const lastYear = calculatePastTimestamp(365); 98 - const _lastTwoYears = calculatePastTimestamp(730); 99 100 const starters = await getWorkspaceIdsByPlan("starter"); 101 const teams = await getWorkspaceIdsByPlan("team");
··· 28 const date = new Date(); 29 date.setDate(date.getDate() - days); 30 const timestamp = date.getTime(); 31 + console.log(`${days}d back: ${timestamp}`); 32 return timestamp; 33 } 34 ··· 95 const lastTwoWeeks = calculatePastTimestamp(14); 96 const lastThreeMonths = calculatePastTimestamp(90); 97 const lastYear = calculatePastTimestamp(365); 98 + // const _lastTwoYears = calculatePastTimestamp(730); 99 100 const starters = await getWorkspaceIdsByPlan("starter"); 101 const teams = await getWorkspaceIdsByPlan("team");
+2 -1
biome.jsonc
··· 3 "files": { 4 "ignore": [ 5 "packages/ui/src/components/*.tsx", 6 - "packages/ui/src/components/*.ts" 7 ] 8 }, 9 "linter": {
··· 3 "files": { 4 "ignore": [ 5 "packages/ui/src/components/*.tsx", 6 + "packages/ui/src/components/*.ts", 7 + ".devbox" 8 ] 9 }, 10 "linter": {
+8
devbox.json
···
··· 1 + { 2 + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.6/.schema/devbox.schema.json", 3 + "packages": ["turso-cli@latest", "nodejs@20", "bun@latest", "sqld@latest"], 4 + "env": { 5 + "DEVBOX_COREPACK_ENABLED": "true", 6 + "COREPACK_ENABLE_DOWNLOAD_PROMPT": "0" 7 + } 8 + }
+214
devbox.lock
···
··· 1 + { 2 + "lockfile_version": "1", 3 + "packages": { 4 + "bun@latest": { 5 + "last_modified": "2025-01-13T08:04:02Z", 6 + "resolved": "github:NixOS/nixpkgs/ef56e777fedaa4da8c66a150081523c5de1e0171#bun", 7 + "source": "devbox-search", 8 + "version": "1.1.43", 9 + "systems": { 10 + "aarch64-darwin": { 11 + "outputs": [ 12 + { 13 + "name": "out", 14 + "path": "/nix/store/wg9x0cb4rl5jrvmkak6ihljdxl811h62-bun-1.1.43", 15 + "default": true 16 + } 17 + ], 18 + "store_path": "/nix/store/wg9x0cb4rl5jrvmkak6ihljdxl811h62-bun-1.1.43" 19 + }, 20 + "aarch64-linux": { 21 + "outputs": [ 22 + { 23 + "name": "out", 24 + "path": "/nix/store/6yszhl10ah05n17lf35kpnyzrqlsg3hl-bun-1.1.43", 25 + "default": true 26 + } 27 + ], 28 + "store_path": "/nix/store/6yszhl10ah05n17lf35kpnyzrqlsg3hl-bun-1.1.43" 29 + }, 30 + "x86_64-darwin": { 31 + "outputs": [ 32 + { 33 + "name": "out", 34 + "path": "/nix/store/4rjyabjj3fj3qd01sv6aii0qcnxb6jyx-bun-1.1.43", 35 + "default": true 36 + } 37 + ], 38 + "store_path": "/nix/store/4rjyabjj3fj3qd01sv6aii0qcnxb6jyx-bun-1.1.43" 39 + }, 40 + "x86_64-linux": { 41 + "outputs": [ 42 + { 43 + "name": "out", 44 + "path": "/nix/store/k8yj5s89swbhlh1i8lp25ha2hw9171yr-bun-1.1.43", 45 + "default": true 46 + } 47 + ], 48 + "store_path": "/nix/store/k8yj5s89swbhlh1i8lp25ha2hw9171yr-bun-1.1.43" 49 + } 50 + } 51 + }, 52 + "nodejs@20": { 53 + "last_modified": "2024-05-03T15:42:32Z", 54 + "plugin_version": "0.0.2", 55 + "resolved": "github:NixOS/nixpkgs/5fd8536a9a5932d4ae8de52b7dc08d92041237fc#nodejs_20", 56 + "source": "devbox-search", 57 + "version": "20.12.2", 58 + "systems": { 59 + "aarch64-darwin": { 60 + "outputs": [ 61 + { 62 + "name": "out", 63 + "path": "/nix/store/844n9zrm2sindml9hvla57shgwavwyjn-nodejs-20.12.2", 64 + "default": true 65 + }, 66 + { 67 + "name": "libv8", 68 + "path": "/nix/store/zsss2v62ff4wxbbv6zizjk6zmlhkqjzh-nodejs-20.12.2-libv8" 69 + } 70 + ], 71 + "store_path": "/nix/store/844n9zrm2sindml9hvla57shgwavwyjn-nodejs-20.12.2" 72 + }, 73 + "aarch64-linux": { 74 + "outputs": [ 75 + { 76 + "name": "out", 77 + "path": "/nix/store/4lpbv14w1bhh1z2v3sjhv9xcp05pgn2d-nodejs-20.12.2", 78 + "default": true 79 + }, 80 + { 81 + "name": "libv8", 82 + "path": "/nix/store/lhiqlp28jmnhqibsr550yf6fqbx5c9c7-nodejs-20.12.2-libv8" 83 + } 84 + ], 85 + "store_path": "/nix/store/4lpbv14w1bhh1z2v3sjhv9xcp05pgn2d-nodejs-20.12.2" 86 + }, 87 + "x86_64-darwin": { 88 + "outputs": [ 89 + { 90 + "name": "out", 91 + "path": "/nix/store/49d07knzwmlaggcq86d98fpzqa70a2ya-nodejs-20.12.2", 92 + "default": true 93 + }, 94 + { 95 + "name": "libv8", 96 + "path": "/nix/store/idmhy1hnfndv43ncd6yy60vdwin1nyfa-nodejs-20.12.2-libv8" 97 + } 98 + ], 99 + "store_path": "/nix/store/49d07knzwmlaggcq86d98fpzqa70a2ya-nodejs-20.12.2" 100 + }, 101 + "x86_64-linux": { 102 + "outputs": [ 103 + { 104 + "name": "out", 105 + "path": "/nix/store/71r8n7ckcpvav9qwshlr12hjd5nlchds-nodejs-20.12.2", 106 + "default": true 107 + }, 108 + { 109 + "name": "libv8", 110 + "path": "/nix/store/zyp86pvn1l7fsw0s7kgv5iwmn14z0vg7-nodejs-20.12.2-libv8" 111 + } 112 + ], 113 + "store_path": "/nix/store/71r8n7ckcpvav9qwshlr12hjd5nlchds-nodejs-20.12.2" 114 + } 115 + } 116 + }, 117 + "sqld@latest": { 118 + "last_modified": "2024-12-03T12:40:06Z", 119 + "resolved": "github:NixOS/nixpkgs/566e53c2ad750c84f6d31f9ccb9d00f823165550#sqld", 120 + "source": "devbox-search", 121 + "version": "0.24.18", 122 + "systems": { 123 + "aarch64-darwin": { 124 + "outputs": [ 125 + { 126 + "name": "out", 127 + "path": "/nix/store/znd6nm9kkpkdwk4db7h6j7fkk5p5k6wa-sqld-0.24.18", 128 + "default": true 129 + } 130 + ], 131 + "store_path": "/nix/store/znd6nm9kkpkdwk4db7h6j7fkk5p5k6wa-sqld-0.24.18" 132 + }, 133 + "aarch64-linux": { 134 + "outputs": [ 135 + { 136 + "name": "out", 137 + "path": "/nix/store/dm4jsc4acixxznds5xdpxg1isd2scg8a-sqld-0.24.18", 138 + "default": true 139 + } 140 + ], 141 + "store_path": "/nix/store/dm4jsc4acixxznds5xdpxg1isd2scg8a-sqld-0.24.18" 142 + }, 143 + "x86_64-darwin": { 144 + "outputs": [ 145 + { 146 + "name": "out", 147 + "path": "/nix/store/rsvrgk3b71b149a0d5hxf7zhcn42d7qm-sqld-0.24.18", 148 + "default": true 149 + } 150 + ], 151 + "store_path": "/nix/store/rsvrgk3b71b149a0d5hxf7zhcn42d7qm-sqld-0.24.18" 152 + }, 153 + "x86_64-linux": { 154 + "outputs": [ 155 + { 156 + "name": "out", 157 + "path": "/nix/store/gfxfxqnj208jhld4m56zab3byhhq63dc-sqld-0.24.18", 158 + "default": true 159 + } 160 + ], 161 + "store_path": "/nix/store/gfxfxqnj208jhld4m56zab3byhhq63dc-sqld-0.24.18" 162 + } 163 + } 164 + }, 165 + "turso-cli@latest": { 166 + "last_modified": "2024-12-23T21:10:33Z", 167 + "resolved": "github:NixOS/nixpkgs/de1864217bfa9b5845f465e771e0ecb48b30e02d#turso-cli", 168 + "source": "devbox-search", 169 + "version": "0.97.2", 170 + "systems": { 171 + "aarch64-darwin": { 172 + "outputs": [ 173 + { 174 + "name": "out", 175 + "path": "/nix/store/0lddbdg4y9fyiqvvlp82zblmmcrnji3c-turso-cli-0.97.2", 176 + "default": true 177 + } 178 + ], 179 + "store_path": "/nix/store/0lddbdg4y9fyiqvvlp82zblmmcrnji3c-turso-cli-0.97.2" 180 + }, 181 + "aarch64-linux": { 182 + "outputs": [ 183 + { 184 + "name": "out", 185 + "path": "/nix/store/dyfpmyx65n26c44dvw6h80aa66s5cabf-turso-cli-0.97.2", 186 + "default": true 187 + } 188 + ], 189 + "store_path": "/nix/store/dyfpmyx65n26c44dvw6h80aa66s5cabf-turso-cli-0.97.2" 190 + }, 191 + "x86_64-darwin": { 192 + "outputs": [ 193 + { 194 + "name": "out", 195 + "path": "/nix/store/b6k6gkc2np7dhlrd6r2347pd2zgp7dg5-turso-cli-0.97.2", 196 + "default": true 197 + } 198 + ], 199 + "store_path": "/nix/store/b6k6gkc2np7dhlrd6r2347pd2zgp7dg5-turso-cli-0.97.2" 200 + }, 201 + "x86_64-linux": { 202 + "outputs": [ 203 + { 204 + "name": "out", 205 + "path": "/nix/store/fxgjr95908fmpysl9wamziaa6lq50kws-turso-cli-0.97.2", 206 + "default": true 207 + } 208 + ], 209 + "store_path": "/nix/store/fxgjr95908fmpysl9wamziaa6lq50kws-turso-cli-0.97.2" 210 + } 211 + } 212 + } 213 + } 214 + }
+1
package.json
··· 3 "scripts": { 4 "build": "turbo run build", 5 "dev": "turbo run dev", 6 "lint": "biome lint .", 7 "format": "pnpm biome format . --write && pnpm biome check . --write ", 8 "lint:fix": "pnpm biome lint --write --unsafe .",
··· 3 "scripts": { 4 "build": "turbo run build", 5 "dev": "turbo run dev", 6 + "env": "bun env.ts", 7 "lint": "biome lint .", 8 "format": "pnpm biome format . --write && pnpm biome check . --write ", 9 "lint:fix": "pnpm biome lint --write --unsafe .",
+2 -2
packages/analytics/.env.example
··· 1 - OPENPANEL_CLIENT_ID= 2 - OPENPANEL_CLIENT_SECRET=
··· 1 + OPENPANEL_CLIENT_ID=something 2 + OPENPANEL_CLIENT_SECRET=something
+2
packages/api/src/env.ts
··· 8 TEAM_ID_VERCEL: z.string(), 9 VERCEL_AUTH_BEARER_TOKEN: z.string(), 10 TINY_BIRD_API_KEY: z.string(), 11 }, 12 13 runtimeEnv: { ··· 16 TEAM_ID_VERCEL: process.env.TEAM_ID_VERCEL, 17 VERCEL_AUTH_BEARER_TOKEN: process.env.VERCEL_AUTH_BEARER_TOKEN, 18 TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY, 19 }, 20 skipValidation: process.env.NODE_ENV === "test", 21 });
··· 8 TEAM_ID_VERCEL: z.string(), 9 VERCEL_AUTH_BEARER_TOKEN: z.string(), 10 TINY_BIRD_API_KEY: z.string(), 11 + RESEND_API_KEY: z.string(), 12 }, 13 14 runtimeEnv: { ··· 17 TEAM_ID_VERCEL: process.env.TEAM_ID_VERCEL, 18 VERCEL_AUTH_BEARER_TOKEN: process.env.VERCEL_AUTH_BEARER_TOKEN, 19 TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY, 20 + RESEND_API_KEY: process.env.RESEND_API_KEY, 21 }, 22 skipValidation: process.env.NODE_ENV === "test", 23 });
+1
packages/api/src/router/monitor.ts
··· 627 with: { 628 monitorTagsToMonitors: { with: { monitorTag: true } }, 629 }, 630 }); 631 632 return z
··· 627 with: { 628 monitorTagsToMonitors: { with: { monitorTag: true } }, 629 }, 630 + orderBy: (monitor, { desc }) => [desc(monitor.active)], 631 }); 632 633 return z
+16
packages/api/src/router/notification.ts
··· 40 }); 41 } 42 43 const _data = NotificationDataSchema.safeParse(JSON.parse(props.data)); 44 if (!_data.success) { 45 throw new TRPCError({ 46 code: "BAD_REQUEST",
··· 40 }); 41 } 42 43 + const limitedProviders = ["sms", "pagerduty", "opsgenie"]; 44 + if (limitedProviders.includes(props.provider)) { 45 + const isAllowed = 46 + opts.ctx.workspace.limits[ 47 + props.provider as "sms" | "pagerduty" | "opsgenie" 48 + ]; 49 + 50 + if (!isAllowed) { 51 + throw new TRPCError({ 52 + code: "FORBIDDEN", 53 + message: "Upgrade to use the notification channel.", 54 + }); 55 + } 56 + } 57 + 58 const _data = NotificationDataSchema.safeParse(JSON.parse(props.data)); 59 + 60 if (!_data.success) { 61 throw new TRPCError({ 62 code: "BAD_REQUEST",
+1 -1
packages/db/.env.example
··· 1 - DATABASE_URL=file:./../../openstatus-dev.db 2 DATABASE_AUTH_TOKEN=any-token
··· 1 + DATABASE_URL=http://127.0.0.1:8080 2 DATABASE_AUTH_TOKEN=any-token
+1
packages/db/src/schema/notifications/constants.ts
··· 4 "slack", 5 "sms", 6 "pagerduty", 7 ] as const;
··· 4 "slack", 5 "sms", 6 "pagerduty", 7 + "opsgenie", 8 ] as const;
+54
packages/db/src/schema/notifications/validation.ts
··· 36 export const emailSchema = z.string().email(); 37 export const urlSchema = z.string().url(); 38 39 export const emailDataSchema = z.object({ email: emailSchema }); 40 export const phoneDataSchema = z.object({ sms: phoneSchema }); 41 export const slackDataSchema = z.object({ slack: urlSchema }); 42 export const discordDataSchema = z.object({ discord: urlSchema }); 43 export const pagerdutyDataSchema = z.object({ pagerduty: z.string() }); 44 45 export const NotificationDataSchema = z.union([ 46 emailDataSchema, ··· 48 slackDataSchema, 49 discordDataSchema, 50 pagerdutyDataSchema, 51 ]);
··· 36 export const emailSchema = z.string().email(); 37 export const urlSchema = z.string().url(); 38 39 + export const webhookDataSchema = z.object({ webhook: urlSchema }); 40 export const emailDataSchema = z.object({ email: emailSchema }); 41 export const phoneDataSchema = z.object({ sms: phoneSchema }); 42 export const slackDataSchema = z.object({ slack: urlSchema }); 43 export const discordDataSchema = z.object({ discord: urlSchema }); 44 export const pagerdutyDataSchema = z.object({ pagerduty: z.string() }); 45 + export const opsgenieDataSchema = z.object({ 46 + opsgenie: z.object({ 47 + apiKey: z.string(), 48 + region: z.enum(["us", "eu"]), 49 + }), 50 + }); 51 52 export const NotificationDataSchema = z.union([ 53 emailDataSchema, ··· 55 slackDataSchema, 56 discordDataSchema, 57 pagerdutyDataSchema, 58 + opsgenieDataSchema, 59 ]); 60 + 61 + export const InsertNotificationWithDataSchema = z.discriminatedUnion( 62 + "provider", 63 + [ 64 + insertNotificationSchema.merge( 65 + z.object({ 66 + provider: z.literal("email"), 67 + data: emailDataSchema, 68 + }), 69 + ), 70 + insertNotificationSchema.merge( 71 + z.object({ 72 + provider: z.literal("sms"), 73 + data: phoneDataSchema, 74 + }), 75 + ), 76 + insertNotificationSchema.merge( 77 + z.object({ 78 + provider: z.literal("slack"), 79 + data: slackDataSchema, 80 + }), 81 + ), 82 + insertNotificationSchema.merge( 83 + z.object({ 84 + provider: z.literal("discord"), 85 + data: discordDataSchema, 86 + }), 87 + ), 88 + insertNotificationSchema.merge( 89 + z.object({ 90 + provider: z.literal("pagerduty"), 91 + data: pagerdutyDataSchema, 92 + }), 93 + ), 94 + insertNotificationSchema.merge( 95 + z.object({ 96 + provider: z.literal("opsgenie"), 97 + data: opsgenieDataSchema, 98 + }), 99 + ), 100 + ], 101 + ); 102 + 103 + export type InsertNotificationWithData = z.infer< 104 + typeof InsertNotificationWithDataSchema 105 + >;
+4
packages/db/src/schema/plan/config.ts
··· 35 notifications: true, 36 sms: false, 37 pagerduty: false, 38 "notification-channels": 1, 39 members: 1, 40 "audit-log": false, ··· 64 "white-label": false, 65 notifications: true, 66 pagerduty: true, 67 sms: true, 68 "notification-channels": 10, 69 members: "Unlimited", ··· 131 notifications: true, 132 sms: true, 133 pagerduty: true, 134 "notification-channels": 20, 135 members: "Unlimited", 136 "audit-log": true, ··· 197 notifications: true, 198 sms: true, 199 pagerduty: true, 200 "notification-channels": 50, 201 members: "Unlimited", 202 "audit-log": true,
··· 35 notifications: true, 36 sms: false, 37 pagerduty: false, 38 + opsgenie: false, 39 "notification-channels": 1, 40 members: 1, 41 "audit-log": false, ··· 65 "white-label": false, 66 notifications: true, 67 pagerduty: true, 68 + opsgenie: true, 69 sms: true, 70 "notification-channels": 10, 71 members: "Unlimited", ··· 133 notifications: true, 134 sms: true, 135 pagerduty: true, 136 + opsgenie: true, 137 "notification-channels": 20, 138 members: "Unlimited", 139 "audit-log": true, ··· 200 notifications: true, 201 sms: true, 202 pagerduty: true, 203 + opsgenie: true, 204 "notification-channels": 50, 205 members: "Unlimited", 206 "audit-log": true,
+1
packages/db/src/schema/plan/schema.ts
··· 37 */ 38 notifications: z.boolean().default(true), 39 pagerduty: z.boolean().default(false), 40 sms: z.boolean().default(false), 41 "notification-channels": z.number().default(1), 42 /**
··· 37 */ 38 notifications: z.boolean().default(true), 39 pagerduty: z.boolean().default(false), 40 + opsgenie: z.boolean().default(false), 41 sms: z.boolean().default(false), 42 "notification-channels": z.number().default(1), 43 /**
+1
packages/db/src/schema/status_reports/validation.ts
··· 52 typeof insertStatusReportUpdateSchema 53 >; 54 export type StatusReportUpdate = z.infer<typeof selectStatusReportUpdateSchema>;
··· 52 typeof insertStatusReportUpdateSchema 53 >; 54 export type StatusReportUpdate = z.infer<typeof selectStatusReportUpdateSchema>; 55 + export type StatusReportStatus = z.infer<typeof statusReportStatusSchema>;
+35 -11
packages/db/src/seed.mts
··· 51 paidUntil: null, 52 }, 53 ]) 54 .run(); 55 56 await db ··· 93 body: '{"hello":"world"}', 94 }, 95 ]) 96 .run(); 97 98 await db ··· 107 customDomain: "", 108 published: true, 109 }) 110 .run(); 111 112 await db ··· 119 email: "ping@openstatus.dev", 120 photoUrl: "", 121 }) 122 .run(); 123 await db 124 .insert(usersToWorkspaces) 125 .values({ workspaceId: 1, userId: 1 }) 126 .run(); 127 128 - await db.insert(monitorsToPages).values({ monitorId: 1, pageId: 1 }).run(); 129 await db 130 .insert(notification) 131 .values({ ··· 135 data: '{"email":"ping@openstatus.dev"}', 136 workspaceId: 1, 137 }) 138 .run(); 139 await db 140 .insert(notificationsToMonitors) 141 .values({ monitorId: 1, notificationId: 1 }) 142 .run(); 143 144 await db ··· 151 status: "investigating", 152 updatedAt: new Date(), 153 }) 154 .run(); 155 156 await db ··· 162 message: "Message", 163 date: new Date(), 164 }) 165 .run(); 166 167 await db ··· 174 status: "investigating", 175 updatedAt: new Date(), 176 }) 177 .run(); 178 179 await db ··· 185 message: "Message", 186 date: new Date(), 187 }) 188 .run(); 189 190 await db ··· 198 to: new Date(Date.now() + 1000), 199 pageId: 1, 200 }) 201 .run(); 202 203 await db ··· 206 maintenanceId: 1, 207 monitorId: 1, 208 }) 209 .run(); 210 211 - await db.insert(monitorsToStatusReport).values([ 212 - { 213 - monitorId: 1, 214 - statusReportId: 2, 215 - }, 216 - { 217 - monitorId: 2, 218 - statusReportId: 2, 219 - }, 220 - ]); 221 222 await db 223 .insert(incidentTable) ··· 228 createdAt: new Date(), 229 startedAt: new Date(), 230 }) 231 .run(); 232 233 await db ··· 239 createdAt: new Date(), 240 startedAt: new Date(Date.now() + 1000), 241 }) 242 .run(); 243 // on status update 244 await db ··· 254 message: "test", 255 date: new Date(), 256 }) 257 .run(); 258 process.exit(0); 259 }
··· 51 paidUntil: null, 52 }, 53 ]) 54 + .onConflictDoNothing() 55 .run(); 56 57 await db ··· 94 body: '{"hello":"world"}', 95 }, 96 ]) 97 + .onConflictDoNothing() 98 .run(); 99 100 await db ··· 109 customDomain: "", 110 published: true, 111 }) 112 + .onConflictDoNothing() 113 .run(); 114 115 await db ··· 122 email: "ping@openstatus.dev", 123 photoUrl: "", 124 }) 125 + .onConflictDoNothing() 126 .run(); 127 await db 128 .insert(usersToWorkspaces) 129 .values({ workspaceId: 1, userId: 1 }) 130 + .onConflictDoNothing() 131 .run(); 132 133 + await db 134 + .insert(monitorsToPages) 135 + .values({ monitorId: 1, pageId: 1 }) 136 + .onConflictDoNothing() 137 + .run(); 138 await db 139 .insert(notification) 140 .values({ ··· 144 data: '{"email":"ping@openstatus.dev"}', 145 workspaceId: 1, 146 }) 147 + .onConflictDoNothing() 148 .run(); 149 await db 150 .insert(notificationsToMonitors) 151 .values({ monitorId: 1, notificationId: 1 }) 152 + .onConflictDoNothing() 153 .run(); 154 155 await db ··· 162 status: "investigating", 163 updatedAt: new Date(), 164 }) 165 + .onConflictDoNothing() 166 .run(); 167 168 await db ··· 174 message: "Message", 175 date: new Date(), 176 }) 177 + .onConflictDoNothing() 178 .run(); 179 180 await db ··· 187 status: "investigating", 188 updatedAt: new Date(), 189 }) 190 + .onConflictDoNothing() 191 .run(); 192 193 await db ··· 199 message: "Message", 200 date: new Date(), 201 }) 202 + .onConflictDoNothing() 203 .run(); 204 205 await db ··· 213 to: new Date(Date.now() + 1000), 214 pageId: 1, 215 }) 216 + .onConflictDoNothing() 217 .run(); 218 219 await db ··· 222 maintenanceId: 1, 223 monitorId: 1, 224 }) 225 + .onConflictDoNothing() 226 .run(); 227 228 + await db 229 + .insert(monitorsToStatusReport) 230 + .values([ 231 + { 232 + monitorId: 1, 233 + statusReportId: 2, 234 + }, 235 + { 236 + monitorId: 2, 237 + statusReportId: 2, 238 + }, 239 + ]) 240 + .onConflictDoNothing() 241 + .run(); 242 243 await db 244 .insert(incidentTable) ··· 249 createdAt: new Date(), 250 startedAt: new Date(), 251 }) 252 + .onConflictDoNothing() 253 .run(); 254 255 await db ··· 261 createdAt: new Date(), 262 startedAt: new Date(Date.now() + 1000), 263 }) 264 + .onConflictDoNothing() 265 .run(); 266 // on status update 267 await db ··· 277 message: "test", 278 date: new Date(), 279 }) 280 + .onConflictDoNothing() 281 .run(); 282 process.exit(0); 283 }
+22
packages/emails/emails/_components/footer.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { Link, Section, Text } from "@react-email/components"; 4 + import { styles } from "./styles"; 5 + 6 + export function Footer() { 7 + return ( 8 + <Section style={{ textAlign: "center" }}> 9 + <Text> 10 + <Link style={styles.link} href="https://openstatus.dev"> 11 + Home Page 12 + </Link>{" "} 13 + ・{" "} 14 + <Link style={styles.link} href="mailto:ping@openstatus.dev"> 15 + Contact Support 16 + </Link> 17 + </Text> 18 + 19 + <Text>OpenStatus ・ 122 Rue Amelot ・ 75011 Paris, France</Text> 20 + </Section> 21 + ); 22 + }
+33
packages/emails/emails/_components/layout.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { Container, Img, Link, Section } from "@react-email/components"; 4 + import type * as React from "react"; 5 + import { Footer } from "./footer"; 6 + import { styles } from "./styles"; 7 + 8 + interface LayoutProps { 9 + children?: React.ReactNode; 10 + img?: { 11 + src: string; 12 + alt: string; 13 + href: string; 14 + }; 15 + } 16 + 17 + const defaultImg = { 18 + src: "https://openstatus.dev/assets/logos/OpenStatus.png", 19 + alt: "OpenStatus", 20 + href: "https://openstatus.dev", 21 + }; 22 + 23 + export function Layout({ children, img = defaultImg }: LayoutProps) { 24 + return ( 25 + <Container style={styles.container}> 26 + <Link href={img.href}> 27 + <Img src={img.src} width="36" height="36" alt={img.alt} /> 28 + </Link> 29 + <Section style={styles.section}>{children}</Section> 30 + <Footer /> 31 + </Container> 32 + ); 33 + }
+42
packages/emails/emails/_components/styles.ts
···
··· 1 + export const colors = { 2 + success: "#51b363", 3 + danger: "#ec6041", 4 + warning: "#ffd60a", 5 + info: "#3d9eff", 6 + }; 7 + 8 + export const styles = { 9 + main: { 10 + backgroundColor: "#ffffff", 11 + color: "#24292e", 12 + fontFamily: 13 + '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', 14 + }, 15 + container: { 16 + maxWidth: "480px", 17 + margin: "0 auto", 18 + padding: "20px 0 48px", 19 + }, 20 + section: { 21 + padding: "24px", 22 + margin: "24px 0", 23 + border: "solid 1px #dedede", 24 + borderRadius: "5px", 25 + }, 26 + button: { 27 + backgroundColor: "#24292e", 28 + color: "#ffffff", 29 + padding: "8px 16px", 30 + borderRadius: "6px", 31 + }, 32 + link: { 33 + textDecoration: "underline", 34 + color: colors.info, 35 + }, 36 + bold: { 37 + fontWeight: "bold", 38 + }, 39 + row: { 40 + borderTop: "1px solid #dedede", 41 + }, 42 + } satisfies Record<string, React.CSSProperties>;
-66
packages/emails/emails/alert.tsx
··· 1 - /** @jsxImportSource react */ 2 - 3 - import { 4 - Body, 5 - Button, 6 - Column, 7 - Container, 8 - Head, 9 - Heading, 10 - Html, 11 - Preview, 12 - Row, 13 - Section, 14 - Text, 15 - } from "@react-email/components"; 16 - import { z } from "zod"; 17 - 18 - export const EmailDataSchema = z.object({ 19 - monitorName: z.string(), 20 - monitorUrl: z.string().url(), 21 - recipientName: z.string(), 22 - }); 23 - 24 - const Alert = ({ data }: { data: z.infer<typeof EmailDataSchema> }) => { 25 - return ( 26 - <Html> 27 - <Head> 28 - <title>New incident detected 🚨</title> 29 - <Preview>New incident detected : {data.monitorName} 🚨</Preview> 30 - <Body className="mx-auto my-auto bg-white font-sans"> 31 - <Container className="mx-auto my-[40px] w-[465px] rounded border border-[#eaeaea] border-solid p-[20px]"> 32 - <Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black"> 33 - New incident detected! 34 - </Heading> 35 - <Text className="text-[14px] text-black leading-[24px]"> 36 - Hello {data.recipientName}, <br /> 37 - We have detected a new incident. 38 - </Text> 39 - 40 - <Section className="my-[30px] rounded border border-gray-200 border-solid bg-gray-100 p-2"> 41 - <Row> 42 - <Column className="text-lg">Monitor</Column> 43 - <Column>{data.monitorName}</Column> 44 - </Row> 45 - <Row className="mt-2"> 46 - <Column className="text-lg">URL</Column> 47 - <Column>{data.monitorUrl}</Column> 48 - </Row> 49 - </Section> 50 - 51 - <Section className="mt-[32px] mb-[32px] text-center"> 52 - <Button 53 - className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[14px] text-white no-underline" 54 - href="https://www.openstatus.dev/app" 55 - > 56 - See incident 57 - </Button> 58 - </Section> 59 - </Container> 60 - </Body> 61 - </Head> 62 - </Html> 63 - ); 64 - }; 65 - 66 - export { Alert };
···
+1 -1
packages/emails/emails/followup.tsx
··· 36 ); 37 }; 38 39 - export { FollowUpEmail };
··· 36 ); 37 }; 38 39 + export default FollowUpEmail;
+168
packages/emails/emails/monitor-alert.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Button, 6 + CodeInline, 7 + Column, 8 + Container, 9 + Head, 10 + Heading, 11 + Html, 12 + Img, 13 + Link, 14 + Preview, 15 + Row, 16 + Text, 17 + } from "@react-email/components"; 18 + import { z } from "zod"; 19 + import { Layout } from "./_components/layout"; 20 + import { colors, styles } from "./_components/styles"; 21 + 22 + const MonitorAlertSchema = z.object({ 23 + type: z.enum(["degraded", "up", "down"]), 24 + name: z.string().optional(), 25 + url: z.string().optional(), 26 + method: z.string().optional(), 27 + status: z.string().optional(), 28 + latency: z.string().optional(), 29 + location: z.string().optional(), 30 + timestamp: z.string().optional(), 31 + }); 32 + 33 + export type MonitorAlertProps = z.infer<typeof MonitorAlertSchema>; 34 + 35 + function getIcon(type: MonitorAlertProps["type"]): { 36 + src: string; 37 + color: string; 38 + } { 39 + switch (type) { 40 + case "up": 41 + return { 42 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNoZWNrIj48cGF0aCBkPSJNMjAgNiA5IDE3bC01LTUiLz48L3N2Zz4=", 43 + color: colors.success, 44 + }; 45 + case "down": 46 + return { 47 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXgiPjxwYXRoIGQ9Ik0xOCA2IDYgMTgiLz48cGF0aCBkPSJtNiA2IDEyIDEyIi8+PC9zdmc+", 48 + color: colors.danger, 49 + }; 50 + case "degraded": 51 + return { 52 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXRyaWFuZ2xlLWFsZXJ0Ij48cGF0aCBkPSJtMjEuNzMgMTgtOC0xNGEyIDIgMCAwIDAtMy40OCAwbC04IDE0QTIgMiAwIDAgMCA0IDIxaDE2YTIgMiAwIDAgMCAxLjczLTMiLz48cGF0aCBkPSJNMTIgOXY0Ii8+PHBhdGggZD0iTTEyIDE3aC4wMSIvPjwvc3ZnPg==", 53 + color: colors.warning, 54 + }; 55 + } 56 + } 57 + 58 + const MonitorAlertEmail = (props: MonitorAlertProps) => ( 59 + <Html> 60 + <Head /> 61 + <Preview> 62 + A fine-grained personal access token has been added to your account 63 + </Preview> 64 + <Body style={styles.main}> 65 + <Layout> 66 + <Container 67 + style={{ 68 + backgroundColor: getIcon(props.type).color, 69 + display: "flex", 70 + alignItems: "center", 71 + justifyContent: "center", 72 + width: "40px", 73 + height: "40px", 74 + borderRadius: "50%", 75 + }} 76 + > 77 + <Img src={getIcon(props.type).src} width="24" height="24" /> 78 + </Container> 79 + <Row> 80 + <Column> 81 + <Heading as="h4">{props.name}</Heading> 82 + </Column> 83 + <Column style={{ textAlign: "right" }}> 84 + <Text 85 + style={{ 86 + color: getIcon(props.type).color, 87 + textTransform: "uppercase", 88 + }} 89 + > 90 + {props.type} 91 + </Text> 92 + </Column> 93 + </Row> 94 + <Row style={styles.row}> 95 + <Column> 96 + <Text style={styles.bold}>Request</Text> 97 + </Column> 98 + <Column 99 + style={{ 100 + textAlign: "right", 101 + flexWrap: "wrap", 102 + wordWrap: "break-word", 103 + maxWidth: "300px", 104 + }} 105 + > 106 + <Text> 107 + <CodeInline>{props.method}</CodeInline> {props.url} 108 + </Text> 109 + </Column> 110 + </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> 127 + <Row style={styles.row}> 128 + <Column> 129 + <Text style={styles.bold}>Location</Text> 130 + </Column> 131 + <Column style={{ textAlign: "right" }}> 132 + <Text>{props.location}</Text> 133 + </Column> 134 + </Row> 135 + <Row style={styles.row}> 136 + <Column> 137 + <Text style={styles.bold}>Timestamp</Text> 138 + </Column> 139 + <Column style={{ textAlign: "right" }}> 140 + <Text>{props.timestamp}</Text> 141 + </Column> 142 + </Row> 143 + <Row style={styles.row}> 144 + <Column> 145 + <Text style={{ textAlign: "center" }}> 146 + <Link style={styles.link} href="https://openstatus.dev/app"> 147 + View details 148 + </Link> 149 + </Text> 150 + </Column> 151 + </Row> 152 + </Layout> 153 + </Body> 154 + </Html> 155 + ); 156 + 157 + MonitorAlertEmail.PreviewProps = { 158 + type: "up", 159 + name: "Ping Pong", 160 + url: "https://openstatus.dev/ping", 161 + method: "GET", 162 + status: "200", 163 + latency: "300ms", 164 + location: "San Francisco", 165 + timestamp: "2021-10-13T17:29:00Z", 166 + } satisfies MonitorAlertProps; 167 + 168 + export default MonitorAlertEmail;
+67
packages/emails/emails/monitor-deactivation.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Button, 6 + Head, 7 + Heading, 8 + Html, 9 + Preview, 10 + Text, 11 + } from "@react-email/components"; 12 + import { z } from "zod"; 13 + import { Layout } from "./_components/layout"; 14 + import { styles } from "./_components/styles"; 15 + 16 + export const MonitorDeactivationSchema = z.object({ 17 + lastLogin: z.coerce.date(), 18 + deactivateAt: z.coerce.date(), 19 + reminder: z.boolean().optional(), 20 + }); 21 + 22 + export type MonitorDeactivationProps = z.infer< 23 + typeof MonitorDeactivationSchema 24 + >; 25 + 26 + const MonitorDeactivationEmail = ({ 27 + lastLogin, 28 + deactivateAt, 29 + reminder, 30 + }: MonitorDeactivationProps) => { 31 + return ( 32 + <Html> 33 + <Head /> 34 + <Preview> 35 + {reminder ? "[REMINDER] " : ""}Deactivation of your monitor(s) 36 + </Preview> 37 + <Body style={styles.main}> 38 + <Layout> 39 + <Heading as="h3">Deactivation of the your monitor(s)</Heading> 40 + <Text>Your last login was {lastLogin.toDateString()}.</Text> 41 + <Text> 42 + To avoid having stale monitors and reduce the number of testing 43 + accounts, we will deactivate your monitor(s) at{" "} 44 + {deactivateAt.toDateString()}. 45 + </Text> 46 + <Text> 47 + If you would like to keep your monitor(s) active, please login to 48 + your account or upgrade your plan. 49 + </Text> 50 + <Text style={{ textAlign: "center" }}> 51 + <Button style={styles.button} href="https://.openstatus.dev/app"> 52 + Login 53 + </Button> 54 + </Text> 55 + </Layout> 56 + </Body> 57 + </Html> 58 + ); 59 + }; 60 + 61 + MonitorDeactivationEmail.PreviewProps = { 62 + lastLogin: new Date(new Date().setDate(new Date().getDate() - 100)), 63 + deactivateAt: new Date(new Date().setDate(new Date().getDate() + 7)), 64 + reminder: true, 65 + } satisfies MonitorDeactivationProps; 66 + 67 + export default MonitorDeactivationEmail;
+75
packages/emails/emails/page-subscription.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Head, 6 + Heading, 7 + Html, 8 + Link, 9 + Preview, 10 + Text, 11 + } from "@react-email/components"; 12 + import { z } from "zod"; 13 + import { Layout } from "./_components/layout"; 14 + import { styles } from "./_components/styles"; 15 + 16 + export const PageSubscriptionSchema = z.object({ 17 + token: z.string(), 18 + page: z.string(), 19 + domain: z.string(), 20 + img: z 21 + .object({ 22 + src: z.string(), 23 + alt: z.string(), 24 + href: z.string(), 25 + }) 26 + .optional(), 27 + }); 28 + 29 + export type PageSubscriptionProps = z.infer<typeof PageSubscriptionSchema>; 30 + 31 + const PageSubscriptionEmail = ({ 32 + token, 33 + page, 34 + domain, 35 + img, 36 + }: PageSubscriptionProps) => { 37 + return ( 38 + <Html> 39 + <Head /> 40 + <Preview>Confirm your subscription to "{page}" Status Page</Preview> 41 + <Body style={styles.main}> 42 + <Layout img={img}> 43 + <Heading as="h3"> 44 + Confirm your subscription to "{page}" Status Page 45 + </Heading> 46 + <Text> 47 + You are receiving this email because you subscribed to receive 48 + updates from "{page}" Status Page. 49 + </Text> 50 + <Text> 51 + To confirm your subscription, please click the link below. The link 52 + is valid for 7 days. If you believe this is a mistake, please ignore 53 + this email. 54 + </Text> 55 + <Text> 56 + <Link 57 + style={styles.link} 58 + href={`https://${domain}.openstatus.dev/verify/${token}`} 59 + > 60 + Confirm subscription 61 + </Link> 62 + </Text> 63 + </Layout> 64 + </Body> 65 + </Html> 66 + ); 67 + }; 68 + 69 + PageSubscriptionEmail.PreviewProps = { 70 + token: "token", 71 + page: "OpenStatus", 72 + domain: "slug", 73 + } satisfies PageSubscriptionProps; 74 + 75 + export default PageSubscriptionEmail;
+121
packages/emails/emails/status-report.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Column, 6 + Head, 7 + Heading, 8 + Html, 9 + Preview, 10 + Row, 11 + Text, 12 + } from "@react-email/components"; 13 + import { z } from "zod"; 14 + import { Layout } from "./_components/layout"; 15 + import { colors, styles } from "./_components/styles"; 16 + 17 + export const StatusReportSchema = z.object({ 18 + pageTitle: z.string(), 19 + status: z.enum(["investigating", "identified", "monitoring", "resolved"]), 20 + date: z.string(), 21 + message: z.string(), 22 + reportTitle: z.string(), 23 + monitors: z.array(z.string()), 24 + }); 25 + 26 + export type StatusReportProps = z.infer<typeof StatusReportSchema>; 27 + 28 + function getStatusColor(status: string) { 29 + switch (status) { 30 + case "investigating": 31 + return colors.danger; 32 + case "identified": 33 + return colors.warning; 34 + case "resolved": 35 + return colors.success; 36 + case "monitoring": 37 + return colors.info; 38 + default: 39 + return colors.success; 40 + } 41 + } 42 + 43 + function StatusReportEmail({ 44 + status, 45 + date, 46 + message, 47 + reportTitle, 48 + pageTitle, 49 + monitors, 50 + }: StatusReportProps) { 51 + return ( 52 + <Html> 53 + <Head /> 54 + <Preview>There are new updates on "{pageTitle}" page</Preview> 55 + <Body style={styles.main}> 56 + <Layout> 57 + <Row> 58 + <Column> 59 + <Heading as="h3">{pageTitle}</Heading> 60 + </Column> 61 + <Column style={{ textAlign: "right" }}> 62 + <Text 63 + style={{ 64 + color: getStatusColor(status), 65 + textTransform: "uppercase", 66 + }} 67 + > 68 + {status} 69 + </Text> 70 + </Column> 71 + </Row> 72 + <Row style={styles.row}> 73 + <Column> 74 + <Text style={styles.bold}>Title</Text> 75 + </Column> 76 + <Column style={{ textAlign: "right" }}> 77 + <Text>{reportTitle}</Text> 78 + </Column> 79 + </Row> 80 + <Row style={styles.row}> 81 + <Column> 82 + <Text style={styles.bold}>Date</Text> 83 + </Column> 84 + <Column style={{ textAlign: "right" }}> 85 + <Text>{date}</Text> 86 + </Column> 87 + </Row> 88 + <Row style={styles.row}> 89 + <Column> 90 + <Text style={styles.bold}>Affected</Text> 91 + </Column> 92 + <Column style={{ textAlign: "right" }}> 93 + <Text style={{ flexWrap: "wrap", wordWrap: "break-word" }}> 94 + {monitors.join(", ")} 95 + </Text> 96 + </Column> 97 + </Row> 98 + <Row style={styles.row}> 99 + <Column> 100 + <Text>{message}</Text> 101 + </Column> 102 + </Row> 103 + </Layout> 104 + </Body> 105 + </Html> 106 + ); 107 + } 108 + 109 + // TODO: add unsubscribe link! 110 + 111 + StatusReportEmail.PreviewProps = { 112 + pageTitle: "OpenStatus Status", 113 + reportTitle: "API Unavaible", 114 + status: "investigating", 115 + date: "2021-07-19", 116 + message: 117 + "The API is down, including the webhook. We are actively investigating the issue and will provide updates as soon as possible.", 118 + monitors: ["OpenStatus API", "OpenStatus Webhook"], 119 + }; 120 + 121 + export default StatusReportEmail;
+15 -9
packages/emails/emails/subscribe.tsx
··· 2 3 import { Body, Head, Html, Link, Preview } from "@react-email/components"; 4 5 - export const SubscribeEmail = ({ 6 - token, 7 - page, 8 - domain, 9 - }: { 10 token: string; 11 page: string; 12 domain: string; 13 - }) => { 14 return ( 15 <Html> 16 <Head> 17 - <title>Confirm your subscription to {page} Status Page</title> 18 - <Preview>Confirm your subscription to {page} Status Page</Preview> 19 <Body> 20 - <p>Confirm your subscription to {page} Status Page</p> 21 <p> 22 You are receiving this email because you subscribed to receive 23 updates from {page} Status Page. ··· 38 </Html> 39 ); 40 };
··· 2 3 import { Body, Head, Html, Link, Preview } from "@react-email/components"; 4 5 + interface SubscribeProps { 6 token: string; 7 page: string; 8 domain: string; 9 + } 10 + 11 + const SubscribeEmail = ({ token, page, domain }: SubscribeProps) => { 12 return ( 13 <Html> 14 <Head> 15 + <title>Confirm your subscription to "{page}" Status Page</title> 16 + <Preview>Confirm your subscription to "{page}" Status Page</Preview> 17 <Body> 18 + <p>Confirm your subscription to "{page}" Status Page</p> 19 <p> 20 You are receiving this email because you subscribed to receive 21 updates from {page} Status Page. ··· 36 </Html> 37 ); 38 }; 39 + 40 + SubscribeEmail.PreviewProps = { 41 + token: "token", 42 + page: "OpenStatus", 43 + domain: "slug", 44 + } satisfies SubscribeProps; 45 + 46 + export default SubscribeEmail;
+63
packages/emails/emails/team-invitation.tsx
···
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Head, 6 + Heading, 7 + Html, 8 + Link, 9 + Preview, 10 + Text, 11 + } from "@react-email/components"; 12 + import { z } from "zod"; 13 + import { Layout } from "./_components/layout"; 14 + import { styles } from "./_components/styles"; 15 + 16 + export const TeamInvitationSchema = z.object({ 17 + invitedBy: z.string(), 18 + workspaceName: z.string().optional().nullable(), 19 + token: z.string(), 20 + }); 21 + 22 + export type TeamInvitationProps = z.infer<typeof TeamInvitationSchema>; 23 + 24 + const TeamInvitationEmail = ({ 25 + token, 26 + workspaceName, 27 + invitedBy, 28 + }: TeamInvitationProps) => { 29 + return ( 30 + <Html> 31 + <Head /> 32 + <Preview>You have been invited to join OpenStatus.dev</Preview> 33 + <Body style={styles.main}> 34 + <Layout> 35 + <Heading as="h3"> 36 + You have been invited to join{" "} 37 + {`"${workspaceName}" workspace` || "OpenStatus.dev"} by {invitedBy} 38 + </Heading> 39 + <Text> 40 + Click here to access the workspace:{" "} 41 + <Link 42 + style={styles.link} 43 + href={`https://openstatus.dev/app/invite?token=${token}`} 44 + > 45 + accept invitation 46 + </Link> 47 + </Text> 48 + <Text> 49 + If you don't have an account yet, it will require you to create one. 50 + </Text> 51 + </Layout> 52 + </Body> 53 + </Html> 54 + ); 55 + }; 56 + 57 + TeamInvitationEmail.PreviewProps = { 58 + token: "token", 59 + workspaceName: "OpenStatus", 60 + invitedBy: "max@openstatus.dev", 61 + } satisfies TeamInvitationProps; 62 + 63 + export default TeamInvitationEmail;
+1 -1
packages/emails/emails/welcome.tsx
··· 57 ); 58 }; 59 60 - export { WelcomeEmail };
··· 57 ); 58 }; 59 60 + export default WelcomeEmail;
+105 -1
packages/emails/src/client.tsx
··· 2 3 import { render } from "@react-email/render"; 4 import { Resend } from "resend"; 5 - import { FollowUpEmail } from "../emails/followup"; 6 7 export class EmailClient { 8 public readonly client: Resend; ··· 31 throw result.error; 32 } catch (err) { 33 console.error(`Error sending follow up email to ${req.to}: ${err}`); 34 } 35 } 36 }
··· 2 3 import { render } from "@react-email/render"; 4 import { Resend } from "resend"; 5 + import FollowUpEmail from "../emails/followup"; 6 + import MonitorAlertEmail from "../emails/monitor-alert"; 7 + import type { MonitorAlertProps } from "../emails/monitor-alert"; 8 + import PageSubscriptionEmail from "../emails/page-subscription"; 9 + import type { PageSubscriptionProps } from "../emails/page-subscription"; 10 + import StatusReportEmail from "../emails/status-report"; 11 + import type { StatusReportProps } from "../emails/status-report"; 12 + import TeamInvitationEmail from "../emails/team-invitation"; 13 + import type { TeamInvitationProps } from "../emails/team-invitation"; 14 15 export class EmailClient { 16 public readonly client: Resend; ··· 39 throw result.error; 40 } catch (err) { 41 console.error(`Error sending follow up email to ${req.to}: ${err}`); 42 + } 43 + } 44 + 45 + public async sendStatusReportUpdate(req: StatusReportProps & { to: string }) { 46 + if (process.env.NODE_ENV === "development") return; 47 + 48 + try { 49 + 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 + }); 56 + 57 + if (!result.error) { 58 + console.log(`Sent status report update email to ${req.to}`); 59 + return; 60 + } 61 + 62 + throw result.error; 63 + } catch (err) { 64 + console.error( 65 + `Error sending status report update email to ${req.to}: ${err}`, 66 + ); 67 + } 68 + } 69 + 70 + public async sendTeamInvitation(req: TeamInvitationProps & { to: string }) { 71 + if (process.env.NODE_ENV === "development") return; 72 + 73 + try { 74 + const html = await render(<TeamInvitationEmail {...req} />); 75 + const result = await this.client.emails.send({ 76 + from: `${req.workspaceName ?? "OpenStatus"} <notifications@openstatus.dev>`, 77 + subject: `You've been invited to join ${req.workspaceName ?? "OpenStatus"}`, 78 + to: req.to, 79 + html, 80 + }); 81 + 82 + if (!result.error) { 83 + console.log(`Sent team invitation email to ${req.to}`); 84 + return; 85 + } 86 + 87 + throw result.error; 88 + } catch (err) { 89 + console.error(`Error sending team invitation email to ${req.to}: ${err}`); 90 + } 91 + } 92 + 93 + public async sendMonitorAlert(req: MonitorAlertProps & { to: string }) { 94 + if (process.env.NODE_ENV === "development") return; 95 + 96 + try { 97 + const html = await render(<MonitorAlertEmail {...req} />); 98 + const result = await this.client.emails.send({ 99 + from: "OpenStatus <notifications@openstatus.dev>", 100 + subject: `${req.name}: ${req.type.toUpperCase()}`, 101 + to: req.to, 102 + html, 103 + }); 104 + 105 + if (!result.error) { 106 + console.log(`Sent monitor alert email to ${req.to}`); 107 + return; 108 + } 109 + 110 + throw result.error; 111 + } catch (err) { 112 + console.error(`Error sending monitor alert to ${req.to}: ${err}`); 113 + } 114 + } 115 + 116 + public async sendPageSubscription( 117 + req: PageSubscriptionProps & { to: string }, 118 + ) { 119 + if (process.env.NODE_ENV === "development") return; 120 + 121 + try { 122 + const html = await render(<PageSubscriptionEmail {...req} />); 123 + const result = await this.client.emails.send({ 124 + from: `${req.page} <notifications@openstatus.dev>`, 125 + subject: "Status page subscription", 126 + to: req.to, 127 + html, 128 + }); 129 + 130 + if (!result.error) { 131 + console.log(`Sent page subscription email to ${req.to}`); 132 + return; 133 + } 134 + 135 + throw result.error; 136 + } catch (err) { 137 + console.error(`Error sending page subscription to ${req.to}: ${err}`); 138 } 139 } 140 }
+5 -16
packages/emails/src/index.ts
··· 1 - import { Alert, EmailDataSchema } from "../emails/alert"; 2 - import { FollowUpEmail } from "../emails/followup"; 3 - import { SubscribeEmail } from "../emails/subscribe"; 4 - import { WelcomeEmail } from "../emails/welcome"; 5 - import { validateEmailNotDisposable } from "./utils"; 6 - 7 - export { 8 - WelcomeEmail, 9 - validateEmailNotDisposable, 10 - Alert, 11 - EmailDataSchema, 12 - SubscribeEmail, 13 - FollowUpEmail, 14 - }; 15 - 16 - export { sendEmail, sendEmailHtml } from "./send"; 17 18 export { EmailClient } from "./client";
··· 1 + export { default as FollowUpEmail } from "../emails/followup"; 2 + export { default as SubscribeEmail } from "../emails/subscribe"; 3 + export { default as WelcomeEmail } from "../emails/welcome"; 4 + export { default as TeamInvitationEmail } from "../emails/team-invitation"; 5 + export { sendEmail, sendEmailHtml, sendBatchEmailHtml } from "./send"; 6 7 export { EmailClient } from "./client";
+2 -1
packages/notifications/discord/package.json
··· 3 "version": "1.0.0", 4 "main": "src/index.ts", 5 "dependencies": { 6 - "@openstatus/db": "workspace:*" 7 }, 8 "devDependencies": { 9 "@openstatus/tsconfig": "workspace:*",
··· 3 "version": "1.0.0", 4 "main": "src/index.ts", 5 "dependencies": { 6 + "@openstatus/db": "workspace:*", 7 + "zod": "3.22.4" 8 }, 9 "devDependencies": { 10 "@openstatus/tsconfig": "workspace:*",
+4 -3
packages/notifications/discord/src/index.ts
··· 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 3 const postToWebhook = async (content: string, webhookUrl: string) => { 4 await fetch(webhookUrl, { ··· 29 incidentId?: string; 30 cronTimestamp: number; 31 }) => { 32 - const notificationData = JSON.parse(notification.data); 33 const { discord: webhookUrl } = notificationData; // webhook url 34 const { name } = monitor; 35 ··· 61 incidentId?: string; 62 cronTimestamp: number; 63 }) => { 64 - const notificationData = JSON.parse(notification.data); 65 const { discord: webhookUrl } = notificationData; // webhook url 66 const { name } = monitor; 67 ··· 93 incidentId?: string; 94 cronTimestamp: number; 95 }) => { 96 - const notificationData = JSON.parse(notification.data); 97 const { discord: webhookUrl } = notificationData; // webhook url 98 const { name } = monitor; 99
··· 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import { DataSchema } from "./schema"; 3 4 const postToWebhook = async (content: string, webhookUrl: string) => { 5 await fetch(webhookUrl, { ··· 30 incidentId?: string; 31 cronTimestamp: number; 32 }) => { 33 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 34 const { discord: webhookUrl } = notificationData; // webhook url 35 const { name } = monitor; 36 ··· 62 incidentId?: string; 63 cronTimestamp: number; 64 }) => { 65 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 66 const { discord: webhookUrl } = notificationData; // webhook url 67 const { name } = monitor; 68 ··· 94 incidentId?: string; 95 cronTimestamp: number; 96 }) => { 97 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 98 const { discord: webhookUrl } = notificationData; // webhook url 99 const { name } = monitor; 100
+5
packages/notifications/discord/src/schema.ts
···
··· 1 + import { z } from "zod"; 2 + 3 + export const DataSchema = z.object({ 4 + discord: z.string(), 5 + });
+19
packages/notifications/opsgenie/package.json
···
··· 1 + { 2 + "name": "@openstatus/notification-opsgenie", 3 + "version": "0.0.0", 4 + "main": "src/index.ts", 5 + "dependencies": { 6 + "@openstatus/db": "workspace:*", 7 + "@t3-oss/env-core": "0.7.1", 8 + "@types/validator": "13.11.6", 9 + "validator": "13.12.0", 10 + "zod": "3.22.4" 11 + }, 12 + "devDependencies": { 13 + "@openstatus/tsconfig": "workspace:*", 14 + "@types/node": "20.8.0", 15 + "@types/react": "18.2.64", 16 + "@types/react-dom": "18.2.21", 17 + "typescript": "5.4.5" 18 + } 19 + }
+183
packages/notifications/opsgenie/src/index.ts
···
··· 1 + import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import { OpsGeniePayloadAlert, OpsGenieSchema } from "./schema"; 3 + 4 + export const sendAlert = async ({ 5 + monitor, 6 + notification, 7 + statusCode, 8 + message, 9 + incidentId, 10 + cronTimestamp, 11 + }: { 12 + monitor: Monitor; 13 + notification: Notification; 14 + statusCode?: number; 15 + message?: string; 16 + incidentId?: string; 17 + cronTimestamp: number; 18 + }) => { 19 + const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 20 + const { name } = monitor; 21 + 22 + const event = OpsGeniePayloadAlert.parse({ 23 + alias: `${monitor.id}}-${incidentId}`, 24 + message: `${name} is down`, 25 + description: message, 26 + details: { 27 + message, 28 + status: statusCode, 29 + severity: "down", 30 + }, 31 + }); 32 + 33 + const url = 34 + opsgenie.region === "eu" 35 + ? "https://api.eu.opsgenie.com/v2/alerts" 36 + : "https://api.opsgenie.com/v2/alerts"; 37 + try { 38 + await fetch(url, { 39 + method: "POST", 40 + body: JSON.stringify(event), 41 + headers: { 42 + "Content-Type": "application/json", 43 + Authorization: `GenieKey ${opsgenie.apiKey}`, 44 + }, 45 + }); 46 + } catch (err) { 47 + console.log(err); 48 + // Do something 49 + } 50 + }; 51 + 52 + export const sendDegraded = async ({ 53 + monitor, 54 + notification, 55 + statusCode, 56 + message, 57 + incidentId, 58 + }: { 59 + monitor: Monitor; 60 + notification: Notification; 61 + statusCode?: number; 62 + message?: string; 63 + incidentId?: string; 64 + cronTimestamp: number; 65 + }) => { 66 + const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 67 + const { name } = monitor; 68 + 69 + const event = OpsGeniePayloadAlert.parse({ 70 + alias: `${monitor.id}}-${incidentId}`, 71 + message: `${name} is down`, 72 + description: message, 73 + details: { 74 + message, 75 + status: statusCode, 76 + severity: "degraded", 77 + }, 78 + }); 79 + 80 + const url = 81 + opsgenie.region === "eu" 82 + ? "https://api.eu.opsgenie.com/v2/alerts" 83 + : "https://api.opsgenie.com/v2/alerts"; 84 + try { 85 + await fetch(url, { 86 + method: "POST", 87 + body: JSON.stringify(event), 88 + headers: { 89 + "Content-Type": "application/json", 90 + Authorization: `GenieKey ${opsgenie.apiKey}`, 91 + }, 92 + }); 93 + } catch (err) { 94 + console.log(err); 95 + // Do something 96 + 97 + // Do something 98 + } 99 + }; 100 + 101 + export const sendRecovery = async ({ 102 + monitor, 103 + notification, 104 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 105 + statusCode, 106 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 107 + message, 108 + incidentId, 109 + }: { 110 + monitor: Monitor; 111 + notification: Notification; 112 + statusCode?: number; 113 + message?: string; 114 + incidentId?: string; 115 + cronTimestamp: number; 116 + }) => { 117 + const { opsgenie } = OpsGenieSchema.parse(JSON.parse(notification.data)); 118 + 119 + const url = 120 + opsgenie.region === "eu" 121 + ? `https://api.eu.opsgenie.com/v2/alerts/${monitor.id}}-${incidentId}/close` 122 + : `https://api.opsgenie.com/v2/alerts/${monitor.id}}-${incidentId}/close`; 123 + 124 + const event = OpsGeniePayloadAlert.parse({ 125 + alias: `${monitor.id}}-${incidentId}`, 126 + message: `${monitor.name} has recovered`, 127 + description: message, 128 + details: { 129 + message, 130 + status: statusCode, 131 + }, 132 + }); 133 + try { 134 + await fetch(url, { 135 + method: "POST", 136 + body: JSON.stringify(event), 137 + headers: { 138 + "Content-Type": "application/json", 139 + Authorization: `GenieKey ${opsgenie.apiKey}`, 140 + }, 141 + }); 142 + } catch (err) { 143 + console.log(err); 144 + // Do something 145 + } 146 + }; 147 + export const sendTest = async (props: { 148 + apiKey: string; 149 + region: "eu" | "us"; 150 + }) => { 151 + const { apiKey, region } = props; 152 + 153 + const url = 154 + region === "eu" 155 + ? "https://api.eu.opsgenie.com/v2/alerts" 156 + : "https://api.opsgenie.com/v2/alerts"; 157 + 158 + const alert = OpsGeniePayloadAlert.parse({ 159 + alias: "test-openstatus", 160 + message: "Test Alert <OpenStatus>", 161 + description: 162 + "If you can read this, your OpsGenie integration is functioning correctly! Please ignore this alert and delete it.", 163 + }); 164 + 165 + try { 166 + const res = await fetch(url, { 167 + method: "POST", 168 + body: JSON.stringify(alert), 169 + headers: { 170 + "Content-Type": "application/json", 171 + Authorization: `GenieKey ${apiKey}`, 172 + "Access-Control-Allow-Origin": "*", 173 + }, 174 + }); 175 + console.log(await res.json()); 176 + return true; 177 + } catch (err) { 178 + console.log(err); 179 + return false; 180 + } 181 + }; 182 + 183 + export { OpsGenieSchema };
+26
packages/notifications/opsgenie/src/schema.ts
···
··· 1 + import { z } from "zod"; 2 + 3 + export const OpsGenieSchema = z.object({ 4 + opsgenie: z.object({ 5 + apiKey: z.string(), 6 + region: z.enum(["eu", "us"]), 7 + }), 8 + }); 9 + 10 + export const OpsGeniePayloadAlert = z.object({ 11 + message: z.string(), 12 + alias: z.string(), 13 + description: z.string(), 14 + source: z.string().default("OpenStatus"), 15 + details: z 16 + .object({ 17 + message: z.string(), 18 + status: z.number().optional(), 19 + severity: z.enum(["degraded", "down"]), 20 + }) 21 + .optional(), 22 + }); 23 + 24 + export const OpsGenieCloseAlert = z.object({ 25 + source: z.string().default("OpenStatus"), 26 + });
+4
packages/notifications/opsgenie/tsconfig.json
···
··· 1 + { 2 + "extends": "@openstatus/tsconfig/nextjs.json", 3 + "include": ["src", "*.ts"] 4 + }
+51 -19
packages/notifications/pagerduty/src/index.ts
··· 24 const notificationData = PagerDutySchema.parse(JSON.parse(notification.data)); 25 const { name } = monitor; 26 27 - const event = triggerEventPayloadSchema.parse({ 28 - rounting_key: notificationData.integration_keys[0].integration_key, 29 - dedup_key: `${monitor.id}}-${incidentId}`, 30 - event_action: "trigger", 31 - payload: { 32 - summary: `${name} is down`, 33 - source: "Open Status", 34 - severity: "error", 35 - timestamp: new Date(cronTimestamp).toISOString(), 36 - custom_details: { 37 - statusCode, 38 - message, 39 - }, 40 - }, 41 - }); 42 - 43 try { 44 for await (const integrationKey of notificationData.integration_keys) { 45 // biome-ignore lint/correctness/noUnusedVariables: <explanation> 46 const { integration_key, type } = integrationKey; 47 - 48 await fetch("https://events.pagerduty.com/v2/enqueue", { 49 method: "POST", 50 body: JSON.stringify(event), ··· 73 const { name } = monitor; 74 75 const event = triggerEventPayloadSchema.parse({ 76 - rounting_key: notificationData.integration_keys[0].integration_key, 77 dedup_key: `${monitor.id}}`, 78 event_action: "trigger", 79 payload: { ··· 125 try { 126 for await (const integrationKey of notificationData.integration_keys) { 127 const event = resolveEventPayloadSchema.parse({ 128 - rounting_key: integrationKey.integration_key, 129 dedup_key: `${monitor.id}}-${incidentId}`, 130 event_action: "resolve", 131 }); ··· 140 } 141 }; 142 143 export { PagerDutySchema };
··· 24 const notificationData = PagerDutySchema.parse(JSON.parse(notification.data)); 25 const { name } = monitor; 26 27 try { 28 for await (const integrationKey of notificationData.integration_keys) { 29 // biome-ignore lint/correctness/noUnusedVariables: <explanation> 30 const { integration_key, type } = integrationKey; 31 + const event = triggerEventPayloadSchema.parse({ 32 + routing_key: integration_key, 33 + dedup_key: `${monitor.id}}-${incidentId}`, 34 + event_action: "trigger", 35 + payload: { 36 + summary: `${name} is down`, 37 + source: "Open Status", 38 + severity: "error", 39 + timestamp: new Date(cronTimestamp).toISOString(), 40 + custom_details: { 41 + statusCode, 42 + message, 43 + }, 44 + }, 45 + }); 46 await fetch("https://events.pagerduty.com/v2/enqueue", { 47 method: "POST", 48 body: JSON.stringify(event), ··· 71 const { name } = monitor; 72 73 const event = triggerEventPayloadSchema.parse({ 74 + routing_key: notificationData.integration_keys[0].integration_key, 75 dedup_key: `${monitor.id}}`, 76 event_action: "trigger", 77 payload: { ··· 123 try { 124 for await (const integrationKey of notificationData.integration_keys) { 125 const event = resolveEventPayloadSchema.parse({ 126 + routing_key: integrationKey.integration_key, 127 dedup_key: `${monitor.id}}-${incidentId}`, 128 event_action: "resolve", 129 }); ··· 138 } 139 }; 140 141 + export const sendTest = async ({ 142 + integrationKey, 143 + }: { 144 + integrationKey: string; 145 + }) => { 146 + console.log("Sending test alert to PagerDuty"); 147 + try { 148 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 149 + const event = triggerEventPayloadSchema.parse({ 150 + routing_key: integrationKey, 151 + dedup_key: "openstatus-test", 152 + event_action: "trigger", 153 + payload: { 154 + summary: "This is a test from OpenStatus", 155 + source: "Open Status", 156 + severity: "error", 157 + timestamp: new Date().toISOString(), 158 + custom_details: { 159 + statusCode: 418, 160 + message: 'I"m a teapot', 161 + }, 162 + }, 163 + }); 164 + 165 + const res = await fetch("https://events.pagerduty.com/v2/enqueue", { 166 + method: "POST", 167 + body: JSON.stringify(event), 168 + }); 169 + } catch (err) { 170 + console.log(err); 171 + return false; 172 + } 173 + return true; 174 + }; 175 export { PagerDutySchema };
+1 -1
packages/notifications/pagerduty/src/schema/config.ts
··· 37 }); 38 39 const baseEventPayloadSchema = z.object({ 40 - rounting_key: z.string(), 41 dedup_key: z.string(), 42 }); 43
··· 37 }); 38 39 const baseEventPayloadSchema = z.object({ 40 + routing_key: z.string(), 41 dedup_key: z.string(), 42 }); 43
+2 -1
packages/notifications/slack/package.json
··· 3 "version": "0.0.0", 4 "main": "src/index.ts", 5 "dependencies": { 6 - "@openstatus/db": "workspace:*" 7 }, 8 "devDependencies": { 9 "@openstatus/tsconfig": "workspace:*",
··· 3 "version": "0.0.0", 4 "main": "src/index.ts", 5 "dependencies": { 6 + "@openstatus/db": "workspace:*", 7 + "zod": "3.22.4" 8 }, 9 "devDependencies": { 10 "@openstatus/tsconfig": "workspace:*",
+4 -3
packages/notifications/slack/src/index.ts
··· 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 3 // biome-ignore lint/suspicious/noExplicitAny: <explanation> 4 const postToWebhook = async (body: any, webhookUrl: string) => { ··· 29 incidentId?: string; 30 cronTimestamp: number; 31 }) => { 32 - const notificationData = JSON.parse(notification.data); 33 const { slack: webhookUrl } = notificationData; // webhook url 34 const { name } = monitor; 35 ··· 88 incidentId?: string; 89 cronTimestamp: number; 90 }) => { 91 - const notificationData = JSON.parse(notification.data); 92 const { slack: webhookUrl } = notificationData; // webhook url 93 const { name } = monitor; 94 ··· 139 message?: string; 140 cronTimestamp: number; 141 }) => { 142 - const notificationData = JSON.parse(notification.data); 143 const { slack: webhookUrl } = notificationData; // webhook url 144 const { name } = monitor; 145
··· 1 import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + import { DataSchema } from "./schema"; 3 4 // biome-ignore lint/suspicious/noExplicitAny: <explanation> 5 const postToWebhook = async (body: any, webhookUrl: string) => { ··· 30 incidentId?: string; 31 cronTimestamp: number; 32 }) => { 33 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 34 const { slack: webhookUrl } = notificationData; // webhook url 35 const { name } = monitor; 36 ··· 89 incidentId?: string; 90 cronTimestamp: number; 91 }) => { 92 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 93 const { slack: webhookUrl } = notificationData; // webhook url 94 const { name } = monitor; 95 ··· 140 message?: string; 141 cronTimestamp: number; 142 }) => { 143 + const notificationData = DataSchema.parse(JSON.parse(notification.data)); 144 const { slack: webhookUrl } = notificationData; // webhook url 145 const { name } = monitor; 146
+5
packages/notifications/slack/src/schema.ts
···
··· 1 + import { z } from "zod"; 2 + 3 + export const DataSchema = z.object({ 4 + slack: z.string(), 5 + });
+5 -5
packages/tinybird/src/client.ts
··· 9 private readonly tb: Client; 10 11 constructor(token: string) { 12 - // if (process.env.NODE_ENV === "development") { 13 - // this.tb = new NoopTinybird(); 14 - // } else { 15 - this.tb = new Client({ token }); 16 - // } 17 } 18 19 public get homeStats() {
··· 9 private readonly tb: Client; 10 11 constructor(token: string) { 12 + if (process.env.NODE_ENV === "development") { 13 + this.tb = new NoopTinybird(); 14 + } else { 15 + this.tb = new Client({ token }); 16 + } 17 } 18 19 public get homeStats() {
+66 -65
pnpm-lock.yaml
··· 142 '@openstatus/notification-emails': 143 specifier: workspace:* 144 version: link:../../packages/notifications/email 145 '@openstatus/notification-pagerduty': 146 specifier: workspace:* 147 version: link:../../packages/notifications/pagerduty ··· 248 '@openstatus/notification-emails': 249 specifier: workspace:* 250 version: link:../../packages/notifications/email 251 '@openstatus/notification-pagerduty': 252 specifier: workspace:* 253 version: link:../../packages/notifications/pagerduty ··· 277 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) 278 '@sentry/nextjs': 279 specifier: 8.46.0 280 - 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.1(@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)) 281 '@stripe/stripe-js': 282 specifier: 2.1.6 283 version: 2.1.6 ··· 304 version: 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 305 '@trpc/next': 306 specifier: 11.0.0-rc.666 307 - 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.1(@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) 308 '@trpc/react-query': 309 specifier: 11.0.0-rc.666 310 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) ··· 352 version: 5.0.7 353 next: 354 specifier: 15.1.1 355 - version: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 356 next-auth: 357 specifier: 5.0.0-beta.25 358 - version: 5.0.0-beta.25(next@15.1.1(@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) 359 next-plausible: 360 specifier: 3.12.4 361 - version: 3.12.4(next@15.1.1(@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) 362 next-themes: 363 specifier: 0.2.1 364 - version: 0.2.1(next@15.1.1(@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) 365 nuqs: 366 specifier: 2.2.3 367 - version: 2.2.3(next@15.1.1(@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) 368 random-word-slugs: 369 specifier: 0.1.7 370 version: 0.1.7 ··· 434 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) 435 '@content-collections/next': 436 specifier: 0.2.4 437 - version: 0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) 438 '@openstatus/tsconfig': 439 specifier: workspace:* 440 version: link:../../packages/tsconfig ··· 721 '@openstatus/db': 722 specifier: workspace:* 723 version: link:../../db 724 devDependencies: 725 '@openstatus/tsconfig': 726 specifier: workspace:* ··· 769 specifier: 5.6.2 770 version: 5.6.2 771 772 packages/notifications/pagerduty: 773 dependencies: 774 '@openstatus/db': ··· 808 '@openstatus/db': 809 specifier: workspace:* 810 version: link:../../db 811 devDependencies: 812 '@openstatus/tsconfig': 813 specifier: workspace:* ··· 11417 - acorn 11418 - supports-color 11419 11420 - '@content-collections/next@0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': 11421 dependencies: 11422 '@content-collections/core': 0.7.3(typescript@5.6.2) 11423 '@content-collections/integrations': 0.2.1(@content-collections/core@0.7.3(typescript@5.6.2)) 11424 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 11425 11426 '@cspotcode/source-map-support@0.8.1': 11427 dependencies: ··· 13996 '@sentry/types': 8.9.2 13997 '@sentry/utils': 8.9.2 13998 13999 - '@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.1(@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))': 14000 dependencies: 14001 '@opentelemetry/api': 1.9.0 14002 '@opentelemetry/semantic-conventions': 1.28.0 ··· 14009 '@sentry/vercel-edge': 8.46.0 14010 '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 14011 chalk: 3.0.0 14012 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14013 resolve: 1.22.8 14014 rollup: 3.29.5 14015 stacktrace-parser: 0.1.10 ··· 14629 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14630 typescript: 5.6.2 14631 14632 - '@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.1(@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)': 14633 dependencies: 14634 '@trpc/client': 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 14635 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14636 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14637 react: 19.0.0 14638 react-dom: 19.0.0(react@19.0.0) 14639 typescript: 5.6.2 ··· 17527 17528 jest-worker@27.5.1: 17529 dependencies: 17530 - '@types/node': 20.14.8 17531 merge-stream: 2.0.0 17532 supports-color: 8.1.1 17533 ··· 18600 18601 netmask@2.0.2: {} 18602 18603 - next-auth@5.0.0-beta.25(next@15.1.1(@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): 18604 - dependencies: 18605 - '@auth/core': 0.37.2 18606 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18607 - react: 19.0.0 18608 - 18609 next-auth@5.0.0-beta.25(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18610 dependencies: 18611 '@auth/core': 0.37.2 18612 next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18613 react: 19.0.0 18614 18615 - next-plausible@3.12.4(next@15.1.1(@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): 18616 - dependencies: 18617 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18618 - react: 19.0.0 18619 - react-dom: 19.0.0(react@19.0.0) 18620 - 18621 - next-themes@0.2.1(next@15.1.1(@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): 18622 dependencies: 18623 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18624 react: 19.0.0 18625 react-dom: 19.0.0(react@19.0.0) 18626 ··· 18656 - '@babel/core' 18657 - babel-plugin-macros 18658 18659 - next@15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18660 - dependencies: 18661 - '@next/env': 15.1.1 18662 - '@swc/counter': 0.1.3 18663 - '@swc/helpers': 0.5.15 18664 - busboy: 1.6.0 18665 - caniuse-lite: 1.0.30001689 18666 - postcss: 8.4.31 18667 - react: 19.0.0 18668 - react-dom: 19.0.0(react@19.0.0) 18669 - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) 18670 - optionalDependencies: 18671 - '@next/swc-darwin-arm64': 15.1.1 18672 - '@next/swc-darwin-x64': 15.1.1 18673 - '@next/swc-linux-arm64-gnu': 15.1.1 18674 - '@next/swc-linux-arm64-musl': 15.1.1 18675 - '@next/swc-linux-x64-gnu': 15.1.1 18676 - '@next/swc-linux-x64-musl': 15.1.1 18677 - '@next/swc-win32-arm64-msvc': 15.1.1 18678 - '@next/swc-win32-x64-msvc': 15.1.1 18679 - '@opentelemetry/api': 1.9.0 18680 - sharp: 0.33.5 18681 - transitivePeerDependencies: 18682 - - '@babel/core' 18683 - - babel-plugin-macros 18684 - 18685 next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18686 dependencies: 18687 '@next/env': 15.1.1 ··· 18781 dependencies: 18782 boolbase: 1.0.0 18783 18784 - nuqs@2.2.3(next@15.1.1(@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): 18785 dependencies: 18786 mitt: 3.0.1 18787 react: 19.0.0 18788 optionalDependencies: 18789 - next: 15.1.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18790 18791 oauth4webapi@2.10.4: {} 18792 ··· 20334 optionalDependencies: 20335 '@babel/core': 7.24.5 20336 20337 - styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): 20338 - dependencies: 20339 - client-only: 0.0.1 20340 - react: 19.0.0 20341 - optionalDependencies: 20342 - '@babel/core': 7.26.0 20343 - 20344 sucrase@3.34.0: 20345 dependencies: 20346 '@jridgewell/gen-mapping': 0.3.5 ··· 20672 '@tsconfig/node14': 1.0.3 20673 '@tsconfig/node16': 1.0.4 20674 '@types/node': 20.14.8 20675 - acorn: 8.14.0 20676 acorn-walk: 8.3.2 20677 arg: 4.1.3 20678 create-require: 1.1.1
··· 142 '@openstatus/notification-emails': 143 specifier: workspace:* 144 version: link:../../packages/notifications/email 145 + '@openstatus/notification-opsgenie': 146 + specifier: workspace:* 147 + version: link:../../packages/notifications/opsgenie 148 '@openstatus/notification-pagerduty': 149 specifier: workspace:* 150 version: link:../../packages/notifications/pagerduty ··· 251 '@openstatus/notification-emails': 252 specifier: workspace:* 253 version: link:../../packages/notifications/email 254 + '@openstatus/notification-opsgenie': 255 + specifier: workspace:* 256 + version: link:../../packages/notifications/opsgenie 257 '@openstatus/notification-pagerduty': 258 specifier: workspace:* 259 version: link:../../packages/notifications/pagerduty ··· 283 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) 284 '@sentry/nextjs': 285 specifier: 8.46.0 286 + 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.1(@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)) 287 '@stripe/stripe-js': 288 specifier: 2.1.6 289 version: 2.1.6 ··· 310 version: 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 311 '@trpc/next': 312 specifier: 11.0.0-rc.666 313 + 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.1(@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) 314 '@trpc/react-query': 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/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) ··· 358 version: 5.0.7 359 next: 360 specifier: 15.1.1 361 + version: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 362 next-auth: 363 specifier: 5.0.0-beta.25 364 + version: 5.0.0-beta.25(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 365 next-plausible: 366 specifier: 3.12.4 367 + version: 3.12.4(next@15.1.1(@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) 368 next-themes: 369 specifier: 0.2.1 370 + version: 0.2.1(next@15.1.1(@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 nuqs: 372 specifier: 2.2.3 373 + version: 2.2.3(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) 374 random-word-slugs: 375 specifier: 0.1.7 376 version: 0.1.7 ··· 440 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) 441 '@content-collections/next': 442 specifier: 0.2.4 443 + version: 0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) 444 '@openstatus/tsconfig': 445 specifier: workspace:* 446 version: link:../../packages/tsconfig ··· 727 '@openstatus/db': 728 specifier: workspace:* 729 version: link:../../db 730 + zod: 731 + specifier: 3.22.4 732 + version: 3.22.4 733 devDependencies: 734 '@openstatus/tsconfig': 735 specifier: workspace:* ··· 778 specifier: 5.6.2 779 version: 5.6.2 780 781 + packages/notifications/opsgenie: 782 + dependencies: 783 + '@openstatus/db': 784 + specifier: workspace:* 785 + version: link:../../db 786 + '@t3-oss/env-core': 787 + specifier: 0.7.1 788 + version: 0.7.1(typescript@5.4.5)(zod@3.22.4) 789 + '@types/validator': 790 + specifier: 13.11.6 791 + version: 13.11.6 792 + validator: 793 + specifier: 13.12.0 794 + version: 13.12.0 795 + zod: 796 + specifier: 3.22.4 797 + version: 3.22.4 798 + devDependencies: 799 + '@openstatus/tsconfig': 800 + specifier: workspace:* 801 + version: link:../../tsconfig 802 + '@types/node': 803 + specifier: 20.8.0 804 + version: 20.8.0 805 + '@types/react': 806 + specifier: 18.2.64 807 + version: 18.2.64 808 + '@types/react-dom': 809 + specifier: 18.2.21 810 + version: 18.2.21 811 + typescript: 812 + specifier: 5.4.5 813 + version: 5.4.5 814 + 815 packages/notifications/pagerduty: 816 dependencies: 817 '@openstatus/db': ··· 851 '@openstatus/db': 852 specifier: workspace:* 853 version: link:../../db 854 + zod: 855 + specifier: 3.22.4 856 + version: 3.22.4 857 devDependencies: 858 '@openstatus/tsconfig': 859 specifier: workspace:* ··· 11463 - acorn 11464 - supports-color 11465 11466 + '@content-collections/next@0.2.4(@content-collections/core@0.7.3(typescript@5.6.2))(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': 11467 dependencies: 11468 '@content-collections/core': 0.7.3(typescript@5.6.2) 11469 '@content-collections/integrations': 0.2.1(@content-collections/core@0.7.3(typescript@5.6.2)) 11470 + next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 11471 11472 '@cspotcode/source-map-support@0.8.1': 11473 dependencies: ··· 14042 '@sentry/types': 8.9.2 14043 '@sentry/utils': 8.9.2 14044 14045 + '@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.1(@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))': 14046 dependencies: 14047 '@opentelemetry/api': 1.9.0 14048 '@opentelemetry/semantic-conventions': 1.28.0 ··· 14055 '@sentry/vercel-edge': 8.46.0 14056 '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 14057 chalk: 3.0.0 14058 + next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14059 resolve: 1.22.8 14060 rollup: 3.29.5 14061 stacktrace-parser: 0.1.10 ··· 14675 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14676 typescript: 5.6.2 14677 14678 + '@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.1(@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)': 14679 dependencies: 14680 '@trpc/client': 11.0.0-rc.666(@trpc/server@11.0.0-rc.666(typescript@5.6.2))(typescript@5.6.2) 14681 '@trpc/server': 11.0.0-rc.666(typescript@5.6.2) 14682 + next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 14683 react: 19.0.0 14684 react-dom: 19.0.0(react@19.0.0) 14685 typescript: 5.6.2 ··· 17573 17574 jest-worker@27.5.1: 17575 dependencies: 17576 + '@types/node': 22.10.2 17577 merge-stream: 2.0.0 17578 supports-color: 8.1.1 17579 ··· 18646 18647 netmask@2.0.2: {} 18648 18649 next-auth@5.0.0-beta.25(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18650 dependencies: 18651 '@auth/core': 0.37.2 18652 next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18653 react: 19.0.0 18654 18655 + next-plausible@3.12.4(next@15.1.1(@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): 18656 dependencies: 18657 + next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18658 react: 19.0.0 18659 react-dom: 19.0.0(react@19.0.0) 18660 ··· 18690 - '@babel/core' 18691 - babel-plugin-macros 18692 18693 next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): 18694 dependencies: 18695 '@next/env': 15.1.1 ··· 18789 dependencies: 18790 boolbase: 1.0.0 18791 18792 + nuqs@2.2.3(next@15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): 18793 dependencies: 18794 mitt: 3.0.1 18795 react: 19.0.0 18796 optionalDependencies: 18797 + next: 15.1.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 18798 18799 oauth4webapi@2.10.4: {} 18800 ··· 20342 optionalDependencies: 20343 '@babel/core': 7.24.5 20344 20345 sucrase@3.34.0: 20346 dependencies: 20347 '@jridgewell/gen-mapping': 0.3.5 ··· 20673 '@tsconfig/node14': 1.0.3 20674 '@tsconfig/node16': 1.0.4 20675 '@types/node': 20.14.8 20676 + acorn: 8.11.3 20677 acorn-walk: 8.3.2 20678 arg: 4.1.3 20679 create-require: 1.1.1
+34
process-compose.yaml
···
··· 1 + version: "0.5" 2 + is_strict: true 3 + processes: 4 + init_dependencies: 5 + namespace: init 6 + command: | 7 + pnpm install 8 + is_tty: true 9 + init_turso: 10 + namespace: init 11 + command: | 12 + turso dev --db-file openstatus-dev.db 13 + is_tty: true 14 + availability: 15 + restart: "no" 16 + init_dx: 17 + namespace: init 18 + command: | 19 + pnpm dx 20 + kill $(pgrep -f "turso dev") 21 + is_tty: true 22 + depends_on: 23 + init_dependencies: 24 + condition: process_completed_successfully 25 + init_turso: 26 + condition: process_started 27 + dev: 28 + namespace: dev 29 + command: | 30 + pnpm dev:web 31 + is_tty: true 32 + depends_on: 33 + init_dx: 34 + condition: process_completed_successfully
+7 -1
turbo.json
··· 28 "dev": { 29 "cache": false 30 }, 31 "test": { 32 "cache": false 33 }, ··· 43 "cache": false, 44 "dependsOn": ["env"] 45 }, 46 - "env": {} 47 } 48 }
··· 28 "dev": { 29 "cache": false 30 }, 31 + "@openstatus/web#dev": { 32 + "cache": false, 33 + "dependsOn": ["@openstatus/react#build"] 34 + }, 35 "test": { 36 "cache": false 37 }, ··· 47 "cache": false, 48 "dependsOn": ["env"] 49 }, 50 + "env": { 51 + "cache": true 52 + } 53 } 54 }